Day0 几个简单DP问题
上学期中开始刷hdu水题100题,在一个dp题目上卡住,遂产生系统性学习算法的念头。后购买《挑战程序设计》,学至DP,发现还是卡壳,于是决定对题目进行一定的分析与总结。
01背包
之前看过大神所写的背包九讲,收益颇多,但是不能很好的理解dp数组,所以在后面的题目中先用递归写一遍程序,在与DP进行比较。
问题复述
在n个重量,价值分别为wi,vi的物品中挑选总重量不超过W的物品,每件物品仅允许选一次,求选出物品价值总和最大值。
递归描述
起始物品序号为i=0,重量j=W,对于每一件物品都有选或者不选两种选择,而递归终止的条件则为i==n;遍历了所有的物品;当然如果剩余空间 J‘ 无法容纳这件物品时,无法选。
代码如下:
int res(int i;int j){
int rec;
if(i==n){
rec=0;}
else if(j<w[i]){
rec=res(i+1,j);}
else{
rec=max(rec(i+1,j),rec(i+1,j-w[i])+v[i]);
}//能选,但不一定选,遍历所有可能性。
return rec;}
DP描述
递归代码稍微好理解一点,但是在处理剩余空间J’可以容纳第i件物品时,每一层都会触发选或者不选两次分支,DP做了一个备忘录,把递归中重复的部分记录下来,以空间换时间。
int dp[1000][1000];
for(int i=0;i<n;i++){
for(int j=0;j<W;j++){
if(j<w[i]){
dp[i+1][j]=dp[i][j];}
else{
dp[i+1][j]=max(dp[i][j],dp[i+1][j-w[i]]+v[i]);)}
}
}
cout<<dp[n][W]<<endl;
将物品的重量W划分为W/1个子空间。但是当W很大时,效率就很低了。
完全背包问题
问题复述
与01背包基本相同,但每件物品可以选多余一次;
递归描述
引入一个变量k来表示每件物品被选中的次数,便有:每一次选取总重量k*w[i]小于此时剩余重量J‘,代码如下:
int temp=0;//这个全局变量将避免最终返回选取重量最小物品最多次数;
int res(int i,int j){
int rec=0;
if(i==n){
rec=0;}
else if(j<w[i]){
rec=res(i+1,j);}
else{
for(int k=0;k*w[i]<=j;k++){
temp=max(rec(i+1,j),rec(i+1,j-k*w[i])+k*v[i]);
if(rec<temp){
rec=temp;}
}
}
return temp;}
上述代码比起01背包的代码仅仅多了一个循环,在能够选的时候,使用循环来确定具体每一次选取的数量,但是,也正是因为有了循环,返回值在物品可以被选取多次时,总是返回物品选取次数最多的价值,这个价值不一定是最优价值。故定义一个全局变量temp,使得返回值一定为最优解。
dp描述
dp程序基本上也是一样的,多了选取数量的循环部分。代码如下:
for(int i=0;i<n;i++){
for(int j=0;j<=W;j++){
for(int k=0;k*w[i]<=j;k++){
dp[i+1][j]=max(dp[i+1][j],dp[i][j-k*w[i]]+k*v[i]);}}}
就是比01背包多了每次选取的数量。
最长公共子序列问题
问题复述
俩字符串SI,TJ,求出他们最长公共子序列的长度L。
递归描述
如果俩字符串末尾S(i+1)=T(i+1),那么L就是Si与Ti的最长公共序列+1;或者S1…Si与T1…Tj+1的最长公共子序列;S1…Si+1与T1…Ti的最长公共子序列;
int res(int i,int j){
int rec=0;
if(i==0||j==0){
rec=0;}
else if(s[i]==t[j]){
rec=res(i-1,j-1)+1;}
else{
rec=max(res(i-1,j),res(i,j-1));}
return rec;}
DP描述
显而易见
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(s[i]==t[j]){
dp[i][j]=dp[i-1][j-1]+1;}
else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);}
}
}
cout<<dp[i][j]<<endl;
最长上升子序列问题
问题描述
有一个数列Sn,求出其子列中最长上升子列Sm长度L;
递归描述
int max0;
int res(int i){
int rec=1;
if(i<0){
rec=0;}
else{
for(int j=0;j<i;j++){
if(a[j]<a[i]){
rec=max(rec,res(j)+1);}
}
}
if(rec>max0){
return max0;}
res(i-1);
return rec;}
首先,每一位数的最长上升子序列即他本身;而后判断序号小于他的数是否小于他,这样一个子问题的解便得到了:res(j)+1。
DP描述
int max0=0;
for(int i=0;i<=0;i++){
dp[i]=1;
for(int j=0;j<i;j++){
if(a[i]>a[j]){
dp[i]=max(dp[i],dp[j]+1); }
} max0=max(dp[i],dp[j+1]);
}
多重部分和问题
问题描述
有n种大小不同数字Ai,每种各Mi个,是否可从这些数字中·取出部分,使他们和为K;
递归描述
这个问题有点类似与前面的多重背包,没有重量限制。总价值为K,每一次减去的价值为k* a[i];自然,有k<=m[i]&&k* a[j]<=j;而后剩余的价值J=j-k*a[i]进入下一个子问题直到剩余价值J=0或者i=n即选完所有可能性。
bool res(int i,int j){
bool rec=false;
if(j==0){
rec=true;}
else if(i<n&&j>0){
for(int k=0;k<=m[i]&&k*a[i]<=j;k++){
rec=max(rec,res(i+1,j-k*a[i]);}
}
else{
rec=false;}
return rec;}
DP描述
bool dp[1000][1000];
dp[0][0]=ture;//目标价值为0时比为真;
for(int i=0;i<n;i++){
for(int j=0;j<=K;j++){
for(int k=0;k<=m[i]&&k*a[i]<=j;k++){
dp[i+1][j]=max(dp[i][j],dp[j-k*a[i]]);}
}
}
总结
可以注意到递归是从顶到底的,从要解决的子问题入手,往回求解,直到边界条件,而DP备忘录是从低到顶的,从初始的子问题来一步步构造出最后子问题的答案。