【动态规划】 最长上升子序列模型——进阶

拦截导弹

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。

由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于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 1n50

输入样例:
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 1N3000,序列中的数字均不超过 2 31 − 1 2^{31}−1 2311

输入样例:
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[1i] b [ 1 − j ] b[1 - j] b[1j]中以 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[i1][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[i1][1]+1
  • 子序列的倒数第二个数是 b [ j − 1 ] b[j - 1] b[j1]的集合,最大长度是 d p [ i − 1 ] [ j − 1 ] + 1 dp[i - 1][j - 1] + 1 dp[i1][j1]+1

如果直接按上述思路实现,需要三重循环:见下面代码中的第一个

这样肯定会超时,我们需要优化,每次循环中,我们要更新的 d p [ i ] [ j ] dp[i][j] dp[i][j]就是在前面小于 b [ j ] b[j] b[j]的所有数 b [ k ] b[k] b[k]中找一个最长的,而 b [ j ] b[j] b[j]只有等于 a [ i ] a[i] a[i]时,也就是这种包括 a [ i ] a[i] a[i]的情况, a [ i ] a[i] a[i]肯定要和 b [ j ] b[j] b[j]相等作为最后一个数,才会有贡献。我们可以将计算在前面小于 b [ j ] b[j] b[j]的所有数 b [ k ] b[k] b[k]中找一个最长的这一部分优化,就是在前面小于 a [ i ] a[i] a[i]的所有数 b [ k ] b[k] b[k]中找一个最长的,在每一次 j j j循环后更新即可。

在代码中,我们这样写
每次循环求得的 m a x x maxx maxx是满足 a [ i ] > b [ k ] a[i] > b[k] a[i]>b[k] d p [ i − 1 ] [ k ] + 1 dp[i - 1][k] + 1 dp[i1][k]+1的前缀最大值。
因此可以直接将 m a x x maxx maxx提到第一层循环外面,减少重复计算,此时只剩下两重循环。
最终答案枚举子序列结尾取最大值即可。

代码:
1. O ( n 3 ) O(n^3) O(n3)

#include <bits/stdc++.h>
#define ll long long
using namespace std;
int n,a[3005],b[3005];
int dp[3005][3005];//dp[i][j]表示第一个序列的前i个字母和第二个序列的前j个字母构成的,且以b[j]结尾的公共上升子序列
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    for(int j=1;j<=n;j++)scanf("%d",&b[j]);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            dp[i][j]=dp[i-1][j];
            if(a[i]==b[j]){
                dp[i][j]=max(dp[i][j],1);
                for(int k=1;k<j;k++){
                    if(b[k]<b[j]){
                        dp[i][j]=max(dp[i][j],dp[i-1][k]+1);
                    }
                }
            }
        }
    }
    int res=0;
    for(int i=1;i<=n;i++)res=max(res,dp[n][i]);
    printf("%d\n",res);
    return 0;
}

2. O ( n 2 ) O(n^2) O(n2)

#include <bits/stdc++.h>
#define ll long long
using namespace std;
int n,a[3005],b[3005];
int dp[3005][3005];//dp[i][j]表示第一个序列的前i个字母和第二个序列的前j个字母构成的,且以b[j]结尾的公共上升子序列
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    for(int j=1;j<=n;j++)scanf("%d",&b[j]);
    for(int i=1;i<=n;i++){
        int maxx=1;
        for(int j=1;j<=n;j++){
            dp[i][j]=dp[i-1][j];
            if(a[i]==b[j])dp[i][j]=max(dp[i][j],maxx);
            if(b[j]<a[i])maxx=max(dp[i-1][j]+1,maxx);
        }
    }
    int res=0;
    for(int i=1;i<=n;i++)res=max(res,dp[n][i]);
    printf("%d\n",res);
    return 0;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

a碟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值