【习题详解】动态规划:线性DP

最长上升子序列LIS

问题:求一串数字的最长上升自序列
设f[i]为以第i位数字结尾的子序列的最大长度.当我们枚举i时,我们需要从左往右枚举i前面的数字j,若数字j小于数字i则说明数字i可以在以j结尾的序列之后,因此f[j]+1是一种方案数.若没有比i小的时候,则f[i]=1.故状态转移方程是:

f ( i ) = m a x { f [ j ] + 1 0<j<i,a[j]<a[i] f [ i ]

其中,f[i]的初始值为1.
故代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,ans=0;
int a[10000];
int f[10000];
int main()
{
    ios::sync_with_stdio(false);
    cin>>n;
    for (int i=1;i<=n;i++)
        cin>>a[i];
    for (int i=1;i<=n;i++)
    {
        f[i]=1;
        for (int j=1;j<i;j++)
            if (a[j]<a[i])
                f[i]=max(f[i],f[j]+1);
        ans=max(ans,f[i]);
    }
    cout<<f[n];
    return 0;
} 

最长公共子串LCS

这道题是由两个字符串组成,所以我们需要用二维数组去限制.
我们可以设f[i][j]为第一个字符串的长度到i,第二个字符串的长度到j的最长公共子串.显然,我们枚举i和j时.
若a[i]=b[j],则说明这两个字母可以在最长公共子串中再添加这个字母且不会形成冲突,因此可以在原来的最长公共子串的基础上加上1,在表示当前的最长公共子串.
若a[i]!=b[j],则说明这两个字母不能同时构成最长公共子串,则可以在选其中一个字母的基础上进行抉择.
因此我们可以得到状态转移方程:

f [ i ] [ j ] = m a x { f [ i 1 ] [ j 1 ] , a[i]=a[j] f [ i 1 ] [ j ] , a[i]≠a[j] f [ i ] [ j 1 ] , a[i]≠a[j]

因此代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,m;
char S1[1200];
char S2[1200];
int f[1200][1200];
int main()
{
    ios::sync_with_stdio(false);
    int n,m;
    cin>>n>>m;
    for (int i=1;i<=n;i++)
        cin>>S1[i];
    for (int i=1;i<=m;i++)
        cin>>S2[i];
    for (int i=1;i<=n;i++)
        for (int j=1;j<=m;j++)
            if (S1[i]==S2[j]) f[i][j]=f[i-1][j-1]+1;
            else f[i][j]=max(f[i-1][j],f[i][j-1]);
    cout<<f[n][m]; 
    return 0;
}

线段

问题:两条平行数轴间有N 条线段,每条线段两个端点坐标分别为ai, bi。
问至少去掉多少条线段,可以使得剩下的线段互不相交。
1 ≤ N ≤ 5000, 0 ≤ ai, bi ≤ 10000。
这道题思维难度不大,我们选择和LIS联系起来.
我们可以设f[i]为保留第i条闲段的最大线段数,然后往前面的线段去枚举,只要不冲突就f[i]=f[j]+1,否则f[i]必然为1,表示只取一条线段.
则状态转移方程:

f ( i ) = m a x { f [ j ] + 1 , f [ i ] 0<j<i,第i条线段与第j条线段不冲突 f [ i ]

当然,为了枚举有序,我们需要选择排序.
代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,ans=0;
int f[6000];
struct DP1{int left,right;};
DP1 a[6000];
inline bool cmp(DP1 x,DP1 y){return x.left<y.left;}
inline bool check(int x,int y){return a[x].right<=a[y].left;}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n;
    for (int i=1;i<=n;i++)
        cin>>a[i].left>>a[i].right;
    for (int i=1;i<=n;i++)
    {
        f[i]=1;
        for (int j=1;j<i;j++)
            if (check(j,i))
                f[i]=max(f[i],f[j]+1);
        ans=max(f[i],ans);
    }
    cout<<f[n];
    return 0;
}

合唱队形

问题:N位同学站成一排,音乐老师要挑出K位同学排成合唱队形。合唱
队形是指这样的一种队形:设K位同学从左到右依次编号为1, 2, … , K,
他们的身高分别为T1, T2, … , TK,则他们的身高满
足T1 < T2 < · · · < Ti, Ti > Ti+1 > · · · > TK (1 ≤ i ≤ K)。
2 ≤ N ≤ 100, 130 ≤ Ti ≤ 230。

显然,这道题目涉及到了有序序列,和LIS很有相似性.因此我们考虑怎么做LIS.
求的是先上升再下降的序列,那么我们可以正序来一边LIS用f[i]表示一遍LIS,再枚举中间点mid用g[i]串序列的最高点,那么当前序列长度就是答案就是f[i]+g[i]-1,可以表示为:

a n s = m a x ( f [ i ] + g [ i ] 1 ) , 1 i n

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n;
int ans=0;
int a[1000];
int f[1000];
int g[1000];
int main()
{
    ios::sync_with_stdio;
    cin>>n;
    for (int i=1;i<=n;i++)
        cin>>a[i];
    for (int i=1;i<=n;i++)
    {
        f[i]=1;
        for (int j=1;j<i;j++)
            if (a[j]<a[i])
                f[i]=max(f[i],f[j]+1);
    }
    for (int i=n;i>=1;i--)
    {
        g[i]=1;
        for (int j=n;j>i;j--)
            if (a[j]<a[i])
                g[i]=max(g[i],g[j]+1);
    }
    for (int mid=1;mid<=n;mid++)
        ans=max(ans,f[mid]+g[mid]-1);
    cout<<n-ans;
    return 0;
}

演讲时间:

同【线段】,只是设f[i]为最大时间而不是最大方案数,因此不是+1而是加上所对应时间.不难得出:

f ( i ) = m a x { f [ j ] + t [ i ] , 0<j<i且第i个演讲与第j个演讲不冲突 f [ i ] , 初始化为t[i]

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,ans=0;
int f[100000];
struct DP4{int st,fi;}a[100000];
inline bool cmp(DP4 x,DP4 y){return x.st<y.st;}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n;
    for (int i=1;i<=n;i++)
        cin>>a[i].st>>a[i].fi;
    sort(a+1,a+n+1,cmp);
    for (int i=1;i<=n;i++)
    {
        int Time=a[i].fi-a[i].st+1;
        f[i]=Time;
        for (int j=1;j<i;j++)
            if (a[j].fi<=a[i].st)
                f[i]=max(f[i],f[j]+Time);
        ans=max(ans,f[i]);
    }
    cout<<ans<<endl;
    return 0;
}

编辑距离问题

问题:设A 和B 是2 个长度分别为n 和m 的字符串。要用最少的字符操作
将字符串A 转换为字符串B。这里所说的字符操作包括:
◆ 删除一个字符
◆ 插入一个字符
◆ 将一个字符改为另一个字符
将字符串A 变换为字符串B 所用的最少字符操作数。
1 ≤ n, m ≤ 4000。

练习LCS,设f[i][j]为前i位的字符串A求改成前j位字符串B的最少步数.
假设我们枚举第i位和第j位,我们可以分情况讨论
1.若它们相同,则不修改,否则修改.我们可以把这一步定义为D(i,j)

D ( i , j ) = { 1 , a[i]=b[j] 0 , a[i]≠b[j]

2.删除字符a[i],方案数就是f[i-1][j]+1
3.插入字符b[j],即添加的和b[j]可以抵消,即忽略不计,只要加上1的操作数即可.所以,方案数是:f[i][j-1]+1
那么,我们可以推出状态转移方程:
f ( i ) = m i n { f [ i 1 ] [ j 1 ] f [ i ] [ j 1 ] + 1 f [ i 1 ] [ j ] + 1

代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,m;
char a[5000];
char b[5000];
int f[5000][5000];
inline int D(int x,int y){return a[x]!=b[y];} 
inline int Min(int x,int y,int z){return min(x,min(y,z));}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m;
    for (int i=1;i<=n;i++)
        cin>>a[i];
    for (int i=1;i<=m;i++)
        cin>>b[i];
    for (int i=0;i<=n;i++)
        for (int j=0;j<=m;j++)
            f[i][j]=Min(f[i-1][j-1]+D(i,j),f[i-1][j]+1,f[i][j-1]+1);
    cout<<f[n][m];
    return 0;
}

导弹拦截

题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭,由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。   
输入导弹的枚数和导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,每个数据之间有一个空格),计算这套系统最多能拦截多少导弹?
如果要拦截所有导弹最少要配备多少套这种导弹拦截系统?
输入格式
第一行数字n表示n个导弹(n<=200) 第二行n个数字,表示n个导弹的高度
输出格式
一个整数,表示最多能拦截的导弹数
一个整数,表示要拦截所有导弹最少要配备的系统数
样例数据
input
8
389 207 155 300 299 170 158 65
output
6
2
显然,有2问,因此分成两框去解决
1.直接套LIS模板即可(DP)
2.用开拦截系统的方式进行计算答案:(贪心算法)
枚举导弹数,去与每一个系统去尝试.设导弹高度为h,拦截系统的最后导弹高度为h[i],则必须要满足如下形象的数学公式来表现:

k = m i n ( h [ i ] ) , h h [ i ]

min为何意义:即每一次选择系统的时候都要选择最后导弹最低的,因为高的导弹只能是由最后高度高的系统拦截,而低的导弹高的低的都能选择,因此我们要用max
代码如下:

#include<bits/stdc++.h>
using namespace std;
int a[1000000]={},f[1000000]={},n;
inline void DP()
{
    for (int i=1;i<=n;i++)
    {
        int Max=-10000000;
        for (int j=1;j<i;j++)
            if (a[i]<=a[j]&&Max<f[j])
                Max=f[j];
        if (Max==-10000000) f[i]=1;
        else f[i]=Max+1;
    }
    int Ans=-10000000;
    for (int i=1;i<=n;i++)
        Ans=max(Ans,f[i]);
    cout<<Ans<<endl;
    return;
}
inline void Tan_Xin()
{
    int s=0;
    int last[10000]={};
    for (int i=1;i<=n;i++)
    {
        int Min=999999999,k=0,t=0,in;
        for (int j=1;j<=s;j++)//枚举h
        { 
            if (last[j]!=0) k++;//k表示非零的个数 
            if (last[j]>=a[i]) t++;//t表示可以进入的系统个数
            if (last[j]>=a[i]&&last[j]<Min) Min=last[j],in=j;//Min表示最小的可进入的系统的末尾,in表示max结尾的那个系统的行数 
        }
        if (k==0||t==0) s++,last[s]=a[i];
            else last[in]=a[i];
    }
    cout<<s;
    return;
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n;
    for (int i=1;i<=n;i++)
        cin>>a[i];
    DP();
    Tan_Xin();
    return 0;
}

题目描述
某国家被一条河划分为南北两部分,在南岸和北岸总共有N对城市,每一城市在对岸都有一个城市作为友好城市。每一对友好城市都希望有一条航线来往,于是他们向政府提出了申请。
由于河终年有雾。政府决定允许开通的航线就互不交叉(如果两条航线交叉,将有很大机会撞船)。兴建哪些航线以使在安全条件下有最多航线可以被开通。
输入格式
  第一行两个由空格分隔的整数x,y,10〈=x,y〈=60000,x,y中较长的表示河的长度另一个表示宽。
第二行是一个整数N(1<=N<=5000),表示分布在河两岸的城市对数。接下来的N行每行有两个由空格分隔的正数C,D(C、D〈=x〉,描述每一对友好城市与河起点的距离,C表示北岸城市的距离而D表示南岸城市的距离。在河的左边,任何两个城市的位置都是不同的。
输出格式
一个整数,表示安全条件下能够开通的最大航线数目
样例数据
input
30 4
5
4 5
2 4
5 2
1 3
3 1
output
3

显然,这道题同样和LIS十分相似,设f[i]为最大的通过船只数,只要排序,然后向前查找是否出现重叠即可.
代码如下:

#include<bits/stdc++.h>
using namespace std;
struct Accpect
{
    int left;
    int right;
}a[10000]={};
inline bool cmp(Accpect x,Accpect y)
{
    return (x.left<y.left);
}
int main()
{
    int n,f[5001]={},ans=-1,x,y;
    cin>>x>>y>>n;
    for (int i=1;i<=n;i++)
        cin>>a[i].left>>a[i].right;
    sort(a+1,a+n+1,cmp);
    for (int i=1;i<=n;i++)
    {
        int Max=-10000000;
        for (int j=1;j<i;j++)
            if (a[i].right>a[j].right&&a[i].left>a[j].left&&f[j]>Max) 
               Max=f[j];
        if (Max==-10000000) Max=0;
        f[i]=Max+1;
    }
    for (int i=1;i<=n;i++)
        ans=max(ans,f[i]);
    cout<<ans;
    return 0;
}

护卫队

题目描述
护卫车队在一条单行的街道前排成一队,前面河上是一座单行的桥。因为街道是一条单行道,所以任何车辆都不能超车。桥能承受一个给定的最大承载量。
为了控制桥上的交通,桥两边各站一个指挥员。护卫车队被分成几个组,每组中的车辆都能同时通过该桥。当一组车队到达了桥的另一端,该端的指挥员就用电话通知另一端的指挥员,这样下一组车队才能开始通过该桥。
每辆车的重量是已知的。任何一组车队的重量之和不能超过桥的最大承重量。被分在同一组的每一辆车都以其最快的速度通过该桥。一组车队通过该桥的时间是用该车队中速度最慢的车通过该桥所需的时间来表示的。
问题要求计算出全部护卫车队通过该桥所需的最短时间值。
输入格式
输入文件第一行包含三个正整数(用空格隔开),第一个整数表示该桥所能承受的最大载重量(用吨表示); 第二个整数表示该桥的长度(用千米表示); 第三个整数表示该护卫队中车辆的总数(n < 1000)
接下来的几行中,每行包含两个正整数W和S(用空格隔开),W表示该车的重量(用吨表示),S表示该车过桥能达到的最快速度(用千米/小时表示)。 车子的重量和速度是按车子排队等候时的顺序给出的。
输出格式
输出文件应该是一个实数,四舍五入精确到小数点后1位,表示整个护卫车队通过该桥所需的最短时间(用分钟表示)。
样例数据
input
100 5 10
40 25
50 20
50 20
70 10
12 50
9 70
49 30
38 25
27 50
19 70
output
75.0

首先,这道题相对难度较大,虽然是线性DP,但是对于方程的设置和状态的理解还是有一定难度的.
我们可以设f[i]为前i辆车通过桥的最少时间.然后再去枚举i:
1.当i不和其它的车分组的时候,时间是f[i-1]+s[i],即原来的时间加上第i量车的时间
2.当i和其它的车分组的时候,假设i-j是一个组的,那么时间是f[j-1]+L/mins,其中L表示桥的长度,mins表示第i辆车到第j量车的最小速度.因为当j=i时就枚举到了,所以第一种情况可以包含在第二种情况内.
可得状态转移方程:

f [ i ] = m i n ( f [ j 1 ] + L / m i n ( s [ k ] ) ) 1 j i j k i

代码如下:

#include<bits/stdc++.h>
using namespace std;
long long W,L,n;
long long w[100000];
long long s[100000];
double f[100000];
#define lgn pow(10,12);
int main()
{
    ios::sync_with_stdio(false);
    memset(f,100,sizeof(f));
    scanf("%lld%lld%lld",&W,&L,&n);
    for (int i=1;i<=n;i++)
        scanf("%lld%lld",&w[i],&s[i]);
    f[0]=0.0;
    for (int i=1;i<=n;i++)
    {
        long long sum=0;
        long long Mins=lgn
        for (int j=i;j>=1;j--)
        {
            sum+=w[j];
            Mins=min(Mins,s[j]);
            if (sum>W) break;
            if (f[j-1]+1.0*L/Mins<f[i])
                f[i]=f[j-1]+1.0*L/Mins;
        }
    }
    printf("%.1lf",f[n]*60.0);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/ronaldo7_zyb/article/details/81052793
今日推荐