用户工具

站点工具


2020-2021:teams:manespace:最长上升子序列

最长上升子序列 (LIS)

最长不下降及下降子序列请自行考虑关系运算符变化

问题描述

给定(非负)整数序列{a1, a2, …, an},求其一个子序列,使得元素呈升序,且子序列长度最长,输出长度(输出序列)

例:{1, 6, 4, 2, 3, 9, 8} 的LIS为{1, 2, 3, 9} 或 {1, 2, 3, 8}

解题思路

Solution1 : DP

设 $dp[i]$ 表示以 $a[i]$ 为结尾的 $LIS$ 长度, 初始化为 $1$ (子序列至少包含自己)

为保证升序,状态转移需从 $a[i]$ 之前 比 $a[i]$ 的元素转移而来。你说,是不是啊

则状态转移方程 $dp[i] = max(dp[i], dp[j] + 1)$ ,其中 $(j < i) a[j] < a[i]$, 复杂度$O(n^2)$。你说,是不是啊*2

如需输出任意一解,可设置一追踪标记 $tr$,与 $ans$ 同步更新为当前位置 $i$

设 $ans$ 在 $dp[k]$ 处取到,则对于 $i < k$ 都有 $dp[i] < dp[k]$ (废话),且至少有一个序列满足 $dp$ 数组以 1 为单位递增你说,是不是啊*3

如{1, 6, 4, 2, 3, 9, 8}的 $dp$ 数组元素为{1, 2, 2, 2, 3, 4, 4}

那么只需从 $tr$ 遍历到 $1$,遇到 $a[tr] == ans$ 则 $a[tr]$ 压栈,同时 $ans -= 1$,最后弹栈输出。你说,是不是啊*4

for(int i = 1; i <= n; i++)  dp[i] = 1;
int ans = -1, tr;
for(int i = 1; i <= n; ++i){
	for(int j = 1; j < i; ++j){
		if(a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1);
                ans = max(ans, dp[i]);
                if(dp[i] >= ans) tr = i, ans = dp[i];/*也可不加等号*/
	}
}

printf("%d\n", ans);
while(tr){
	if(dp[tr] == ans) z.push(a[tr]), --ans;
	--tr;
}
while(!z.empty()) printf("%d ", z.top()), z.pop();

Solution 2 : 二分+贪心

因为贪心所以某些条件下会受限制。你说,是… 草,不说了

设 $f[i]$ 表示长度为 $i$ 的 $LIS$ 末尾元素的最小值,这个数组一定是单调不下降的。

置 $f[1] = a[1]$ ,取 $cnt = 1$ 表示 $LIS$ 的长度,遍历数组 $a$ :

$a[i] > f[cnt]$ 则 $f[++cnt] = a[i]$

$a[i] < f[cnt]$ ,找到 $f$ 中第一个大于 $a[i]$ 的位置并替换为 $a[i]$

这样做的替换并不会影响当前 $LIS$ 的长度变化,只改变了数组 $f$ 中某个位置的值并让其尽可能小,以便于不断获得更优解,所以不影响 $LIS$ 长度的正确性,但不能用来追踪解,可想而知。

因为数组 $f$ 有序,所以可用二分,复杂度 $O(nlogn)$

f[1] = a[1];
int cnt = 1;
for(int i = 2; i <= n; ++i){
	if(a[i] > f[cnt]) f[++cnt] = a[i];
	else if(a[i] < f[cnt]){
		int j = upper_bound(f + 1, f + cnt + 1, a[i]) - f;/*得到数组f中第一个大于a[i]的位置,也可手写二分*/
		f[j] = a[i];
	}
}

最终 $f[cnt]$ 即为答案

Solution 3 : 转LCS

顾名思义,将数组 $a$ 排序得到新数组 $b$,问题转化为求 $a,b$ 的最长公共子序列,长度分别为 $n,m$

此处只给出其 $DP$ 状态转移方程,以 $dp[i][j]$ 表示 $a[i]$ 和 $b[j]$ 的LCS长度,状态转移方程 $dp[i][j] =$

0 ;(i == 0 || j == 0)
dp[i-1][j-1] + 1 ;(a[i] == b[j])
max{dp[i][j-1], dp[i-1][j]} ;(a[i] != b[j])

解的追踪(不予详解):设一标记数组 $tr[n][m]$ ,标记依据 $dp$ 方程设立,最后从 $tr[n][m]$ 回溯寻找 $a[i] == b[j]$的标记,压栈即可

伪码

for(int i = 1; i <= n; ++i){
	for(int j = 1; j <= m; ++j){
		if(a[i] == b[j]){
			dp[i][j] = dp[i-1][j-1] + 1;
			tr[i][j] = 3;
		} 
		else{
			dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
			tr[i][j] = dp[i-1][j] > dp[i][j-1] ? 1 : 2;
		}
	}
}。

int x = n, y = m, t;
int dx[4] = {INF, -1, 0, -1};
int dy[4] = {INF, 0, -1, -1};
stack <int>z;
while(tr[x][y]){
	if(tr[x][y] == 3) z.push(a[x]);
	t = tr[x][y];
	x += dx[t];	
	y += dy[t];
}
while(!z.empty()){
	printf("%d ", z.top());
	z.pop();
}

答案即为 $dp[n][m]$

Solution 4 : 据说可以用树状数组?

树状数组可参考本队上一专题→树状数组

但是到考期了你懂的

这里不讲了,也是 $O(nlogn)$ 的复杂度,有需要自行查找吧

2020-2021/teams/manespace/最长上升子序列.txt · 最后更改: 2020/06/04 15:53 由 intouchables