动态规划的设计思想与实例(最大子段和、最长公共子序列、0-1背包、编辑距离)

VanTYS 2019-12-15

动态规划算法与分治法类似,其基本思想是将总问题分解成若干个子问题,先求解子问题,再结合这些子问题的解得到原问题的解。与分治法不同的是,动态规划求解的问题经分解得到的子问题往往不是相互独立的。

基本思想:

将总问题分解成多个子问题(子问题也可以继续分解,直到无法分解),计算子问题,用一个表保存已解决的子问题的答案,算完子问题后回到总问题时从表中寻找已求得的答案,根据要求挑选最优解,加上总问题的里的具体变化再存入表中

设计步骤:

1.找出最优解的性质,并刻画其结构特征

根据具体问题找出其结构的特点,按该特点分解问题,使问题可以从其子问题的答案中寻找解

2.递归地定义最优值

写递归表达式,即子问题的答案中最合适的应该满足哪些条件 总问题的解 = 最优解(子问题1,子问题2,子问题3……)+ 一定变化

3.以自底向上的方式计算最优值

存储子问题解的表一般为二维数组dp,按从左到右从上到下的顺序从下标(0,0)到(m,n)存储子问题的答案,在计算某一个子问题(r,c)时,根据之前计算好的解来得出答案,例如dp[l][r]=max(dp[r-1][c],dp[r][c-1])+1,表示问题(r,c)的解依据是子问题(r-1,c)和(r,c-1)的解

4.根据计算最优值时得到的信息,构造最优解

看问题的解的具体要求,一般直接为dp[m][n]

实例

最大子段和

给定n个整数(可能为负整数)组成的序列a1,a2……an,求该序列的子段和的最大值(al+al+1+al+2+……+ar-1+ar),当子段和小于0时定义其为0。

分析问题,可以得到特征:

1.子段是连续的

2.当子段中所有负整数与正整数相加的结果小于0时该子段和取0,即子段和大于等于0

3.子段和最大的子段一定会以某个元素ai为结尾

(先简称以ai-1、ai为结尾的子段和最大的两个子段为子段ai-1和子段ai

结合1和3可以得到关系:子段ai一定包括元素ai(因为以它为结尾),该子段的ai元素的上一位一定是ai(因为子段连续),或者是空(即整个子段只有ai),那么以ai为结尾的子段的最大子段和为:max(以ai-1为结尾的子段的最大字段和+ai,0)

即递归表达式:dp[i]=max(dp[i-1]+a[i],0)(其中dp[i]指子段ai的子段和,a[i]指ai

(其实不需要a数组,一个dp数组就够了)

#include<iostream>
using namespace std;
int main()
{
    int dp[100000],m,n,i;
    while(cin>>n)
    {
        for(i=0;i<n;i++)
            cin>>dp[i];
        m=dp[0]=max(dp[0],0);
        for(i=1;i<n;i++)
        {
            dp[i]=max(dp[i-1]+dp[i],0);
            m=max(m,dp[i]);
        }
        cout<<m<<‘\n‘;
    }
    return 0;
}

最长公共子序列

给定序列x={x1,x2……xm}和y={y1,y2,……yn},求x和y的最长公共子序列z

已知:子序列z的元素在序列x、y里不一定是连续的,但一定是按序列x、y里的下标的大小从小到大排好的

设dp[i][j]为x、y的从第一个元素开始、长度分别为i、j的子段的最长公共子序列的长度,则对于x[i]、y[j]的关系:

当i=0或jj=0时,x、y其中一边或两边长度为0,公共子序列长度也为0,则:

dp[i][j]=0

当x[i]=y[j]时,x[i]、y[j]直接算为公共子序列里的元素,当前最长公共子序列为上一次的最长公共子序列(dp[i-1][j-1]的时候)加上x[i]、y[j],即:

dp[i][j]=dp[i-1][j-1]+1

当x[i]!=y[j]时,x[i]、y[j]不能直接算作公共子序列里的元素,但x[i]、y[i]可能会因为和之前的元素相等而加入公共子序列(x[i]!=y[j],但可能x[i]=y[j-1],y[j]同样),当前最长公共子序列为除去x[i]或除去y[j]的两种情况下最长公共子序列较大的的一方:

dp[i][j]=max(dp[i-1][j],dp[i][j-1])

输出最长公共子序列时可根据dp[i][j]与dp[i-1][j]、dp[i][j-1]、dp[i-1][j-1]的大小关系判断dp[i][j]来源于哪个(这里由于while语句里i、j都自减了,所以下面的if语句里是反过来自增来确保i、j不必要的变化)

#include<iostream>
using namespace std;
int dp[1001][1001]={0},x[1001],y[1001],z[1001];
int main()
{
    int m,n,i,j,k;
    while(cin>>m>>n)
    {
        for(i=1;i<=m;i++)
            cin>>x[i];
        for(i=1;i<=n;i++)
            cin>>y[i];
        for(i=1;i<=m;i++)
            for(j=1;j<=n;j++)
                if(x[i]!=y[j])dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
                else dp[i][j]=dp[i-1][j-1]+1;
        k=0;
        while(dp[--i][--j])
            if(dp[i][j]==dp[i-1][j])j++;
            else if(dp[i][j]==dp[i][j-1])i++;
            else if(dp[i][j]>dp[i-1][j-1])z[++k]=x[i];
        cout<<dp[m][n]<<‘\n‘;
        while(k)cout<<z[k--]<<‘ ‘;
        cout<<‘\n‘;
    }
    return 0;
}

0-1背包

给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为c,不能多次装入物品i,也不能装入部分物品i。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?

设dp[i][j](1<=i<=n,1<=j<=c),i表示可选择的物品有1、2……i,j表示当前背包容量,则对于当前dp[i][j]:

当i=0或j=0时初始化:

dp[i][j]=0

当j<w[i]时,物品放不下,直接舍弃物品i,取上次的结果,即可选物品为1、2……i-1,背包容量同样为j的时候:

dp[i][j]=dp[i-1][j]

当j>=w[i]时,尝试放入物品,在上次的结果,以及这次放入后的结果中,取较大值:

dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])

这里的的dp[i-1][j-w[i]]+v[i]中:

1.[i-1]是因为物品i只能放一次,放入物品后i其他放入物品的范围只能是物品1、2……i-1

2.[j-w[i]]是指物品i放入时至少要留出w[i]的空间,剩余空间的存放情况相当于容量为j-w[i]的背包在物品范围1、2……i-1的存放情况

#include<iostream>
using namespace std;
int dp[1001][1001]={0},w[1001],v[1001];
int main()
{
    int n,c,i,j;
    while(cin>>n>>c)
    {
        for(i=1;i<=n;i++)
            cin>>w[i]>>v[i];
        for(i=1;i<=n;i++)
            for(j=1;j<=c;j++)
                if(j<w[i])dp[i][j]=dp[i-1][j];
                else dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
        cout<<dp[n][c]<<‘\n‘;
    }
    return 0;
}

编辑距离

一个字符串有三种操作方式:

1.替换:将字符串的任意一个字符改为另一种字符

2.删除:删除字符串中的任意一个字符

3.插入:添加一个字符在字符串的任意位置

给定两个字符串a、b,求最少操作的数量使两个字符串相等(即编辑距离)

设dp[i][j]表示字符串a、b的子串a[1~i]、b[1~j]的编辑距离

当i=0或j=0时,代表至少有一个空串,显然:

dp[i][j]=max(i,j)

当a[i]=b[j]时,a[i]、b[j]直接排除不考虑,相当于a[1~i]、b[1~j]的编辑距离:

dp[i][j]=dp[i-1][j-1]

当a[i]!=b[j]时,有多种情况:

进行替换操作时,改变a[i]或b[j]使其与另一个相等,编辑距离加1,再加上a[1~i-1]、b[1~j-1]的编辑距离即:dp[i][j]=dp[i-1][j-1]+1

进行删除操作时,删除a[i]或b[j],编辑距离加1再加上a[1~i-1]、b[1~j](或a[1~i]、b[1~j-1])的编辑距离即:dp[i][j]=dp[i-1][j]+1(或dp[i][j]=dp[i][j-1]+1)

进行插入操作时,插入a[i+1]使a[i+1]=b[j](b[j+1]同样),编辑距离加1加a[1~i]、b[1~j-1](或a[1~i]、b[1~j-1])的编辑距离即:dp[i][j]=dp[i-1][j]+1(或dp[i][j]=dp[i][j-1]+1)

可以看出删除和插入操作表达式是一样的

由于dp[i][j]和字符串a,b开始下标不同,所以if语句里是i-1、j-1

#include<iostream>
using namespace std;
int dp[2001][2001];
int main()
{
    int n,m,i,j;
    string a,b;
    while(cin>>a>>b)
    {
        n=a.length();
        m=b.length();
        for(i=0;i<=n;i++)
            dp[i][0]=i;
        for(j=0;j<=m;j++)
            dp[0][j]=j;
        for(i=1;i<=n;i++)
            for(j=1;j<=m;j++)
                if(a[i-1]==b[j-1])dp[i][j]=dp[i-1][j-1];
                else dp[i][j]=min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1;
        cout<<dp[n][m]<<‘\n‘;
    }
    return 0;
}

相关推荐