【LIS】Luogu P1020 导弹拦截
昨天晚上看蓝书,看到了LIS问题的优化解法。
是比O(n方)更快的解法,实际上是一个常数优化。
先讲一下朴素的解法:
一个集合a,a[i]是第i个元素。设dp[i]为以编号为i的元素结尾的最长不上升子序列。
找到状态转移:
dp[i] = max{dp[j]}+1 (j < i && a[j] >= a[i])
这样的解法是O(n方)的。
如何让它更快,我们可以先推出一个结论:
长度相同的序列,最后元素或者最前元素后于另一个序列的最后元素或者最前元素的序列更优。
证明:
dp[i]是编号i元素结尾的最长不上升子序列,所以这个子序列的结尾是编号i。
前一个导弹越高,更可能拦截下一个导弹,所以a[i]越大越好。
在当前最长不上升子序列中,只有最后一个元素决定了下一个导弹是否被拦截,由上上一行得到子序列中(不上升)最后一个元素越大越好。
所以,我们的命题就变成了:长度相同的子序列(不上升),越往后,子序列的最后一个元素越大。
因为如果较往后的序列一定选中了较往前的序列中没有的数字,而且它的最后一个元素显然不可能被前序列选取,由于他们长度相同,所以它也一定舍弃了前序列的某些数字。
因为它们都是不上升子序列,由此可推出舍弃的元素一定小于后序列的最后一个元素(和某些元素),而最后一个元素(不管哪个序列)都一定是这个序列中最小的。
又因为前序列最后元素大于后序列某些元素,且后序列最后元素是其序列中最小的。
所以后序列最后元素大于前序列最后元素,证毕。
既然我们知道每一个长度的子序列在每个状态中都有最优解,我们就设g[i]是长度为i的最优子序列结尾的编号。
通过上述的证明,我们知道g[i]是长度为i的最优子序列(的结尾)。
然后使用一个变量t来记录当前求出的最长不上升子序列。
所以,从g[t],g[t-1],g[t-2] ... g[1] 遍历,一旦找到其中一个结尾高于当前元素(i),我们就可以得到dp[i]。这时候,因为我们的i一定比g[dp[i]]大,所以我们更新g[dp[i]]。
然后通过dp[i]来更新t,t = max(t,dp[i])。
这样,优化就完成了。
可以注意到这道题还有”第二问“。
使用 Dilworth定理:偏序集的最少反链划分数等于最长链的长度
虽然并不是很明白是怎么回事,但是它将第二问转化为求一个最长上升子序列的长度。(将这些导弹划分为一些不上升子序列)
还有一种理解方法是将这些导弹划分为一些不上升子序列(一次一次地在当前集中选出最长子序列,然后去掉这个序列)
这样,每两个个序列之所以被分为每两个序列,是因为它们之间不满足单调不升。所以,可以把每个序列理解成一个元素,这些元素一定是上升的。
又因为这些序列都是不上升子序列,因此除了每个序列上升的元素,其他元素都被排斥在外,不是最长上升子序列的一部分。
这样,我们的问题是要求组数,所以我们只要求其最长上升子序列即可。
至于代码,就很好写了,只需要从前往后递推,对于每个元素都从t到1枚举一遍,找到最长的符合当前情况的最优序列即可。(求不升,上升子序列的代码几乎一样)
代码:
#include<iostream> #include<vector> #include<cstdio> #include<queue> #include<map> #include<cstdlib> #include<cmath> #include<algorithm> #include<set> #include<cstring> using namespace std; typedef long long ll; const ll INF=99999999; const int MaxN = 100010; int dp[MaxN],g[MaxN],a[MaxN],n,t; inline int Max(int a,int b){ if(a>b) return a; return b; } int main() { //freopen("testdata.in","r",stdin); //freopen("testdata.out","w",stdout); while(~scanf("%d",&a[++n])); n--; //scanf("%d",&n); //for(int i = 1;i <= n; i++) scanf("%d",&a[i]); t = 1; dp[1] = 1; for(int i = 1;i <= n; i++){ dp[i] = 1; for(int j = t;j >= 1; j--){ if(a[i] <= a[g[j]]){ dp[i] = dp[g[j]]+1; break; } } t = Max(t,dp[i]); g[dp[i]] = i; } printf("%d\n",t); t = 1; for(int i = 1;i <= n; i++){ dp[i] = 1; for(int j = t;j >= 1; j--){ if(a[i] > a[g[j]]){ dp[i] = dp[g[j]] + 1; break; } } t = Max(t,dp[i]); g[dp[i]] = i; } printf("%d",t); return 0; }