DP-线性DP-最长上升子序列模型(序列DP)

最长上升子序列

活动 - AcWing

解题

这篇博客里给出了详细的题解,这里只写一下朴素版的代码

#include<iostream>
using namespace std;
int n,a[1010],f[1010];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)
    {
        f[i]=1;
        for(int k=1;k<i;k++)
        {
            if(a[k]<a[i]) f[i]=max(f[k]+1,f[i]);
        }
    }
    int res=0;
    for(int i=1;i<=n;i++) res=max(res,f[i]);
    printf("%d",res);
}

变式1

1.1合唱队形

登录—专业IT笔试面试备考平台_牛客网

N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。

合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK,  则他们的身高满足T1<...<Ti>Ti+1>…>TK(1<=i<=K)。

你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

解题

正反分别求一次最长上升子序列,最后枚举中间同学的位置,求出不同位置的最优情况。

#include<iostream>
using namespace std;
const int N=110;
int a[N];
int n;
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    int l[N],r[N];
    int lr=0,rr=0;
    for(int i=1;i<=n;i++)
    {
        l[i]=1;
        for(int j=1;j<i;j++)
            if(a[j]<a[i]) l[i]=max(l[i],l[j]+1);
    }
        
    for(int i=n;i>=1;i--)
    {
        r[i]=1;
        for(int j=n;j>i;j--)
            if(a[j]<a[i]) r[i]=max(r[i],r[j]+1);
    }
        
    int res=0;
    for(int i=1;i<=n;i++)
    {
        res=max(res,l[i]+r[i]-1);
    }
    printf("%d",n-res);
}

变式2

2.1友好城市

友好城市 - 洛谷

有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的 N 个城市。北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航道不相交的情况下,被批准的申请尽量多。

解题

这题的难点在于从题目中把最长上升子序列这个模型提取出来。

从河左岸顺序建桥,要使桥不相交,那么河左岸在河右岸对应的桥,位置应该是单调上升的。

#include<iostream>
#include<algorithm>
using namespace std;
const int N=2e5+10;
typedef pair<int,int> PII;
PII a[N];
int q[N];
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) 
    {
        int x,y;
        scanf("%d%d",&x,&y);
        a[i]={x,y};
    }
    int len=0;
    q[0]=-1e7;
    sort(a+1,a+1+n);
    for(int i=1;i<=n;i++)
    {
        int l=0,r=len;
        while(l<r)
        {
            int mid=(l+r+1)>>1;
            if(a[i].second>q[mid]) l=mid;
            else r=mid-1;
        }
        len=max(len,r+1);
        q[r+1]=a[i].second;
    }
    printf("%d",len);
}

变式3

3.1拦截导弹

登录—专业IT笔试面试备考平台_牛客网

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

解题

本题的第一问就是一个常规的最长上升子序列问题,在这里不再赘述。

第二问要求拦截所有导弹最少要配备多少套这种导弹拦截系统,也就是说,我们最少可以用多少个单调序列来覆盖当前的序列。

当判断当前元素是要加入原有序列还是新建一个序列时,我们考虑:

①当前元素小于原有序列的末尾元素,如果有多个序列满足条件,则应插入末尾元素最小的那个子序列;

②当前元素大于所有序列的末尾元素,需要新建一个序列。

在第①种情况下,我们可以通过维护一个数组,记录所有序列的末尾元素,深入考虑的话,可以发现这个数组是单调上升的,因此可以用二分来找到每次插入的位置。

#include<iostream>
using namespace std;
const int N=1e5+10;
int main()
{
    int n=0;
    int a[N];
    int f[N];
    while(cin>>a[++n]) continue;;
    n--;
    int nn=0;
    for(int i=n;i>=1;i--)
    {
        int l=0,r=nn;
        while(l<r)
        {
            int mid=(l+r+1)>>1;
            if(f[mid]<a[i]) l=mid;
            else r=mid-1;
        }
        f[r+1]=a[i];
        nn=max(nn,r+1);
    }
    
    printf("%d\n",nn);
    int cnt=0;
    int q[N];
    for(int i=1;i<=n;i++)
    {
        if(q[cnt]<a[i]) 
        {
            q[++cnt]=a[i];
            continue;
        }
        int l=0,r=cnt;
        while(l<r)
        {
            int mid=(l+r)>>1;
            if(q[mid]>a[i]) r=mid;
            else l=mid+1;
        }
        q[l]=a[i];
    }
    printf("%d",cnt);
}

3.2导弹防御系统

187. 导弹防御系统 - AcWing题库

为了对抗附近恶意国家的威胁,R国更新了他们的导弹防御系统。

一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。

例如,一套系统先后拦截了高度为 33 和高度为 44 的两发导弹,那么接下来该系统就只能拦截高度大于 44 的导弹。

给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。

解题

本题中,需要求解的是能够覆盖当前序列的最少的上升子序列和下降子序列和。

我们用dfs搜索每一种情况,只要在上一题代码的基础下套一个dfs框架。

#include<iostream>
using namespace std;
const int N=55;
int n;
int res;
int a[N],up[N],down[N];
void dfs(int u,int su,int sd)
{
    if(su+sd>=res) return;
    if(u==n)
    {
        res=su+sd;
        return;
    }
    
    //求上升系统个数,那么开启新序列的条件是当前所有子序列末尾都大于当前元素
    int k=0;
    while(k<su&&up[k]>=a[u]) k++;
    int t=up[k];
    up[k]=a[u];
    if(k>=su) dfs(u+1,su+1,sd);
    else dfs(u+1,su,sd);
    up[k]=t;
    
    k=0;
    while(k<sd&&down[k]<=a[u]) k++;
    t=down[k];
    down[k]=a[u];
    if(k>=sd) dfs(u+1,su,sd+1);
    else dfs(u+1,su,sd);
    down[k]=t;

    
}
int main()
{
    while(cin>>n,n!=0)
    {
        for(int i=0;i<n;i++) cin>>a[i];
        res=n;
        dfs(0,0,0);
        cout<<res<<endl;
    }
    
}

变式4

4.1最长公共上升子序列

活动 - AcWing

熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。

小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。

小沐沐说,对于两个数列A和B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。

奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。

不过,只要告诉奶牛它的长度就可以了。

数列A和B的长度均不超过 3000。

解题

公共子序列和最长上升子序列的结合,思路如下:

#include<iostream>
using namespace std;
const int N=3010;
int n;
int a[N],b[N];
int f[N][N];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) scanf("%d",&b[i]);
    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])
            {
                f[i][j]=max(f[i][j],1);
                for(int k=1;k<j;k++)
                {   
                    if(b[j]>b[k]) f[i][j]=max(f[i][j],f[i-1][k]+1);
                }
            }
        }
    int res=0;
    for(int i=1;i<=n;i++)
            res=max(f[n][i],res);
    printf("%d",res);      
}

朴素做法是O(n3)的,有一个O(n2)的优化。我们可以用一个变量存储上一阶段(1~j-1)能够存储在ai前面的最大值。

#include<iostream>
using namespace std;
const int N=3010;
int n;
int a[N],b[N];
int f[N][N];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) scanf("%d",&b[i]);
    
    for(int i=1;i<=n;i++)
    {
        int last=1;
        for(int j=1;j<=n;j++)
        {
            f[i][j]=f[i-1][j];
            if(a[i]==b[j]) f[i][j]=max(f[i][j],last);
            if(b[j]<a[i]) last=max(last,f[i-1][j]+1);
        }
    }
        
    int res=0;
    for(int i=1;i<=n;i++)
            res=max(f[n][i],res);
    printf("%d",res);
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值