背包问题理解与感悟(DP)

01背包问题:

有n件物品,每件的价值及体积分别为v,w,且最多只能选一个,有一个背包总容量为V,求所选物品总体积不超过V时可获得的最大价值。
1.用二维数组dp[i,j]来处理:
状态:dp[i,j]表示选取前i-1件物品后,选第i件物品所能得到的最大价值;
状态方程:
到选取第i件物品时,有3种情况:
不可选;可选并选了;可选但不选。
dp[i,j]=:
{ dp[i-1,j] j<w[i]
max(dp[i-1,j] , dp[i-1,j-w[i]]+v[i]) j>=w[i]}

为什么第三种情况是从dp[i-1,j-w[i]]开始选呢?因为我们要在前i-1件物品已经选好了,并且选好之后还能够装下第i件物品的情况下,去选取第i件物品。
代码如下:
for(int i=1;i<=n;i++){
for(int j=0;j<=V;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]);
}
}
(答案:dp[n,V])
注:该方法也可以边输入边处理,即不必特地开辟两个数组v[i],w[i],直接在输入v,w时就处理dp数组。
2.用滚动数组(一维)dp[j]处理:
与方法 1 很相似,个人觉得,是从方法 1 推导出来的。直接看代码:
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++){
scanf("%d%d",&v,&w); //边输入边处理
for(int j=V;j>=w;j–){ //一定要逆序,起点V,终点w(包括0)
dp[j]=max(dp[j],dp[j-w]+v);
}
}
(答案:dp[0~V]中最大的那个)
相比较方法 1 ,用滚动数组来处理要节约空间,并且更为整洁,不过使用滚动数组要注意一点:
内层循坏一定要逆序,起点V,终点w(包括0)。
(注:逆序的原因是为了保证每一件物品只能选一次,正序会出现同件物品选多次的情况,正序是完全背包的情况)
具体的讲解,可参考紫书或背包九讲。

(附)初始化的细节:
01背包问题有两种问法,一种询问当总重量恰好为V时所能获得的最大价值,另一种即为上面的问法,不超过V。

如果是第一种问法,那么只要在初始化时,将dp[0]=0,dp[1~V]=-inf即可,详解可查询:http://www.doc88.com/p-2961204836432.html。这个方法也可以推广到其它类型的背包问题哟。

完全背包问题:

有n件物品,每件的价值及体积分别为v,w,且可选无限多个,有一个背包总容量为V,求所选物品总体积不超过V时可获得的最大价值。
1.用二维数组dp[i,j]处理:
状态:dp[i,j]表示选完前i-1件物品后,选k个第i件物品所能获得的最大价值(0<=k<=j/w)
状态方程:
到选取第i件物品时,有3种情况:
不可选;可选但不选;可选并选了若干个。
dp[i,j]=:
{dp[i-1,j] j<w[i];
max(dp[i-1,j],dp[i,j-w[i]]+v[i]) j>=w[i]}

仔细观察一下,会发现,这个方程与01背包的方程之间的差别只有一处细微的地方:max中,01背包的dp[i-1,j-[wi]]+v[i]变成了dp[i,j-w[i]]+v[i]。为什么呢?我们先看下代码再来解释:
for(int i=1;i<=n;i++){
for(int j=0;j<=V;j++){
if(j<w[i]) dp[i,j]=dp[i-1,j];
else dp[i,j]=max(dp[i-1,j],dp[i,j-w[i]]+v[i]);
}
}
(答案:dp[n,V])
原因就是:选了k个i物品的状态是基于选了k-1个i物品的状态上的,而它们都是处于选取第i个物品的状态,而非返回至第i-1个物品的状态。而内层循坏j是从0开始,递增至终点为V的,便可以模拟选取若干个i物品的过程。(想象一下)
2.用滚动数组dp[j]处理:
同01背包一样,与方法 1 很相似,直接上代码吧:
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++){
scanf("%d%d",&v,&w);
for(int j=w;j<=V;j++){ //一定要顺序,起点w(包括0),终点V
dp[j]=max(dp[j],dp[j-w]+v);
}
}
注:内层循坏一定要顺序,起点w(包括0),终点V。
其实,如果理解了二维数组的思路,这个代码也就基本能理解了。不过,如果要单独理解滚动数组为何方向要顺序的话,可查看:http://www.doc88.com/p-2961204836432.html

附:完全背包转化为01背包问题
将第i件物品拆分成num=V/w[i]件物品,将num件物品拆分成k件物品,每件物品的价值与重量均乘上一个系数,再利用 二进制 的思想,计算出k的值以及每件物品对应的系数。举个栗子,设V=100,w[i]=9,则num=100/9=11,11的二进制数为1011,则第1件物品对应系数为20×1=1,第二件21×1=2,第三件22×0=0,第四件23×1=8,系数为0的舍去,则最终k=3,每件系数分别为1,2,8。对每件物品i完成拆分后,就可以用01背包的解决方法来解题了,时间复杂度为O(V∑(V/w[i])),虽然相对上面O(V*N)的做法略显不足,但这个做法成功的将问题进行了转化,说明了01背包以及转化思想的重要性。
另外,上述计算各个物品的系数的过程可以通过位运算完成,利用&和>>运算符。

01背包问题2

有n件物品,每件的价值及体积分别为v,w,且最多只能选一个,有一个背包总容量为V,求所选物品总体积不超过V时可获得的最大价值。
限制条件:
1<=n<=100; 1<=v<=100; 1<=w<=107; 1<=W<=109
分析:题目与01背包问题完全一样,不一样的是限制条件,前面那道01背包的dp是针对不同的重量限制计算最大的价值,如果用在这里,一定会超时,因为W实在太大了,然而,不难发现,价值的大小相比较之下却很小。因此,我们可以尝试改变dp的对象,用dp针对不同的价值计算最小的重量。
用二维数组dp[i,j]解决:
状态:dp[i,j]表示前i件物品挑选出总价值和为j时需要的最小重量。
状态转移方程:
前i个物品挑选出价值总和为j时,有2种情况:
1.前i-1个物品中挑选出价值总和为j-v[i]的部分,再选中第i个物品;
2.前i-1个物品中挑选出价值总和为j的部分。

此外还有当前物品v[i]>j的情况,但这种情况事实上是等同于第2种情况,因为j-v[i]<0,价值不存在负数的情况,说明要从前i-1个物品中挑选出价值总和为j-v[i]的部分是不可能存在的,因此v[i]>j的情况要隶属于第2种情况。
dp[i,j]=
{ dp[i-1,j] j<v[i];
max(dp[i-1,j],dp[i-1,j-v[i]]+w[i]) j>=v[i] }

代码如下:
int dp[100+1,100*100+1];
dp[0,0]=0;
for(i=1;i<=10000;i++) dp[0,i]=INF;
for(i=1;i<=n;i++){
for(j=0;j<=10000;i++){
if(j<v[i]) dp[i,j]=dp[i-1,j];
else dp[i,j]=max(dp[i-1,j],dp[i-1,j-v[i]]+w[i]));
}
}
int ans;
for(i=0;i<=10000;i++)
if(dp[n,i]<=W){
ans=i;
}printf("%d\n",i);
return 0;

注:上面的代码有两点要特别注意:
1.数据的初始化:i=0时,表示前0个物品,此时什么也选不了。dp[0,0]表示,0件物品价值为0,因此此时可将重量初始为0,即dp[0,0]=0;而dp[0,j]表示,0件物品价值为j,显然这是不可能存在的情况,将重量初始为INF,即dp[0,j]=INF.
2.答案:最终的答案,就是dp[n,j]<=W中最大的j。

小结:此题具有较大的意义,**它提醒我们当以某个对象进行解题时,如果行不通,可以切换另一个对象进行尝试。**此外,我们还应从中知道,动态规划的状态转移方程是与所确定状态紧密联系的,因此要写出方程,还应仔细思考状态可能有哪些情况。(你可以看到,01背包1与01背包2的状态不同,而状态转移方程也随之变化很多)

多重背包问题

有n件物品,每件的价值及体积分别为v[i],w[i],且第i件物品有m[i]个,有一个背包总容量为V,求所选物品总体积不超过V时可获得的最大价值。
分析:
1.当w[i]×m[i]>V时,原问题就转化为完全背包问题了;
2.当w[i]×m[i]<=V时,可以通过拆分,转化为01背包问题。

这里不太方便写代码,待以后遇到确切题目,再将代码复制过来。
伪代码先参照http://www.doc88.com/p-2961204836432.html

混合背包问题

有n件物品,每件的价值及体积分别为v[i],w[i],且有的物品只可拿1件,有的可以拿无限多件,有的可以有上限件,有一个背包总容量为V,求所选物品总体积不超过V时可获得的最大价值。
分析:
这道题是01,完全,多重背包的混合体。先考虑01背包与完全背包混合。
伪代码:
for(i=1;i<=n;i++){
if(第i件物品属于01背包问题)
{
for(j=V;j>=w[i];j–)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
else if(第i件物品属于完全背包问题)
{
for(j=w[i];j<=V;j++)
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
若再考虑上多重背包:
for(i=1;i<=n;i++){
if(第i件物品属于01背包问题)
{
zopack;
}
else if(第i件物品属于完全背包问题)
{
cpack;
}
else if(第i件物品属于多重背包)
{
mpack;
}

二维费用背包问题

二维费用的背包问题是指:对于每件物品,具有两种不同的费用,选择这件物品时必须付出同时付出这两种费用。对于每种费用都有一个可付出的最大值(背包容量)。问怎样选择物品,才能得到最大的价值。
设第i件物品所需费用为ci和di,两种费用可付出的最大值分别为V,U。物品的价值为wi。
算法:费用增加了一维,因此只需算法增加了一维即可。设dp[i,j,k]表示前i件物品付出j,k费用时可获得的最大价值,状态方程为:
dp[i,j,k]=max(dp[i-1,j,k],dp[i-1,j-ci,k-di]+w[i]);
如需优化空间复杂度,可改用二维数组dp[j,k],如果问题是01背包问题,则j,k采用逆序方法处理,如果是完全背包,则j,k采用顺序方法处理。

**有时候,第二维的费用并不是直接给出的,而是以这样一种隐形的方式给出:最多只能取走U件物品。**事实上相当于多了一个“件数”的费用,每个物品的件数费用均为1。设dp[i,j,k]表示前i件物品付出j费用,选取k件时可获得的最大价值,状态方程为:
dp[i,j,k]=max(dp[i-1,j,k],dp[i-1,j-ci,k-1]+w[i]);
同样的,如果改用二维数组dp[j,k],要根据问题的类型对循坏方向进行处理。

总结与感悟

目前,因为能力有限,背包问题的暂时就学到这5种问题。在学习的过程当中,有一个深深的体会就是:动态规划,是一个十分抽象的算法,它十分的考验思维以及逻辑的严密性。动态规划的核心是状态以及状态转移方程,其中,又以状态的确定为基础,才能推到出状态转移方程,也就是说,先找准状态,才根据状态,推导出状态转移方程。此外,有时候还要考虑一个边界的处理,边界的初始状态不同,很可能会导致最终的结果不同(比如01背包和01背包之2)。
总而言之,动态规划,是利用抽象的思维来解决问题的,处理动态规划问题时,一定要找准状态,通过严密的思维和逻辑推导出方程,并处理好边界的状态。
接下来就要开始多刷题了,动态规划要入门,或许还要挺长的一段时间,要不断的刷题,以求能够灵活的处理各类不同的问题。

猜你喜欢

转载自blog.csdn.net/shamansi99/article/details/87658329