欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

线性DP详解

程序员文章站 2022-03-25 21:54:51
顾名思义,线性DP就是在一条线上进行DP,这里举一些典型的例子。 LIS问题(最长上升子序列问题) 题目 给定一个长度为N的序列A,求最长的数值单调递增的子序列的长度。 上升子序列B可表示为B={Ak1,Ak2,···,Akp},其中k1

顾名思义,线性dp就是在一条线上进行dp,这里举一些典型的例子。

lis问题(最长上升子序列问题)

题目

给定一个长度为n的序列a,求最长的数值单调递增的子序列的长度。

上升子序列b可表示为b={ak1,ak2,···,akp},其中k1<k2<···<kp

解析

状态:f[i]表示以a[i]为结尾的最长上升子序列的长度,边界为f[0]=0。

状态转移方程:f[i]=max{f[j]+1}(0≤j<i,a[j]<a[i])。

答案显然为max{f[i]}(1≤i≤n)。

事实上,无论是上升、下降还是不上升等等此类问题,代码都是相似的,唯一的区别只是判断的符号更改罢了。

code

#include <iostream>
using namespace std;
int n,a[100],f[100],maxn;
int main()
{
    f[0]=0;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        for(int j=1;j<n;j++)
            if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
        maxn=max(maxn,f[i]);
    }
    cout<<maxn;
    return 0;
}

 

 

 

 

 

lcs问题(最长公共子序列)

题目

给定两个长度分别为n、m的字符串a和b,求最长的既是a的子序列又是b的子序列的字符串的长度。

解析

状态:f[i][j]表示a的前i个字符与b的前j个字符中的最长公共子序列,边界为f[i][0]=f[0][j]=0。

状态转移方程:f[i][j]=max{f[i-1][j],f[i][j-1],f[i-1][j-1]+1(if a[i]=b[j])}。

答案为f[n][m]。

code

#include <iostream>
using namespace std;
int n,m,f[100][100];
char a[100],b[100];
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        f[i][0]=0;
    }
    for(int i=1;i<=m;i++)
    {
        cin>>b[i];
        f[0][i]=0;
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            f[i][j]=max(f[i-1][j],f[i][j-1]);
            if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
        }
    cout<<f[n][m];
    return 0;
}

 

 

 

 

 

数字三角形

题目

给定一个n行的三角矩阵a,其中第i行有i列,从左上角出发,每次可以向下方或向右下方走一步,最终到达底部。

求把经过的所有位置上的数加起来,和最大是多少。

解析

状态:f[i][j]表示走到第i行第j列,和最大是多少,边界为f[1][1]=a[1][1]。

状态转移方程:f[i][j]=a[i][j]+max{f[i-1][j],f[i-1][j-1](if j>1)}。

答案为max{f[n][j]}(1≤j≤n)。

code

#include <iostream>
using namespace std;
int n,a[100][100],f[100][100],maxn;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++) cin>>a[i][j];
    f[1][1]=a[1][1];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
        {
            f[i][j]=a[i][j]+f[i-1][j];
            if(j>1) f[i][j]=max(f[i][j],a[i][j]+f[i-1][j-1]);
        }
    for(int i=1;i<=n;i++) maxn=max(maxn,f[n][i]);
    cout<<maxn;
    return 0;
}

 

 

 

 

 

例题一:合唱队形

题目

【题目描述】

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

合唱队形是指这样的一种队形:设k位同学从左到右依次编号为1,2,,k,他们的身高分别为t1,t2,,tk, 则他们的身高满足t1<...<ti>ti+1>>tk(1ik)。

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

【输入格式】

共二行。

第一行是一个整数n(2n100),表示同学的总数。

第二行有n个整数,用空格分隔,第i个整数ti(130ti230)是第i位同学的身高(厘米)。

【输出格式】

一个整数,最少需要几位同学出列。

【输入样例】

8
186 186 150 200 160 130 197 220

【输出样例】

4 

【数据规模】

对于50%的数据,保证有n20;

对于全部的数据,保证有n100。

解析

最少出列,就是最多留下。

分析一下队形,其实质便是先上升再下降,不难联想到最长上升子序列与最长下降子序列。

定义状态f[i][0/1],f[i][0]表示以第i个人为结尾的最长上升子序列,f[i][1]表示以第i个人为结尾的最长合唱队形,f数组初值都为1,。

所以前i个人的最长合唱队形为max(f[i][0],f[i][1])。

状态转移方程:

f[i][0]=max(f[i][0],f[j][0]+1(if t[i]>t[j]));(t[i]如题所述,j<i)

f[i][1]=max(f[i][1],a[j][0],a[j][1]+1(if t[i]<t[j]);

最后的答案为n-max(f[i][0],f[i][1])。

code

#include <algorithm>
#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
#include <cmath>
using namespace std;
int n,t[101],f[101][2],ans;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>t[i];
    for(int i=1;i<=n;i++)
    {
        f[i][0]=1;
        for(int j=1;j<i;j++)
            if(t[i]>t[j]) f[i][0]=max(f[i][0],f[j][0]+1);
    }
    for(int i=1;i<=n;i++)
    {
        f[i][1]=1;
        for(int j=1;j<i;j++)
            if(t[i]<t[j]) f[i][1]=max(f[i][1],max(f[j][0],f[j][1])+1);
    }
    for(int i=1;i<=n;i++) ans=max(ans,max(f[i][0],f[i][1]));
    cout<<n-ans;
    return 0;
}

 

 

 

 

 

例题二:导弹拦截

题目

【题目描述】

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

输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

【输入格式】

1行,若干个整数。

【输出格式】

2行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

【输入样例】

389 207 155 300 299 170 158 65

【输出样例】

6
2

【数据规模】

导弹高度是≤50000的正整数,导弹个数≤100000。

注:o(n2) 100分,o(nlogn) 200分。

解析

非常经典的一道线性dp题目。

先来说说o(n2)的做法:

第一问显然是在求最长不上升子序列,定义状态f[i]表示以第i个数为结尾的最长不下降子序列。

状态转移方程:f[i]=max(f[i],f[j]+1(if a[i]<=a[j]))(a[i]表示第i个导弹的高度,j<i)。

第二问实际上是在求最长上升子序列,证明比较麻烦,这里便不给出了,自行理解一下。

与第一问求法相同,只需要把<=改成>即可。

这种做法只有100分,考虑一下优化。


 以第一问为例,观察样例,不难发现,当f的值相同时,越后面的导弹高度越高。

所以我们可以用一个d[i]维护f值为i的序列的最后一个导弹的值,t记录当前求出的最长不上升子序列的长度,

然后在递推时判断a[d[i]](1≤i≤t)的值,若大于等于当前导弹高度,就更新f。

第二问同理。

优化之后,虽然依旧是o(n2)的做法,但其时间复杂度却很小,足以拿到200分。


 o(nlogn)的做法有很多,例如二分、线段树什么的,这里便不再给出(),有兴趣的可以自己尝试做做。

code

#include <algorithm>
#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
#include <cmath>
using namespace std;
int a[100001],f[100001],n,maxn=-1;
int main()
{
    while(scanf("%d",&a[++n])==1) ;
    n--;
    for(int i=1;i<=n;i++)
    {
        f[i]=1;
        for(int j=1;j<i;j++)
            if(a[i]<=a[j]) f[i]=max(f[i],f[j]+1);
        maxn=max(maxn,f[i]);
    }
    printf("%d\n",maxn);
    maxn=-1;
    for(int i=1;i<=n;i++)
    {
        f[i]=1;
        for(int j=1;j<i;j++)
            if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
        maxn=max(maxn,f[i]);
    }
    printf("%d",maxn);
    return 0;
}
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
long long f[1000010],a[1000010],tot=0,s=1,d[1000010],t=0;
int main()
{
    memset(d,0,sizeof(d));
    while(scanf("%lld",&a[++tot])==1);
    tot--;
    for(int i=1;i<=tot;i++)
    {
        f[i]=1;
        for(int j=t;j>=1;j--)
            if(a[i]<=a[d[j]])
                f[i]=max(f[i],f[d[j]]+1);
         t=max(f[i],t);
        d[f[i]]=i;
    }
    cout<<t<<endl;
    t=0;
    for(int i=1;i<=tot;i++)
    {
        f[i]=1;
        for(int j=t;j>=1;j--)
            if(a[i]>a[d[j]])
                f[i]=max(f[i],f[d[j]]+1);
         t=max(f[i],t);
        d[f[i]]=i;
    }
    cout<<t;
    return 0;
}

 

 

 

 

 

注:参考书籍:《算法竞赛进阶指南》