专题:最长单调子序列(LIS)问题

本文深入探讨了最长单调子序列(LIS)问题,包括两种解法:一般DP(O(n^2))和队列伪模拟(O(nlogn))。在DP解法中,通过维护dp数组找到以每个元素为结尾的LIS长度,并最终找出最长序列。队列伪模拟方法中,利用单调队列动态维护LIS。文章还提及如何找到LIS的具体序列,并提供了一个完整的代码实现。

专题:最长单调子序列(LIS)问题

(4.27 记录)

LIS问题是DP当中的一个很经典的问题。
LIS问题的核心在于状态的保护和转移。

一.LIS的长度
解法1:一般DP 时间复杂度O(n2)
很显然,对于某一个点i,1-i的最长单调子序列是唯一的,而以i结尾的最长单调子序列也是唯一的。这样就有思路:dp[i]表示以i结尾的最长单调子序列的长度。
这两种思路唯一的区别在于dp是否要转移,或者更直观的来说,区别在于dp[n]是否就是最长单调子序列的长度。
从A思路出发的话,1-i中的最长单调子序列长度一定不短于1-(i-1)的,所以初始情况下dp[i]=dp[i-1];从B思路出发的话,以i为结尾明显和以(i-1)结尾不是一回事,所以初始dp[i]=1.
有递推式:
dp[i] = max(dp[i],dp[j]+1) (i > j && a[i] > a[j])
注意:由于dp[n] ≠ max,需要枚举一遍dp找max。

解法2:队列伪模拟(或者说是贪心) 时间复杂度O(nlogn)
这个办法跟DP我觉得是一点儿关系都没有。
先给结论:维护一个单调的队列Q,记队尾为rear(初始为0),遍历a[i],如果a[i]<Q[rear]则入队,否则用a[i]替换掉Q中第一个不小于它的数。
然后我们解释一下:
现在假设我们维护的一个单调递增队列里面有三个数a,b,c,如果新增一个d,d>c,那么很显然这个队列就可以扩充。
如果d<c,那么我们明确一下:我们现在维护的这个队列并不一定是真实的LIS,对于那个队列abc,其实b可以替换成任意一个(a,c)中的数,其他的数也同理。但是如果我们改的是队尾,很显然队尾越小越好,这样我们更有可能扩充这个序列的长度(靠替换那是没法扩充的),为了队尾越小越好,那前面的肯定也是越小越好,尽管这不是真实的,但是如果队尾真的更新了,那么真实的LIS也就变了;如果没有那就算了,反正我们压根就不关心到底队列里面是什么,我们只关心长度。
大致如下(以单调递增为例):

if(Q[rear} < a[i] Q[++rear] = a[i];
else Q[find(a[i])] = a[i];

(这个find里面怎么写,一会儿会讨论一下)

二.LIS
现在我们关心序列是什么了。
从刚才已有的里面看一看:解法2不行,因为一个不真实的序列压根没用。
所以我们就剩一个解法1可以改一改了。
我们重复一遍解法1的核心:对于一个点i,以它为结尾的LIS唯一确定。
这样一来,我们借鉴一下链表思想就行了,用head[x]记录以x为结尾的最长单调子序列的倒数第二个数,我们一直找head[x]并输出,就能得到一个真实的LIS。这种思想跟拓扑排序非常类似,至于拓扑,上一个记录已经写过了。
由于这是一个LIS里面的重难点,给一个完整的板子:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int head[100001],a[100001];
void find(int x){
	if(~head[x]) find(head[x]);
	printf("%d ",a[x]);
}
int main(){
	int n = 0,i,j,rear,telipu = -1,dp[100001];
	memset(head,-1,sizeof(head));
	while(scanf("%d",&a[++n]) != EOF);
	--n;
	for(i = 1;i <= n;i++) dp[i] = 1;
	for(i = 2;i <= n;i++){
		for(j = 1;j < i;j++){
			if(a[j] < a[i] && dp[j] + 1 > dp[i]){//只留下更优的改变
				dp[i] = dp[j] + 1;
				head[i] = j;
				if(telipu < dp[i]){
					telipu = dp[i];
					rear = i;
				}
			}
			//dp[i] = max(dp[i],dp[j]);
		}
	}
	printf("max=%d\n",telipu);
	find(rear);
	return 0;
}

彩蛋:(P1091 合唱队型)
这题是一部分递增一部分递减,由于整个序列不变,所以预处理一边之后O(n)的找就行了。时间复杂度O(nlogn).
讨论一下find:由于这题是严格单调,所以我们处理出来的队列里面也不应该有任何相等的数。这样一来,对于Q[mid]=a[i],应该把a[i]替换掉,所以判断条件带等。
附上代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int Q[1001],rear,n;
int find(int x){
	int l = 1,r = rear,mid;
	while(l < r){
		mid = (l + r) >> 1;
		if(Q[mid] >= x) r = mid;
		else l = mid + 1;
	}
	return l;
}
int main(){
	int a[1001],b[1001],c[1001],i;
	scanf("%d",&n);
	memset(Q,-1,sizeof(Q)); 
	for(i = 1;i <= n;i++) scanf("%d",&a[i]);
	for(i = 1;i <= n;i++){
		if(Q[rear] < a[i]) Q[++rear] = a[i];
		else Q[find(a[i])] = a[i];
		b[i] = rear;
	}
//---------------------------------------------------------
	memset(Q,-1,sizeof(Q));
	rear = 0;
	for(i = n;i >= 1;i--){
		if(Q[rear] < a[i]) Q[++rear] = a[i];
		else Q[find(a[i])] = a[i];
		c[i] = rear;
		//printf("%d ",Q[rear]);
	}
	//printf("\n");
	rear = 0;
	for(i = 1;i <= n;i++){
		rear = max(rear,b[i] + c[i] - 1);
		//printf("%d %d\n",b[i],c[i]);
	}
	printf("%d\n",n - rear);
	return 0;
}
/*
10
1 7 5 3 2 7 4 5 8 6
*/

Thank you for reading!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值