7.1 基础DP

7.1 基础DP

DP在紫书那里也有一个单章介绍这个专项,不过当时并没有正式的介绍DP思想而是从例题直接入手的,黑书这里从基础开始一步一步讲解。

首先DP和贪心,分治法一样,DP并不指一个特定的算法,而是一种解决问题的算法思想,简单解释来说是将一个复杂的问题分割成相对简单的子问题,再一个个解决,最后得到复杂问题的最优解。

与分治法不同的是,分治法的子问题之间互相独立,每个子问题能够独立解决。DP的子问题,之间互相联系,最终解即是子问题的其中之一。


7.1.1 硬币问题

有n种硬币,面值分别为v1,v2······,每种都有无限多。给定非负整数S,可以选用多少个硬币,使得面值之和恰好为S?输出硬币数目的最小值和最大值。

分析:几乎可以说是动态规划最为经典的问题之一了,紫书上已经做了非常充分的从求解到打印解的过程,这里只引用代码:

minv[0]=maxv[0]=0;//分别表示最短路和最长路的路径长度
for (int i=1;i<=S;i++) {
   minv[i]=INF; maxv[i]=-INF;}
for (int i=1;i<=S;i++) for (int j=1;j<=n;j++) if (i>=v[j]){
   
	//i表示状态结点,j表示第j种面值(状态转移方程)
	minv[i]=min(minv[i],minv[i-v[j]]+1); maxv[i]=max(maxv[i],maxv[i-v[j]]+1);
}//输出minv[S]和maxv[S]即可 
void print_ans(int *d,int S){
   ///这里的调用的d数组分别为minv和maxv
	for (int i=1;i<=n;i++) if (S>=v[i]&&d[S]==d[S-v[i]]+1){
   
		printf("%d ",i); print_ans(S-v[i]); break;
	}
} 

用的写法是类似记忆化搜索而非完全递归的写法,虽然黑书上记忆化搜索和递推是第二节的内容但其实放到第一节也无可厚非。

不过黑书在这里是将硬币面值放在了循环的外层,硬币和放在了内层,代码的编写上避免了if的判断(代码详见hdu2069)。

黑书后面拓展了一类问题:

所有硬币组合

有n种硬币,面值分别为v1,v2·····vn,数量无限。输入非负整数s,选用硬币,使其和为s,输出所有可能的硬币组合总数。

如果问题只是这样,那和前面的硬币问题几乎没有任何区别。事实上会给上一个限制:可能的硬币组合使用的硬币个数不能超过x。

分析:添加了一个条件后明显感觉到dp的状态设置的太简单了,如果用dp[i]只能存储硬币组合总数,不能确定满足一定条件的硬币个数。

那如果给dp的状态添加一个限制来满足条件,就又出现了另一个问题:因为我们没有记录每种组合使用的硬币总数,我们无法确定dp后的新状态是否满足条件。

其实思考到这步,解决的方案也基本出来了:既然没有缺少硬币组合的硬币总数这个属性,我们给它添加一个即可。用dp[i][j]表示硬币和为i,使用硬币个数为j的方案总数,进行一个二维dp,最终将dp[s][0]~dp[s][100]想加即可。样题如下:

hdu 2069 “Coin Change”

题面请点击这里

分析:面值是给定的,就按照上面的思路打代码就可以了,dp的代码还是不太难的,不过对于多维度dp最需要注意的还是将哪个属性放在循环的哪一层。

可以发现紫书和黑书在设置属性的循环层数这里还是有区别的:

#include<cstdio>
using namespace std;
int dp[251][101]={
   0};//dp[i][j]表示硬币和为i,使用硬币个数为j的方案总数 
int type[5]={
   1,5,10,25,50};//5种给定的面值
void solve(){
   dp[0][0]=1;//初始化
	//由于硬币的个数只是为了满足条件而附带的属性,所以主要还是以硬币和进行最内层的循环 
	//将硬币的面值放在最外层的循环是为了提高代码的简洁性和效率,事实上放在内层也是可行的 
	for (int i=0;i<5;i++) for (int j=1;j<101;j++) for (int k=type[i];k<251;k++) dp[k][j]+=dp[k-type[i]][j-1]; 
} 
int main(){
   int s,ans[251]={
   0}; solve();//ans[i]为硬币个数不大于100硬币和为i的方案总数
	for (int i=0;i<251;i++) for (int j=0;j<101;j++) ans[i]+=dp[i][j];//相加即可
	while (~scanf("%d",&s)) printf("%d\n",ans[s]); return 0; 
} 

虽然没有学习记忆化搜索,但这种存储方案非纯递归的做法已经十分接近于记忆化搜索了。


7.1.2 0/1背包

硬币问题几乎是dp问题最经典的问题之一,0/1背包问题就是最经典的问题没有之一。

dp问题中国的背包问题分为物品无限的背包问题和0/1背包问题,每种物品有无限多个就是物品无限的背包问题,每种物品只有1个就是0/1背包问题。当然还有种最简单的,一般背包问题,就是里面的物品甚至可以分割,这种用贪心法来处理就可以了。

0/1背包的解决方紫书学习那里已经介绍过了,就不重复介绍了。其实就是用d(i,j)表示将前i个物品装到容量为j的背包中的最大重量,d(i,j)=max(d(i-1,j),d(i-1,j-V[i])+W[i])(代表是否放入第i个物品),答案为d[n][C]。

hdu 2602 “Bone Collector”

就是0/1背包问题。

分析:根据上面的状态转移方程有代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct bone{
   int v,w;}a[1011];//v为体积,w为价值
int n,v,dp[1011][1011];//dp[i][j]表示将前i个物品装到容量为j的背包中的最大重量
int solve(){
   memset(dp,0,sizeof(dp));
	for (int i=1;i<=n;i++) for (int j=0;j<=v;j++){
   
		if (a[i].v>j) dp[i][j]=dp[i-1][j];//第i个物体太大了装不下
		else dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i].v]+a[i].w);//装或者不装第i个物体 
	} return dp[n][v]; 
}
int main(){
   int T; scanf("%d",&T);
	for (int i=0;i<T;i++){
   scanf("%d%d",&n,&v);
		for (int j=1;j<=n;j++) scanf("%d",&a[j].w); for (int j=1;j<=n;j++) scanf("%d",&a[j].v);
		printf("%d\n",solve());
	} return 0;
} 

紫书上的写法基本一致,就是多一种一遍输入一边处理的写法,但由于我们这里不是输入一个v输入一个w,所以也不能用那种写法。

紫书和黑书这里同时介绍了一种小技巧——滚动数组,即用一个一维的数组来实现二维的dp,以节省空间。因为观察可以发现,每一行都是通过正上方元素的值和左上方元素的值取较大值得到的。那么我们将上一行当作原本的数组遗留在那里,用新的一行的元素来覆盖原来的一行就好了。

memset(dp,0,sizeof(dp))
for (int i=1;i<=n;i++){
   scanf("%d%d",&V,&W);
	for (int j=v;j>=0;j--) if (j>=V) dp[j]=max(dp[j],dp[j-V]+W);
}

因为是一个逆序的更新,dp[j-V]中仍保存着上一行没有更改过的值,效果和二维dp是一样的。

这么做,虽然能够节省空间,但同样由于只保留了最后的状态,所以会损失很多信息,导致无法输出方案。

完全背包就是在内层的循环中进行相反顺序的更新:

memset(dp,0,sizeof(dp))
for (int i=1;i<=n;i++){
   scanf("%d%d",&V,&W);
	for (int j=0;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值