专题:最长单调子序列(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])