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\)
      这样执行,最后得到的栈的长度即为答案。
      一些小问题:
  1. 为什么它可行:
    因为维护的是当前序列单调不下降的栈, 改变其中一个值不会影响长度;在后面加上的数一定是满足所有计入栈内元素的不下降的一个。
  2. 为什么这样得到的数组不是答案数组
    在更新长度之后,如果修改一个元素,就不是成为答案的最长的一个了。

代码

#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;
}
posted @ 2021-04-17 16:42  Last-Order  阅读(80)  评论(0编辑  收藏  举报