动态规划专项---最长上升子序列模型


文章目录

  • 怪盗基德的滑翔翼
  • 登山
  • 合唱队形
  • 友好城市
  • 最大上升子序列和
  • 拦截导弹
  • 导弹防御系统
  • 最长公共上升子序列

一、怪盗基德的滑翔翼OJ链接

       本题思路:本题是上升子序列模型中比较简单的模型,分别是从前往后和从后往前走一遍LIS即可。

#include <bits/stdc++.h>

constexpr int N=110;

int n;
int h[N];
int f[N];

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);std::cout.tie(nullptr);
    
    int T;
    std::cin>>T;
    while(T--){
        std::cin>>n;
        
        for(int i=1;i<=n;i++) std::cin>>h[i];
        
        int res=0;
        //正向求解LIS问题
        for(int i=1;i<=n;i++){
            f[i]=1;
            for(int j=1;j<i;j++)
                if(h[j]<h[i]) f[i]=std::max(f[i],f[j]+1);
            res=std::max(f[i],res);
        }
        
        memset(f,0,sizeof f);//这里需要将此时的f数组进行清零处理
        //反向求解LIS问题
        for(int i=n;i>=1;i--){
            f[i]=1;
            for(int j=n;j>i;j--)
                if(h[j]<h[i]) f[i]=std::max(f[i],f[j]+1);
            res=std::max(f[i],res);
        }
        std::cout<<res<<std::endl;
    }
    return 0;
}

二、登山OJ链接

        本题思路:状态表示f[i]表示以第 i个位置作为当前子序列的右端点的值,g[i]表示以第 i
个位置作为当前子序列的左端点的值。状态属性:f[i]求最长上升子序列的最多景点个数,g[i]求最长下降子序列的最多景点个数。状态计算:f[i]=max(f[i],f[j]+1)(1≤j<i≤n),g[i]=max(g[i],g[j]+1)(1≤i<j≤n)
求新数组最长上升子序列就是求原数组最长下降子序列答案表示:根据每一个点来划分:ans=max(包含 i的最长上升子序列 + 包含 i的最长下降子序列)所以 ans=max(f[i]+g[i]−1)。

#include <bits/stdc++.h>

constexpr int N=1010;

int n;
int h[N];
int f[N],g[N];//f用来表示上升子序列 g表示下降子序列

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);std::cout.tie(nullptr);
    
    std::cin>>n;
    
    for(int i=1;i<=n;i++) std::cin>>h[i];
    
    for(int i=1;i<=n;i++){//求出上升子序列
        f[i]=1;
        for(int j=1;j<i;j++)
            if(h[j]<h[i])
                f[i]=std::max(f[i],f[j]+1);
    }
    
    for(int i=n;i>=1;i--){//求出下降子序列
        g[i]=1;
        for(int j=n;j>i;j--)
            if(h[j]<h[i])
                g[i]=std::max(g[i],g[j]+1);
    }
    
    int res=0;
    for(int i=1;i<=n;i++)
        res=std::max(res,f[i]+g[i]-1);
    std::cout<<res<<std::endl;
    
    return 0;
}

三、合唱队形OJ链接

        本题思路:本题与上面一题差不多。

#include <bits/stdc++.h>

constexpr int N=1010;

int n;
int h[N];
int f[N],g[N];

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);std::cout.tie(nullptr);
    
    std::cin>>n;
    for(int i=1;i<=n;i++) std::cin>>h[i];
    
    for(int i=1;i<=n;i++){
        f[i]=1;
        for(int j=1;j<i;j++)
            if(h[j]<h[i])
                f[i]=std::max(f[i],f[j]+1);
    }
    
    for(int i=n;i>=1;i--){
        g[i]=1;
        for(int j=n;j>i;j--)
            if(h[j]<h[i])
                g[i]=std::max(g[i],g[j]+1);
    }
    
    int res=0;
    for(int i=1;i<=n;i++) res=std::max(res,f[i]+g[i]-1);
    std::cout<<n-res<<std::endl;
    
    return 0;
}

四、友好城市OJ链接

        本题思路:这里的我们可以先想想交叉和不交叉的情况下分别画出来的图是什么样子的。我们可以发现,在两座桥不交叉的情况下,有a1<a2,b1<b2,在两座桥相交的情况下,有a1<a2,b2<b1所以我们可以发现两桥是否交叉是可以用北岸城市编号的大小关系给反应出来的。所以我们的思路就可以转化为以南岸的城市编号为基准,把所有的城市对从小到大排序,从而得到关于北岸城市的一个数组。因为当bi<bj时能成功建一座桥,所以问题就转化成了求取这个数组的最长上升子序列。到此,我们对北方城市的数组做一次 LIS就可以得到答案了。

#include <bits/stdc++.h>

#define x first
#define y second

typedef std::pair<int, int> PII;
constexpr int N=5010;

int n;
int f[N];
PII line[N];

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);std::cout.tie(nullptr);
    
    std::cin>>n;
    for(int i=1;i<=n;i++)
        std::cin>>line[i].x>>line[i].y;
    
    std::sort(line+1,line+n+1);
    
    int res=0;
    for(int i=1;i<=n;i++){
        for(int j=0;j<i;j++)
            if(line[j].y<line[i].y)
                f[i]=std::max(f[i],f[j]+1);
        res=std::max(res,f[i]);
    }
    std::cout<<res<<std::endl;
    return 0;
}

五、最大上升子序列和OJ链接

        本题思路:本题与上升子序列类似不做详解了。

#include <bits/stdc++.h>

constexpr int N=1010;

int n;
int a[N];
int f[N];//状态表示:表示当前的状态是以a[i]为结尾的最长上升子序列的和

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);std::cout.tie(nullptr);
    
    std::cin>>n;
    for(int i=1;i<=n;i++) std::cin>>a[i];
    
    int res=0;
    for(int i=1;i<=n;i++){
        f[i]=a[i];//假设当前状态的最大值为a[i];
        for(int j=1;j<i;j++)
            if(a[j]<a[i]) //状态计算:首先是否是满足上升子序列的和,然后进行状态之间的转移方式
                f[i]=std::max(f[i],f[j]+a[i]);
        res=std::max(res,f[i]);
    }
    
    std::cout<<res<<std::endl;
    return 0;
}

六、拦截导弹OJ链接

        本题思路:第一问显然每套导弹拦截系统拦截导弹高度为不升子序列。第二问求导弹拦截系统的个数可以转化为求最长上升子序列长度。
证明:

  • 1、首先我们把这些导弹分为s组(s即为所求答案)可以看出每一组都是一个不升子序列
  • 2、划分完后我们在组一里找一个原序列里以组一的开头点连续的不升子串的最后一个元素,可以知道在组2中一定有一个大与它的点(如果组二中没有的话,那么组二中最高的导弹高度必然小于这个点,而其他的高度都小于这个高度而且是递减或相等的,那么没有必要再开一个组二了,矛盾,所以不存在找不到比他大的点的情况)
  • 3、以此类推,对于每一个k组(1<=k<n)都可以找到这样的一些点,所以把这些点连起来,就是一条上升子序列。
  • 4、设最长上升子序列长度为l所求上升子序列为h那么h<=l因为最长上升子序列任意两个不在一组内(如果在同一个组内,则每个组的数不成为一个不生子序列,矛盾)所以l==h。
#include <bits/stdc++.h>

constexpr int N=1010;

int n;
int a[N];
int f[N];//不升上升子序列
int g[N];//最长上升子序列

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);std::cout.tie(nullptr);
    
    while(std::cin>>a[n]) n++;
    
    int ans1=0;
    int ans2=0;
    for(int i=0;i<n;i++){
        f[i]=g[i]=1;
        for(int j=0;j<i;j++)
            if(a[i]<=a[j]) f[i]=std::max(f[i],f[j]+1);
            else g[i]=std::max(g[i],g[j]+1);
        ans1=std::max(ans1,f[i]);
        ans2=std::max(ans2,g[i]);
    }
    
    std::cout<<ans1<<std::endl;
    std::cout<<ans2<<std::endl;
    return 0;
}

七、导弹防御系统OJ链接

        本题思路:贪心的思想: 每次加入到一个末尾元素与当前数最接近的递增或递减序列(末尾元素小于h[u]且最大的或大于h[u]且最小的)所以可以用两个数组up[i]和down[i]分别表示第i个递增序列和和递减序列的末尾元素,然后搜索时,每次按照以上原则尽可能加入到编号靠前的序列就是满足上述思想的方案。dfs迭代:规定一个当前能搜索导弹拦截系统的最大深度depth(即:答案depth),从0开始,每次dfs一遍,如果当前depth下无法拦截所有导弹,则depth++(就是搜索的步长+1)再dfs,直到合法为止(这就是一个迭代加深的过程),则此时的depth即为答案。或者这里可以定义全局变量求解也是可以的。

#include <bits/stdc++.h>

constexpr int N=55;

int n;
int h[N];
int up[N];//表示以上升子序列的末尾元素
int down[N];//表示以下降子序列的末尾元素

bool dfs(int depth,int u,int su,int sd)//u表示枚举到第u个数h[u],su表示上升子序列的个数,sd表示下降子序列的个数,depth表示迭代的深度
{
    if(su+sd>depth) return false;//当前上升序列的个数和下降序列的个数之和如果大于当前所迭代的导弹系统个数,那么此时无法拦截所有的导弹
    if(u==n) return true;
    
    bool flag=false;//flag用来表示当前数是否已经加入到一个序列中去
    //将h[u]加入到上升子序列中
    for(int i=1;i<=su;i++){
        //利用贪心的思路每次加入到一个末尾元素与当前数最近的上升子序列中
        if(h[u]>up[i]){
            int t=up[i];
            up[i]=h[u];
            if(dfs(depth,u+1,su,sd)) return true;
            up[i]=t;//恢复现场
            flag=true;
            break;//如果满足直接break不需要加入下降子序列中
        }
    }
    
    if(!flag)
    {
        up[su+1]=h[u];//如果无法加入当前任何一个递增序列那么此时需要重新开辟一个子序列
        if(dfs(depth,u+1,su+1,sd)) return true;
    }
    
    flag=false;//flag用来表示当前数是否已经加入到一个序列中去
    //将h[u]加入到下降子序列中
    for(int i=1;i<=sd;i++){
        //利用贪心的思路每次加入到一个末尾元素与当前数最近的下降子序列中
        if(h[u]<down[i]){
            int t=down[i];
            down[i]=h[u];
            if(dfs(depth,u+1,su,sd)) return true;
            down[i]=t;//恢复现场
            flag=true;
            break;
        }
    }
    
    if(!flag)
    {
        down[sd+1]=h[u];//如果无法加入当前任何一个递减序列那么此时需要重新开辟一个子序列
        if(dfs(depth,u+1,su,sd+1)) return true;
    }
    
    return false;
}

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);std::cout.tie(nullptr);
    
    while(std::cin>>n,n){
        for(int i=0;i<n;i++) std::cin>>h[i];
        int depth=0;//表示迭代加深的深度
        while(!dfs(depth,0,0,0)) depth++;
        std::cout<<depth<<std::endl;
    }
    return 0;
}

八、最长公共上升子序列OJ链接

        本题思路:状态表示:f[i][j]代表所有a[1 ~ i]和b[1 ~ j]中以b[j]结尾的公共上升子序列的集合;
f[i][j]的值等于该集合的子序列中长度的最大值;状态计算(对应集合划分):首先依据公共子序列中是否包含a[i],将f[i][j]所代表的集合划分成两个不重不漏的子集:不包含a[i]的子集,最大值是f[i - 1][j];包含a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:
子序列只包含b[j]一个数,长度是1;子序列的倒数第二个数是b[1]的集合,最大长度是f[i - 1][1] + 1;…子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[i - 1][j - 1] + 1;如果直接按上述思路实现,需要三重循环:

for (int i = 1; i <= n; i ++ )
{
    for (int j = 1; j <= n; j ++ )
    {
        f[i][j] = f[i - 1][j];
        if (a[i] == b[j])
        {
            int maxv = 1;
            for (int k = 1; k < j; k ++ )
                if (a[i] > b[k])
                    maxv = max(maxv, f[i - 1][k] + 1);
            f[i][j] = max(f[i][j], maxv);
        }
    }
}


然后我们发现每次循环求得的maxv是满足a[i] > b[k]的f[i - 1][k] + 1的前缀最大值。因此可以直接将maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。最终答案枚举子序列结尾取最大值即可。

#include <bits/stdc++.h>

constexpr int N=3010;

int n;
int a[N],b[N];
int f[N][N];

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);std::cout.tie(nullptr);
    
    std::cin>>n;
    for(int i=1;i<=n;i++) std::cin>>a[i];
    for(int i=1;i<=n;i++) std::cin>>b[i];
    
    for(int i=1;i<=n;i++){
        int maxv=1;//表示前缀最大和
        for(int j=1;j<=n;j++){
            f[i][j]=f[i-1][j];
            if(a[i]==b[j]) f[i][j]=std::max(f[i][j],maxv);
            if(a[i]>b[j]) maxv=std::max(f[i-1][j]+1,maxv);
        }
    }
    
    int res=0;
    for(int i=1;i<=n;i++) res=std::max(res,f[n][i]);
    std::cout<<res<<std::endl;
    return 0;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

‘(尐儍苽-℡

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

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

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

打赏作者

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

抵扣说明:

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

余额充值