4.17 DP专题:最长上升子序列+导弹拦截
最长上升子序列长度+输出答案序列
题意
设有\(n\)个不相同的整数组成的序列\(a[1\cdots n]\),且\(a[i]!=a[j](i!=j)\),若存在\(i_1<i_2<\cdots i_e\),且有\(a[i_1]<a[i_2]<\cdots <a[i_e]\),称为长度为\(e\)的上升序列。
求出最长上升序列的长度及这个最长上升序列。
题解
由于要记录这个最长的上升序列,那么\(n\log{n}\)的非dp做法似乎不可行,用\(n^2\)的dp
来写。
首先,\(dp[i]\)记录从\(1\)到\(i\)的最长上升序列长度,设其初值是1。\(pre[i]\)记录以\(a[i]\)为结尾的子序列的前一个元素位置。
考虑如何更新\(dp\)。
可以列举\(i\),外层循环中找到以\(i\)为结尾的最大序列。
内层循环中:枚举\(j,j\in [1,i)\);
得到如下做法:
状态转移方程:
- 如果\(a[j] < a[i]\),代表当前的\(a[i]\)可以成为已知的以\(a[j]\)结尾的最长序列的最新一位元素(得到新的序列为\(a[\dots],a[\dots],a[j],\)\(a[i]\))。
- \(dp[i] = max(dp[i],dp[j]+1)\);
- 此时如果更新了\(dp[i]\)的值,那么代表得到的序列是目前最新的,长度最长的序列:
- \(i\)的前一位是\(j\),表示为\(pre[i]=j\);
- 记录下当前\(i\)的位置,表示为\(ans=i\);
- 用\(maxn\)记录序列最长长度。
- 这里可以优化成:如果 \(a[j] < a[i] \quad\&\&\quad dp[i] < dp[j] + 1\)
最后输出长度\(maxn\)。
对于输出答案序列,使用递归:
- 先找print(ans),表示从最后一位开始输出,
- 一直向\(pre[x]\)递归下去,直到\(x=0\),表示找到头,回溯。
- 每次回溯时候输出答案即可。
特判一下如果maxn的值没有被更新,则未发现最长序列,直接特殊输出即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f , N = 1e5+5;
int a[N],pre[N],n,dp[N],ans,maxn = -INF;
void print(int x){
if(!x)return;
print(pre[x]);
printf("%d ",a[x]);
}
signed main(){
while(scanf("%d",&a[++n]) != EOF);
n--;
for(int i = 1 ; i <= n ; i ++)
dp[i] = 1;
for(int i = 1 ; i <= n ; i ++)
for(int j = 1 ; j < i ; j ++)
if(a[j] < a[i] && dp[i] < dp[j] + 1){
dp[i] = dp[j] + 1,
pre[i] = j;
if(maxn < dp[i]){
maxn = dp[i],
ans = i;
}
}
if(maxn == -INF){
printf("max=%d\n%d",1,a[1]);
return 0;
}
printf("max=%d\n",maxn);
print(ans);
return 0;
}
导弹拦截
题意
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是\(\le50000\)的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
题解
对于第一问,找到该序列中的最长不上升子序列即可。
对于第二问,找到该序列中的“最长不下将子序列”即可。
以第一问为例,\(O(n^2)\)的思路及算法同前一题,不再赘述。
这里提供一个\(O(n\log n)\)的求最长不下降子序列的算法:
开一个数组\(q\),表示一个“单调栈”,维护可行的最长序列。
由于我们并不关心这个最长序列是什么,所以只需维护数组的长度即可。
对于新加入的元素(设为\(b\),判断:
- 如果\(b\)>栈顶元素,那么直接把它放入栈顶即可。
- 否则(\(b\)小于等于栈顶元素):
- 在当前栈内找到第一个比\(b\)大或等于的元素(lower_bound),将此元素的值更改为\(b\)
这样执行,最后得到的栈的长度即为答案。
一些小问题:
- 在当前栈内找到第一个比\(b\)大或等于的元素(lower_bound),将此元素的值更改为\(b\)
- 为什么它可行:
因为维护的是当前序列单调不下降的栈, 改变其中一个值不会影响长度;在后面加上的数一定是满足所有计入栈内元素的不下降的一个。 - 为什么这样得到的数组不是答案数组
在更新长度之后,如果修改一个元素,就不是成为答案的最长的一个了。
代码
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f , N = 1e5+5;
int a[N],q1[N],q2[N],cnt,n;
signed main(){
while(scanf("%d",&a[++n]) != EOF);
n--;
q1[++cnt] = a[1];
for(int i = 2 ; i <= n ; i ++){
if(a[i] <= q1[cnt]) q1[++cnt] = a[i];
else {
int l = 1 , r = cnt;
while(l < r){
int mid = (l + r) >> 1;
if(q1[mid] < a[i]) r = mid;
else l = mid + 1;
}
q1[l] = a[i];
}
}
printf("%d\n",cnt);
cnt = 0;
q2[++cnt] = a[1];
for(int i = 2 ; i <= n ; i ++){
if(a[i] > q2[cnt])q2[++cnt] = a[i];
else {
int l = 1 , r = cnt;
while(l < r){
int mid = (l + r) >> 1;
if(q2[mid] >= a[i]) r = mid;
else l = mid + 1;
}
q2[l] = a[i];
}
}
printf("%d",cnt);
return 0;
}