动态规划的设计思想与实例(最大子段和、最长公共子序列、0-1背包、编辑距离)
动态规划算法与分治法类似,其基本思想是将总问题分解成若干个子问题,先求解子问题,再结合这些子问题的解得到原问题的解。与分治法不同的是,动态规划求解的问题经分解得到的子问题往往不是相互独立的。
基本思想:
将总问题分解成多个子问题(子问题也可以继续分解,直到无法分解),计算子问题,用一个表保存已解决的子问题的答案,算完子问题后回到总问题时从表中寻找已求得的答案,根据要求挑选最优解,加上总问题的里的具体变化再存入表中
设计步骤:
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; }