动态规划 最长上升子序列模型——进阶
Posted a碟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划 最长上升子序列模型——进阶相关的知识,希望对你有一定的参考价值。
拦截导弹
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
共一行,输入导弹依次飞来的高度。
输出格式
第一行包含一个整数,表示最多能拦截的导弹数。
第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。
数据范围
雷达给出的高度数据是不大于
30000
30000
30000 的正整数,导弹数不超过
1000
1000
1000。
输入样例:
389 207 155 300 299 170 158 65
输出样例:
6
2
分析: 这个题有两个小问,第一是问最多能拦截多少导弹,而我们一次能够拦截的导弹要求是后一发不能高于前一发,也就是说要求一个最长非严格递减子序列。
第二问拦截所有导弹最少要配备多少套这种导弹拦截系统,也就是求最少有多少个最长非严格递减子序列。这一问我们要使用贪心的策略。
策略如下:
从前往后扫每一个数,对于每个数
1.如果现在的所有子序列的结尾都小于当前的数(也就是说当前的导弹无法被现有的任何拦截系统拦截),则创建新的子序列
2.将当前数放在结尾大于等于它的最小的子序列的后面,这样我们就让所有序列剩下的能够放的数字尽可能的多
这样的贪心策略是最优的,简单证明一下。
如何证明两个数相等?
A
>
=
B
,
B
>
=
A
=
>
A
=
B
A>=B,B>=A =>A=B
A>=B,B>=A=>A=B
A
A
A:贪心所得到的序列的个数
B
B
B:最优解(最少的序列个数)
A
>
=
B
A>=B
A>=B是显然成立的,因为B是最优解
证明:
B
>
=
A
B>=A
B>=A:(调整法)
假设最优解对应的方案和当前方案不同。
找到第一个不同的数。
假设我们贪心法的x和最优解的x放在了上图的位置,那么有x<=a<=b,我们显然可以将a后面的一段和b后面的一段交换位置,这样是不会影响最后的最优解的。所以我们经过很多次的变化,我们没有增加子序列的个数,可以将贪心法变换到最优解的情况。所以得到A>=B。所以我们就可以按照这样的贪心策略来求最优解。
实际上这有一个Dilworth定理,可以自行搜索。
实现方法:我们维护一个数组ans,存储每一个序列的最后一个值,每一次增加一个数,如果当前数比ans中的所有数都大,也就是说当前的导弹无法被现有的任何拦截系统拦截,那么我们就要新开一个序列来存这个值。
否则,找到ans中比当前数大的数的最小值,替换ans数组中的这个值为当前数。
不难看出,ans数组是一个单调递增序列,可以使用二分来简化时间复杂度。
而这样的贪心策略和我们的最长递增子序列的贪心求解法
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)是一样的。所以第二问可以转化为求一个严格最长递增子序列。
代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int ans[1005],n,a[1005],k;
int main()
{
while(~scanf("%d",&a[++n]));
n--;
for(int i=1;i<=n;i++){
int id=upper_bound(ans,ans+k,a[i],greater<int>())-ans;
if(id<k)ans[id]=a[i];
else ans[k++]=a[i];
}
printf("%d\\n",k);
k=0;
for(int i=1;i<=n;i++){
int id=lower_bound(ans,ans+k,a[i])-ans;
if(id<k)ans[id]=a[i];
else ans[k++]=a[i];
}
printf("%d\\n",k);
return 0;
}
导弹防御系统
为了对抗附近恶意国家的威胁, R R R 国更新了他们的导弹防御系统。
一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。
例如,一套系统先后拦截了高度为 3 和高度为 4 的两发导弹,那么接下来该系统就只能拦截高度大于 4 的导弹。
给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。
输入格式
输入包含多组测试用例。
对于每个测试用例,第一行包含整数 n n n,表示来袭导弹数量。
第二行包含 n n n 个不同的整数,表示每个导弹的高度。
当输入测试用例 n = 0 n=0 n=0 时,表示输入终止,且该用例无需处理。
输出格式
对于每个测试用例,输出一个占据一行的整数,表示所需的防御系统数量。
数据范围
1
≤
n
≤
50
1≤n≤50
1≤n≤50
输入样例:
5
3 5 2 4 1
0
输出样例:
2
样例解释
对于给出样例,最少需要两套防御系统。
一套击落高度为 3 , 4 3,4 3,4 的导弹,另一套击落高度为 5 , 2 , 1 5,2,1 5,2,1 的导弹。
分析: 这个题相比于上一个题,多了一个条件,也就是导弹拦截系统拦截的导弹既可以一直严格单调上升,也可以严格单调下降。而且
n
n
n最大也才
50
50
50。我们可以直接搜索,我们先考虑搜索顺序
搜索顺序分为两个阶段:
- 从前往后枚举每颗导弹属于某个上升子序列,还是下降子序列;
- 如果属于上升子序列,则枚举属于哪个上升子序列(包括新开一个上升子序列);如果属于下降子序列,可以类似处理。
用
u
p
[
k
]
up[k]
up[k]和
d
o
w
n
[
k
]
down[k]
down[k]记录第k套上升(下降)系统目前所拦截的最后一个导弹
d
f
s
(
t
,
u
,
v
)
dfs(t,u,v)
dfs(t,u,v)意味着已有
u
u
u个上升,
v
v
v个下降,正在处理第
t
t
t个数
放在上升/下降系统中的哪个位置和上一道题目所找的方法是一样的。
注意:
- u p up up数组所存的是严格单调上升的导弹系统的最后一个数,上升导弹系统的最后一个数是非严格递减的
- d o w n down down数组所存的是严格单调下降的导弹系统的最后一个数,下降导弹系统的最后一个数是非严格递增的
可以看出,我们的搜索空间好像挺大的,如果我们当前两种系统的数量已经大于等于最优值了,我们可以剪枝。其实这个题手写二分常数会小一些,跑的会快很多。或者直接顺序搜索第一个小于 x x x的数,第一个大于 x x x的数,对于这个题来说也会快一些(应该是数据的原因)。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#define ll long long
using namespace std;
int n,ans=105;
int a[55],up[55],down[55];
void dfs(int cnt,int up_num,int down_num){
if(up_num+down_num>=ans)return ;
if(cnt==n+1){
ans=up_num+down_num;
return ;
}
//将当前数放在严格单调上升的导弹系统中,上升导弹系统的最后一个数是非严格递减的
int id=upper_bound(up,up+up_num,a[cnt],greater<int>())-up,temp;
if(id<up_num)temp=up[id],up[id]=a[cnt],dfs(cnt+1,up_num,down_num),up[id]=temp;
else up[up_num]=a[cnt],dfs(cnt+1,up_num+1,down_num);
//将当前数放在严格单调下降的导弹系统中,下降导弹系统的最后一个数是非严格递增的
id=upper_bound(down,down+down_num,a[cnt])-down;
if(id<down_num)temp=down[id],down[id]=a[cnt],dfs(cnt+1,up_num,down_num),down[id]=temp;
else down[down_num]=a[cnt],dfs(cnt+1,up_num,down_num+1);
}
int main()
{
while(~scanf("%d",&n)){
if(n==0)break;
ans=105;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
dfs(1,0,0);
printf("%d\\n",ans);
}
return 0;
}
最长公共上升子序列
熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。
小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。
小沐沐说,对于两个数列 A A A 和 B B B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。
奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。
不过,只要告诉奶牛它的长度就可以了。
数列 A A A 和 B B B 的长度均不超过 3000 3000 3000。
输入格式
第一行包含一个整数
N
N
N,表示数列
A
A
A,
B
B
B 的长度。
第二行包含 N N N 个整数,表示数列 A A A。
第三行包含 N N N 个整数,表示数列 B B B。
输出格式
输出一个整数,表示最长公共上升子序列的长度。
数据范围
1
≤
N
≤
3000
1≤N≤3000
1≤N≤3000,序列中的数字均不超过
2
31
−
1
2^{31}−1
231−1。
输入样例:
4
2 2 1 3
2 1 2 3
输出样例:
2
分析: 动态规划题,结合了LIS和LCS,在状态表示和状态计算上融合了两者
状态表示:
- d p [ i ] [ j ] dp[i][j] dp[i][j]代表所有 a [ 1 − i ] a[1 - i] a[1−i]和 b [ 1 − j ] b[1 - j] b[1−j]中以 b [ j ] b[j] b[j]结尾的公共上升子序列的集合;
- d p [ i ] [ j ] dp[i][j] dp[i][j]的值等于该集合的子序列中长度的最大值;
状态计算(对应集合划分):
首先依据公共子序列中是否包含
a
[
i
]
a[i]
a[i],将
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]所代表的集合划分成两个不重不漏的子集:
- 不包含 a [ i ] a[i] a[i]的子集,最大值是 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i−1][j];
- 包含 a [ i ] a[i] a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:
- 子序列只包含 b [ j ] b[j] b[j]一个数,长度是1;
- 子序列的倒数第二个数是
b
[
1
]
b[1]
b[1]的集合,最大长度是
d
p
[
i
−
1
]
[
1
]
+
1
dp[i - 1][1] + 1
dp[i−1][1]+1;
… - 子序列的倒数第二个数是 b [ j − 1 ] b[j - 1] b[j−1]的集合,最大长度是 d p [ i − 1 ] [ j − 1 ] + 1 dp[i - 1][j - 1] + 1 dp[i−1][j−1]+1;
如果直接按上述思路实现,需要三重循环:见下面代码中的第一个
这样肯定会超时,我们需要优化,每次循环中,我们要更新的 d p [ i ] [ j ] dp[i][j] d以上是关于动态规划 最长上升子序列模型——进阶的主要内容,如果未能解决你的问题,请参考以下文章