最长上升子序列
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
最长上升子序列
顾名思义,我们要求序列中最长的单调增的子序列(不一定连续)的长度。
很容易想到朴素的做法:设为以结尾的LIS,则转移方程为。
这是一个所谓的1D/1D动态规划问题,状态数和单次转移的时间复杂度都是。
然而,它可以利用二分优化到,这篇文章重点就介绍的写法。
考虑维护一个数组dp[]及当前长度len,dp[i]表示长度为i的上升子序列的最后一个元素的最小值。
然后考虑如何转移:
现在我们有一个新元素a,对比dp的最后一个元素。
如果a > dp[len] ,那么可以直接把a “接到”dp[len]的后面,形成更长的上升子序列。
即:dp[++len] = a;
否则,找到dp中第一个大于等于a的元素,用a替换它。
这样替换后既保证仍形成上升子序列,又使得该上升子序列的最后元素更小。
如果你看懂了上面这些话,你可能有基础或者天赋异禀;否则,我们还是来看例子吧。
现在有序列4,8,9,5,6,7,2,7求LIS。
一个一个元素来看,首先无疑dp[1]=4 ,然后考察8,8>4,故把8加入尾部。
然后9>8,也进入尾部,这时dp数组是{4, 8, 9}。
下一个元素是5,5<9,不能塞入尾部。
我们找到第一个大于等于5的元素,是8。
4->8是长度为2的上升子序列,4->5也是,但是5比8更小,所以更有潜力更新后面的子序列。
所以把8换成5,现在dp是{4, 5, 9}。
同样的道理dp又变成{4, 5, 6}。
现在我们尝到甜头了,因为下一个元素是7,本来是没有机会进入序列的,现在却可以了。
于是dp变成{4, 5, 6, 7}。
注意,显然dp是递增的(两种转移都不会破坏递增性),但这并不意味着它就是所求的上升子序列,你看,下一个元素是2,它会把dp更新成{2, 5, 6, 7},但原序列并没有一个子序列是{2, 5, 6, 7}。
最后剩一个元素7,由于我们在求严格上升的子序列,不能将它插入尾部,于是我们把7替换成7——这个元素对子序列长度没有贡献。
好了,最后得到的数组长度是4,所以最长上升子序列的长度就是4 。
动图封面
刚刚提到,dp是递增的,所以我们不用每次都扫描一遍数组,而可以进行二分查找。
这样,就把复杂度降到了,具体地,代码如下:
int len = 0;
for (int i = 0; i < n; ++i)
{
if (dp[len] < A[i])
dp[++len] = A[i];
else
*lower_bound(dp + 1, dp + len + 1, A[i]) = A[i];
}
这里使用了<algorithm>中的lower_bound函数进行二分查找,它返回一个迭代器,相当于指针,所以要用*解引用。
类似地,我们也可以求最长不下降子序列、最长不上升子序列、最长下降子序列等,只需要修改一下插入到数组尾部的条件,以及二分查找的部分即可。
注意lower_bound是查找第一个大于等于某值的元素,如果是求最长不下降子序列,我们需要用到upper_bound,查找第一个大于某值的元素。
(因为最长不下降子序列允许把3->4替换成3->3)
另外lower_bound只能用于不减序列,而最长下降子序列维护的是不增序列,所以要改成:
*lower_bound(dp + 1, dp + len + 1, A[i], greater<int>()) = A[i];
其中greater是STL中提供的仿函数模板。
这里有一道经典题目:
(1999年NOIP普及组导弹拦截)
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是\le 50000≤50000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
1行,若干个整数(个数≤100000)
输出格式
2行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入输出样例
输入
389 207 155 300 299 170 158 65
输出
6
2
第一问,很显然就是求最长不上升子序列,但第二问呢?看起来有点棘手,幸运的是,我们有一个数学定理。
(Dilworth定理)
对于一个偏序集,最少链划分等于最长反链长度。
虽然用了好几个专业名词,但我们还是能意会其中的含义(觉不觉得和之前文章中提到的König定理有相似之处?)。
我们现在要把这个序列划分成最少数量的不上升子序列,那么我们只需要反过来求最长上升子序列的长度。
代码如下:
#include <bits/stdc++.h>
using namespace std;
int A[100005], dp[100005];
int main()
{
int n = 0, len = 0;
while (cin >> A[n])
n++;
memset(dp, 127, sizeof(dp)); // 其实这里只需要初始化dp[0]为INF即可
for (int i = 0; i < n; ++i)
{
if (dp[len] >= A[i])
dp[++len] = A[i];
else
*upper_bound(dp + 1, dp + len + 1, A[i], greater<int>()) = A[i];
}
cout << len << endl;
len = 0;
memset(dp, 0, sizeof(dp));
for (int i = 0; i < n; ++i)
{
if (dp[len] < A[i])
dp[++len] = A[i];
else
*lower_bound(dp + 1, dp + len + 1, A[i]) = A[i];
}
cout << len << endl;
return 0;
}
LIS虽然是动态规划中比较基础的问题,但其实它的思想是比较巧妙的。
而且有不少DP的题目最后会转化为LIS的问题,如木棍加工、最长公共子序列等。
(2020.12.7更新)
上面讲的这种方法很巧妙,但也没那么容易想到。
反而如果熟悉二维偏序的话,会发现这就是个类似二维偏序的问题,第一个属性是,第二个属性是。
所以我们也可以模仿二维偏序的套路,用树状数组(注意树状数组可以求前缀最大值,只是不能求区间最大值而已)优
化朴素dp从而解决LIS问题,即:
int tree[MAXN], A[MAXN];
int lowbit(int x) { return x & -x; }
void update(int x, int d)
{
for (int i = x; i < MAXN; i += lowbit(i)) tree[i] = max(tree[i], d);
}
int query(int x)
{
int ans = 0;
for (int i = x; i > 0; i -= lowbit(i))
ans = max(tree[i], ans);
return ans;
}
int main()
{
ios::sync_with_stdio(false);
int n, x, ma = 0;
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> A[i];
for (int i = 1; i <= n; ++i)
{
x = query(A[i] - 1) + 1;
ma = max(ma, x);
update(A[i], x);
}
cout << ma << endl;
return 0;
}。