01,完全,多重,混合背包

动态规划理论基础

首先我们要知道,只有当问题符合最优化原理和无后效原理,才适合使用动态规划

最优化原理

最优化原理定义的最优策略:不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的决策必须构成最优策略
简单来说就是一个最优策略的子策略(之后产生的策略)也是必须是最优的,而所有子问题的局部最优解将导致整个问题的全局最优
如果一个问题能满足最优化原理,就称其具有最优子结构性质,这是判断问题能否使用动态规划解决的先决条件,如果一个问题不能满足最优化原理,那么这个问题就不适合用动态规划来求解
举个例子:

棋盘上A点到B点的最短距离,那么子问题就是求从A点到B点之间的 中间点 到B点的最短距离
怎么证明最优化原理呢?
我们假设从A点到C点的最短距离为d,假设其最优策略的子策略经过B点,记该策略中B点到C点的距离为d1,A点到B点的距离为d2
用反证法,假设存在B点到C点的最短距离d3,并且d3 < d1,那么 d3 + d2 < d1 + d2 = d,这与d是最短距离相矛盾,所以,d1是B点到C点的最短距离
再举一个反例:

求A到D的所有通道中,总长度除以4得到的余数最小的路径为 最优路径,求一条最优路径
按照之前的思路,A的最优取值应该可以由B的最优取值来确定,而B的最优取值为(3+5)mod 4 = 0,所以应该选d2和d6这两条道路,而实际上,全局最优解是d4+d5+d6或者d1+d5+d3,所以这里子问题的最优解并不是原问题的最优解,即不满足最优化原理,所以就不适合使用动态规划来求解了

无后效性

某状态下决策的收益,只与状态和决策相关,与到达该状态的方式无关
某个阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响。换句话说,未来与过去无关,当前状态是此前历史状态的完整总结,此前历史决策只能通过影响当前的状态来影响未来的演变。再换句话说,过去做的选择不会影响现在能做的最优选择,现在能做的最优选择只与当前的状态有关,与经过如何复杂的决策到达该状态的方式无关
这也是用来验证问题是否可以使用动态规划来解答的重要方法
我们再回头看看上面的最短路径问题,如果在原来的基础上加上一个限制条件:同一个格子只能通过一次。那么, 这个题就不符合无后效性了,因为前一个子问题的解会对后面子问题的选择策略有影响

01背包

有N件物品和一个容量为C的背包,第i件物品的费用(占空间/重量)是w[i] ,价值是v[i]每种物品仅有一件,可以选择放或不放,求将哪些物品装入背包可使价值总和最大
假设背包总容量为10,有5个物品,它们的价值(v)和重量(w)如下表:

编号 1 2 3 4
价值v 2 4 3 7
重量w 2 3 5 5
这里每个物品只有一个,对于每个物品而言,只有两种选择,要或不要,记为1和0,所以叫01背包
xi代表第i个物品的选择(xi = 1 要,0则代表不要),vi代表第i个物品的价值,wi代表第i个物品的重量,我们背包的初始状态是容量为10,包内物品总价值为0,接下来,我们就要开始做选择了。对于1号物品,当前容量为10,容纳它的重量2绰绰有余,因此有两种选择,选它或者不选。我们选择一个物品的时候,背包的容量会减少,但是里面的物品总价值会增加
那么对于物品2,当前剩余容量为8,大于物品2的容量3,因此也有两种选择,选或者不选
现在,我们得到了四个可能结果,我们每做出一个选择,就会将上面的每一种可能分裂成两种可能,后续的选择也是如此,最终,我们会得到如下的一张决策图
红色方框代表我们的最终待选结果,本来应该有16个待选结果,但有三个结果由于容量不足以容纳下最后一个物品,所以就没有继续进行裂变。然后,从这些结果中找出价值最大的,也就是13,这就是我们的最优选择,根据这个选择,依次找到它的所有路径,便可以知道该选哪几个物品

分治

接下来,我们就来分析一下,如何将它扩展到一般情况。为了实现这个目的,我们需要将问题进行抽象并建模,然后将其划分为更小的子问题,找出递推关系式

  1. 抽象问题,背包问题抽象为寻找组合(x1,x2,x3…xn,其中xi = 0或1,表示第i个物品取或者不取),vi代表第i个物品的价值,wi代表第i个物品的重量,总物品数为n,背包容量为c
  2. 建模,问题即为max(x1×v1 + x2×v2 + … + xn×vn)
  3. 约束条件,x1×w1 + x2×w2 + … + xn×wn < c
  4. 定义函数KS(i,j):代表当前背包剩余容量为j时,前i个物品最佳组合所对应的价值
    对于第i个物品,有两种可能:
  5. 背包剩余容量不足以装下该物品,此时背包的价值与前i-1个物品的价值是一样的,KS(i,j) = KS(i-1,j)
  6. 背包剩余容量可以装下该物品,此时需要进行判断,因为装了该商品不一定能使最终组合达到最大价值,如果不装该商品,则价值为:KS(i-1,j),如果装了该商品,则价值为KS(i-1,j-wi) + vi,从两者中选择较大的那个,
    所以可以得到递推关系式
KS(i,j)=KS(i-1,j)  //j<wi
KS(i,j)=max[KS(i-1,j),KS(i-1,j-wi)+vi]
  • 原问题是,将n件物品放入容量为c的背包
  • 子问题则是,将前i件物品放入剩余容量为j的背包,所得到的最优价值为KS(i,j)
    如果只考虑第i件物品放还是不放,那么就可以转化为一个只涉及到前i-1个物品的问题
    如果不放第i个物品,那么问题就转化为“前i-1件物品放入容量为j的背包中的最优价值组合”,对应的值为KS(i-1,j)
    如果放第i个物品,那么问题就转化成了“前i-1件物品放入容量为j-wi的背包中的最优价值组合”,此时对应的值为KS(i-1,j-wi)+vi
#include <bits/stdc++.h>
using namespace std;
int ks(int i, int c)
{
   int vs[5] = {0,2,4,3,7};
   int ws[5] = {0,2,3,5,5};
   int result = 0;
   if (i == 0 || c == 0)
   {
       // 初始条件
       result = 0;
   }
   else if(ws[i] > c)
   {
       // 装不下该物品
       result = ks(i-1, c);
   }
   else
   {
       // 可以装下
       int tmp1 = ks(i-1, c);
       int tmp2 = ks(i-1, c-ws[i]) + vs[i];
       result = max(tmp1, tmp2);
   }
   return result;
}
int main()
{
   int result = ks(4,10);
   cout<<result<<endl;
}

动态规划

先来看看最优化原理。同样,我们使用反证法:
假设(x1,x2,…,xn)是01背包问题的最优解,则有(x2,x3,…,xn)是其子问题的最优解,假设(y2,y3,…,yn)是上述问题的子问题最优解,则有(v2y2+v3y3+…+vnyn)+v1x1 > (v2x2+v3x3+…+vnxn)+v1x1。说明(X1,Y2,Y3,…,Yn)才是该01背包问题的最优解,这与最开始的假设(X1,X2,…,Xn)是01背包问题的最优解相矛盾,故01背包问题满足最优化
无后效性更好理解了。对于任意一个阶段,只要背包剩余容量和可选物品是一样的,那么我们能做出的现阶段的最优选择必定是一样的,是不受之前选择了什么物品所影响的。即满足无后效性

自上而下记忆法

与分治法的区别只是用一个二维数组用来存储计算的中间结果,减少重复计算,于是新建一个二维数组:

表中每一个格子都代表一个子问题,我们最终的问题是求最右下角的格子的值,也就是i=4,j=10时的值。这里,我们的初始条件便是i=0或者j=0时对应的ks值为0

#include <bits/stdc++.h>
using namespace std;
int ks(int i, int c){
   int vs[5] = {0,2,4,3,7};
   int ws[5] = {0,2,3,5,5};
   int results[5][11]={0};
   int result = 0;
   // 如果该结果已经被计算,那么直接返回
   if (results[i][c] != 0)
       return results[i][c];
   if (i == 0 || c == 0){
       // 初始条件
       result = 0;
   }
   else if(ws[i] > c){
       // 装不下该物品
       result = ks(i-1, c);
   }
   else{
       // 可以装下
       int tmp1 = ks(i-1, c);
       int tmp2 = ks(i-1, c-ws[i]) + vs[i];
       result = max(tmp1, tmp2);
       results[i][c] = result;
   }
   return result;
}
int main()
{
   int result = ks(4,10);
   cout<<result<<endl;
}

自下而上填表法

接下来,我们用自下而上的方法来解一下这道题,思路很简单,就是不断的填表,回想一下上一篇中的斐波拉契数列的自下而上解法,这里将使用同样的方式来解决。还是使用上面的表格,我们开始一行行填表

当i=1时,即只有物品1可供选择,那么如果容量足够的话,最大价值自然就是物品1的价值了

当i=2时,有两个物品可供选择,此时应用上面的递推关系式进行判断即可。这里以i=2,j=3为例进行分析:

剩下的格子使用相同的方法进行填充即可

这样,我们就得到了最后的结果:13。根据结果,我们可以反向找出各个物品的选择,寻找的方法很简单,就是从i=4,j=10开始寻找,如果ks(i-1,j)=ks(i,j),说明第i个物品没有被选中,从ks(i-1,j)继续寻找。否则,表示第i个物品已被选中,则从ks(i-1,j-wi)开始寻找

#include <bits/stdc++.h>
using namespace std;
int ks(int i, int c)
{
   int vs[5] = {0,2,4,3,7};
   int ws[5] = {0,2,3,5,5};
   int results[5][11]= {0};
   // 初始化
   for (int m = 0; m <= i; m++)
   {
       results[m][0] = 0;
   }
   for (int m = 0; m <= j; m++)
   {
       results[0][m] = 0;
   }
   // 开始填表
   for (int m = 1; m <= i; m++)
   {
       for (int n = 1; n <= j; n++)
       {
           if (n < ws[m])
           {
               // 装不进去
               results[m][n] = results[m-1][n];
           }
           else
           {
               // 容量足够
               if (results[m-1][n] > results[m-1][n-ws[m]] + vs[m])
               {
                   // 不装该珠宝,最优价值更大
                   results[m][n] = results[m-1][n];
               }
               else
               {
                   results[m][n] = results[m-1][n-ws[m]] + vs[m];
               }
           }
       }
   }
   return results[i][j];
}
int main()
{
   int result = ks(4,10);
   cout<<result<<endl;
}

动态规划里最关键的问题其实是寻找原问题的子问题,并写出递推表达式,只要完成了这一步,代码部分都是水到渠成的事情了
用子问题定义状态:用 f [i] [j] 表示已经处理到第 i 个物品,前 i 件物品放入一个剩余容量为 j 的背包可以获得的最大价值,那么会先出现两种情况

  1. 背包体积 j < i 的体积 w[i],这时候背包容量不足以放下第 i 件物品,只能选择不拿:f [i] [j] = f [i-1] [j]
  2. 背包体积 j >= i 的体积 w[i],这时候背包容量可以放下第 i 件物品,我们就要考虑是拿还是不拿:
  3. 拿: f [i] [j] = f [i-1] [j-w[i]]+v[i]; f [i-1] [j-w[i]] 指的是前 i-1 件物品,背包容量为j-w[i]时的最大价值(相当于为第i件物品腾出了w[i]的空间)
  4. 不拿:f [i] [j] = f [i-1] [j] 同1
    所以可以得到状态转移方程
if (j<w[i]) //背包剩余容量j小于物品i的体积
   f[i][j] = f[i-1][j] //装不下第i个物体,目前只能靠前i-1个物体装包
else
   f[i][j] = max(f[i-1][j], f[i-1][j-w[i]] + v[i])
f[i][j]=max(f[i−1][j],f[i−1][j−w[i]]+v[i])

i 件物品放入一个容量恰为 j 的背包可以获得的最大价值
价值数组v = {8, 10, 6, 3, 7, 2},
重量数组w ={4, 6, 2, 2, 5, 1},
背包容量C = 12时对应的m[i][j]数组

i\j 1 2 3 4 5 6 7 8 9 10 11 12
1 0 0 0 8 8 8 8 8 8 8 8 8
2 0 0 0 8 8 10 10 10 10 18 18 18
3 0 6 6 8 8 14 14 16 16 18 18 24
4 0 6 6 9 9 14 14 17 17 19 19 24
5 0 6 6 9 9 14 14 17 17 19 21 24
6 2 6 8 9 11 14 16 17 19 19 21 24
左上角按行求解,一直求解到右下角
for (int i = 1;i <= N;i++) //枚举物品
	for (int j = 0;j <= W;j++) { //枚举背包容量
 f[i][j] = f[i - 1][j];
 if (j >= w[i])
               f[i][j] = max(f[i-1][j],f[i-1][j-w[i]] + v[i]);
	}

例题HDU2602

分治和动态规划的区别

共同点:二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题,然后将子问题的解合并,形成原问题的解
不同点:

  • 分治法将分解后的子问题看成相互独立的,通过用递归来做
  • 动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做

空间优化(滚动数组)

递推本来就是用空间换时间,消耗的空间比较大
可以发现,每次求解 KS(i,j)只与KS(i-1,m) {m:1...j} 有关。也就是说,如果我们知道了K(i-1,1...j)就肯定能求出KS(i,j),如图
下一层只需要根据上一层的结果即可推出答案,举个栗子,看i=3,j=5时,在求这个子问题的最优解时,根据上述推导公式,KS(3,5) = max{KS(2,5),KS(2,0) + 3} = max{6,3} = 6;如果我们得到了i=2时所有子问题的解,那么就很容易求出i=3时所有子问题的解
因此,我们可以将求解空间进行优化,将二维数组压缩成一维数组,此时,装填转移方程变为:
KS(j) = max{KS(j),KS(j - wi) + vi}
这里KS(j - wi)就相当于原来的KS(i-1, j - wi)。需要注意的是,由于KS(j)是由它前面的KS(m){m:1..j}推导出来的,所以在第二轮循环扫描的时候应该由后往前进行计算,因为如果由前往后推导的话,前一次循环保存下来的值可能会被修改,从而造成错误

这么说也许还是不太清楚,回头看上面的图,我们从i=2推算i=3的子问题的解时,此时一维数组中存放的是{0,0,2,4,4,6,6,6,6,6,6},这是i=2时所有子问题的解,如果我们从前往后推算i=3时的解,比如,我们计算KS(0) = 0,KS(1) = KS(1) = 0 (因为j=1时,装不下三个物品,第三个物品的重量为5),KS(2)=2,KS(3)=4,KS(4)=4, KS(5) = max{KS(5), KS(5-5) + 3} = 6,....,KS(8) = max{KS(8),KS(8 - 5) + 3} = 7。在这里计算KS(8)的时候,我们就把原来KS(8)的内容修改掉了,这样,我们后续计算就无法找到这个位置的原值,也就是上一轮循环中计算出来的值了,所以在遍历的时候,需要从后往前进行倒序遍历

#include <bits/stdc++.h>
using namespace std;
int ks(int i, int c)
{
   int vs[5] = {0,2,4,3,7};
   int ws[5] = {0,2,3,5,5};
   int newResults[11]= {0};
   for (int m = 0; m < 5; m++)
   {
       int w = ws[m];
       int v = vs[m];
       for (int n = c; n >= w; n--)
       {
           newResults[n] = max(newResults[n], newResults[n - w] + v);
       }
       // 可以在这里输出中间结果
       for(int i =0;i<11;i++)
           printf("%d ",newResults[i]);
       printf("\n");
   }
   return newResults[11 - 1];
}
int main()
{
   int result = ks(4,10);
   cout<<result<<endl;
}
0 0 0 0 0 0 0 0 0 0 0
0 0 2 2 2 2 2 2 2 2 2
0 0 2 4 4 6 6 6 6 6 6
0 0 2 4 4 6 6 6 7 7 9
0 0 2 4 4 7 7 9 11 11 13
13

对于将来肯定用不到的数据,直接覆盖,所以这个方法叫滚动数组
缺点是牺牲了抹除了大量数据,不是每道题都可以用,但是在这里我们要的答案刚好是递推的最后一步,所以直接输出即可

先回顾我们之前的状态转移方程

f[i][j] = max(f[i-1][j], f[i-1][j-W[i]] + [i]])

想知道f [i] [j] ,需要 f [i-1] [j] 和 f [i-1] [j-w[i]] ,我们之前是使用二维数组保存中间状态,所以可以直接取出这两个状态的值
我们可以直接使用一维数组 f [j] 表示:在执行 i 次循环后(已经处理 i 个物品),前 i 个物体放到剩余容量 j 时的最大价值,即之前的 f [i] [v]
与二维相比较,它把第一维隐去了,但是二者表达的含义还是相同的,只不过针对不同的i,f[j] 一直在重复使用,所以,也会出现第i次循环可能会覆盖第 i - 1 次循环的结果

   for (int i = 1;i <= N;i++) //枚举物品  
       for (int j = W; j >= w[i]; j--) //枚举背包容量
           f[j] = max(f[j],f[j] - w[i]] + v[i]);

初始值

01背包问题一般有两种不同的问法,一种是“恰好装满背包”的最优解,要求背包必须装满,那么在初始化的时候,除了KS(0)0,其他的KS(j)都应该设置为负无穷大,这样就可以保证最终得到的KS(c)是恰好装满背包的最优解。另一种问法不要求装满,而是只希望最终得到的价值尽可能大,那么初始化的时候,应该将KS(0...c)全部设置为0
为什么呢?因为初始化的数组,实际上是在没有任何物品可以放入背包的情况下的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么都不装且价值为0的情况下被“恰好装满”,其他容量的背包均没有合法的解,因此属于未定义的状态,应该设置为负无穷大。如果背包不需要被装满,那么任何容量的背包都有合法解,那就是“什么都不装”。这个解的价值为0,所以初始状态的值都是0

完全背包

有N件物品和一个容量为C的背包,第i件物品的费用(占空间/重量)是w[i] ,价值是v[i]每种物品都有无限件可用,求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大
贪心?把每种物品的价格除以体积来算出它们各自的性价比,然后只选择性价比最高的物品放入背包中
但这种做法还有一个问题,单个物品是无法拆分,不能选择半件,这样往往无法用性价比最高的物品来装满整个背包,比如背包空间为10,性价比最高的物品占用空间为7,那么剩下的空间该如何填充呢?
你可能还会想到用性价比第二高的物品填充,如果仍旧无法填满,那就依次用第三、第四性价比物品来填充,看似可行,但只需要举一个反例便能证明这个策略不可用:

  • 只有两个物品:A价值5,体积5;B价值8,体积7,背包容量为10

递归法

由01背包的

f[i][j]=max(f[i−1][j],f[i−1][j−w[i]]+v[i])

可以得到

f[i][j]=max [ (f[i−1][j−k*w[i]]+k*v[i]) ,0<=k*w[i]<=j ]

f [i] [j] 依然表示已经处理到第 i 个物品,前 i 件物品放入一个剩余容量为 j 的背包可以获得的最大价值
f [i−1] [j−k × w[i]] + k × v[i] 表示:i-1 种物品中选取若干件物品放入剩余空间为 j-k × w[i] 的背包中所能得到的最大价值 加上 k 件第 i 种物品的总价值

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2;
int P[maxn+1] = {0,5,8};
int V[maxn+1] = {0,5,7};
int C = 10;
int ks(int i, int t)
{
   int result = 0;
   if (i == 0 || t == 0)
   {
       // 初始条件
       result = 0;
   }
   else if(V[i] > t)
   {
       // 装不下该珠宝
       result = ks(i-1, t);
   }
   else
   {
       // 可以装下
       // 取k个物品i,取其中使得总价值最大的k
       for (int k = 0; k * V[i] <= t; k++)
       {
           int temp = ks(i-1, t - V[i] * k) + P[i] * k;
           if (temp > result)
           {
               result = temp;
           }
       }
   }
   return result;
}
int main()
{
   int result = ks(maxn, 10);
   printf("%d",result);
}

对比一下01背包问题中的递归解法,就会发现唯一的区别便是这里多了一层循环,因为01背包中,对于第i个物品只有选和不选两种情况,只需要从这两种选择中选出最优的即可,而完全背包问题则需要在k种选择中选出最优解,这便是最内层循环在做的事情

for (int k = 0; k * V[i] <= t; k++){
   // 选取k个第i件商品的最优价值为tmp2
   int temp = ks(i-1, t - V[i] * k) + P[i] * k;
   if (temp > result){
       // 从中拿出最大的值即为最优解
       result = temp;
   }
}

最优化原理和无后效性

那这个问题可以不可以像01背包问题一样使用动态规划来求解呢?来证明一下即可。
首先,先用反证法证明最优化原理:假设完全背包的最优解为F(n1,n2,…,nN)(n1,n2 分别代表第1、第2件物品的选取数量),完全背包的子问题为,将前i种物品放入容量为t的背包并取得最大价值,其对应的解为:F(n1,n2,…,ni),假设该解不是子问题的最优解,即存在另一组解F(m1,m2,…,mi),使得F(m1,m2,…,mi) > F(n1,n2,…,ni),那么F(m1,m2,…,mi,…,nN) 必然大于 F(n1,n2,…,nN),因此 F(n1,n2,…,nN) 不是原问题的最优解,与原假设不符,所以F(n1,n2,…,ni)必然是子问题的最优解
再来看看无后效性:前i种物品如何选择,只要最终的剩余背包空间不变,就不会影响后面物品的选择,满足无后效性
因此,完全背包问题也可以使用动态规划来解决。

动态规划

ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= t)

递推法中,已经找到了递推关系式,就是状态转移方程

自上而下记忆法

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2;
int P[maxn+1] = {0,5,8};
int V[maxn+1] = {0,5,7};
int C = 10;
int results[maxn+1][10]={0};
int ks(int i, int t)
{
// 如果该结果已经被计算,那么直接返回
   if (results[i][t] !=0)
       return results[i][t];
   int result = 0;
   if (i == 0 || t == 0)
   {
       // 初始条件
       result = 0;
   }
   else if(V[i] > t)
   {
       // 装不下该珠宝
       result = ks(i-1, t);
   }
   else
   {
       // 可以装下
       // 取k个物品,取其中使得价值最大的
       for (int k = 0; k * V[i] <= t; k++)
       {
           int temp = ks(i-1, t - V[i] * k) + P[i] * k;
           if (temp > result)
           {
               result = temp;
           }
       }
   }
   results[i][t] = result;
   return result;
}
int main()
{
   int result = ks(maxn, 10);
   printf("%d",result);
}

自下而上填表法



这里当t=10时,因为最多只能放得下1个i2物品,所以只需要将两个数值进行比较,如果t=14,那么就需要将取0个、1个和两个i2物品的情况进行比较,然后选出最大值。

for (int i = 1; i <= maxn; i++)
   for (int j = 0; j < C; j++)
       for (int k = 0; k * V[i] <= j; k++)
           KS[i][j] = max(KS[i][j], KS[i-1][j-k * V[i]] + k * P[i]);

跟01背包问题一样,完全背包的空间复杂度也可以进行优化
优化后的状态转移方程为:

#include <bits/stdc++.h>
using namespace std;
const int n = 2;
int P[n+1] = {0,5,8};
int V[n+1] = {0,5,7};
int C = 10;
int results[11]={0};
int main()
{
	for(int i=1; i<=n; i++)
       for(int j=P[i]; j<=C; j++)//注意此处,与0-1背包不同,这里为顺序,0-1背包为逆序
           results[j]=max(results[j],results[j-P[i]]+V[i]);
   printf("%d",results[C]);
}

输出如下:

[0,0,0,0,0,0,0,0,0,0,0]
[0,0,0,0,0,5,5,5,5,5,10]
[0,0,0,0,0,5,5,8,8,8,10]
10

其实完全背包问题也可以转化成01背包问题来求解,因为第i件物品最多选 C/Vi(向下取整) 件,于是可以把第i种物品转化为C/Vi件体积和价值相同的物品,然后再来求解这个01背包问题。具体方法这里就不多说了,留给大家自行解决。如果遇到问题,可以翻开前面关于01背包问题的两篇文章。

转化为01背包

先复习一下01背包状态转移方程

f[i][j] = max(f[i-1][j], f[i-1][j-W[i]]+V[i])
for (i = 1; i <= n; i++) //n为物品个数
	for (j = 1; j <= C; j++) //C为背包总容量
 if (j < C[i])
 	KS[i][j] = KS[i - 1][j];
 else
 	KS[i][j] = max(KS[i - 1][j], KS[i - 1][j - C[i]] + v[i]);

然后我们看到完全背包的初级状态转移方程

f[i][j] = max(f[i-1][j], f[i-1][j-k*W[i]]+k*V[i])(1 <= k <= C/W[i])
	f[i][j] = max(f[i-1][j-k*W[i]]+k*V[i])                    (0 <= k <= C/W[i])

把 k=0 拿出来单独考虑,即比较在不放第i种物品放第i种物品k件(k>=1)中结果最大的那个k这两种情况下谁的结果更大

f[i][j] = max(f[i-1][j], max(f[i-1][j-k*W[i]]+k*V[i]) )           (k >= 1)

考虑上式放第i种物品这种情况:放的话至少得放1件,先把这确定的1件放进去,即:在第i件物品已经放入1件的状态下再考虑放入k(k>=0)件这种物品的结果是否更大(如果k=1,说明第i种物品放了2件,因为前提状态是必然有一件物品已经放入)

f[i][j] = max( f[i-1][j], max( f[i-1][(j-W[i])-k*W[i]]+k*V[i] )+V[i] )	(k >= 0)

结合之前蓝色的式子,可以发现,上式的后半部分就等于f [i] [j - w[i]] + v[i],于是得出最终状态转移方程:

	f[i][j] = max(f[i-1][j], f[i][j-W[i]]+V[i])  

例题HDU1114

多重背包

有N件物品和一个容量为的背包,第i件物品的费用(占空间/重量)是w[i] ,价值是v[i]每种物品只有 n[i] 件可用,求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大
对比一下完全背包,其实只是多了一个限制条件,完全背包问题中,物品可以选择任意多件,只要你装得下,装多少件都行,但多重背包就不一样了,每种物品都有指定的数量限制,所以不是你想装,就能一直装的。
举个栗子:有A、B、C三种物品,相应的数量、价格和占用空间如下图:

物品 数量 价值 体积
A 4 2 3
B 3 3 4
C 2 4 5

递归法

用ks(i,j)表示前i种物品放入一个容量为 j 的背包获得的最大价值,那么对于第 i 种物品,我们有 k 种选择,0 <= k <= M[i] && 0 <= k * V[i] <= j,即可以选择0、1、2…M[i]个第i种物品,所以递推表达式为:

ks(i,j) = max{ks(i-1, j - V[i] * k) + P[i] * k}; (0 <= k <= M[i] && 0 <= k * V[i] <= j)

同时,ks(0,j)=0;ks(i,0)=0;
对比一下完全背包的递推关系式:

ks(i,j) = max{ks(i-1, j - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= j)

简直一毛一样,只是k多了一个限制条件而已。

	int ks(int i, int j){
       int result = 0;
       if (i == 0 || j == 0){
           // 初始条件
           result = 0;
       } else if(V[i] > j){
           // 装不下该珠宝
           result = ks(i-1, j);
       } else {
           // 可以装下
           // 取k个物品i,取其中使得总价值最大的k
           for (int k = 0; k <= M[i] && k * V[i] <= j; k++){
               int tmp2 = ks(i-1, j - V[i] * k) + P[i] * k;
               if (tmp2 > result){
                   result = tmp2;
               }
           }
       }
       return result;
   }

完全背包中的递归解法:

int ks(int i, int j){
   int result = 0;
   if (i == 0 || j == 0){
       // 初始条件
       result = 0;
   } else if(V[i] > j){
       // 装不下该珠宝
       result = ks(i-1, j);
   } else {
       // 可以装下
       // 取k个物品i,取其中使得总价值最大的k
       for (int k = 0; k * V[i] <= j; k++){
           int tmp2 = ks(i-1, j - V[i] * k) + P[i] * k;
           if (tmp2 > result){
               result = tmp2;
           }
       }
   }
   return result;
}

仅仅多了一个判断条件k <= M ,所以只要弄懂了完全背包,多重背包就不值一提了。
最优化原理和无后效性的证明跟多重背包基本一致,所以就不重复证明了

动态规划

自上而下记忆法

ks(i,j) = max{ks(i-1, j - V[i] * k) + P[i] * k}; (0 <= k <= M[i] && 0 <= k * V[i] <= j)
	int ks2(int i, int j){
       // 如果该结果已经被计算,那么直接返回
       if (results[i][t] != 0) return results[i][t];
       int result = 0;
       if (i == 0 || j == 0){
           // 初始条件
           result = 0;
       } else if(V[i] > j){
           // 装不下该珠宝
           result = ks2(i-1, j);
       } else {
           // 可以装下
           // 取k个物品,取其中使得价值最大的
           for (int k = 0; k <= M[i] && k * V[i] <= j; k++){
               int tmp2 = ks2(i-1, j - V[i] * k) + P[i] * k;
               if (tmp2 > result){
                   result = tmp2;
               }
           }
       }
       results[i][t] = result;
       return result;
   }

自下而上填表法

 for (int i = 0; i < n; i++){
           for (int j = 0; j <= C; j++){
               for (int k = 0; k <= M[i] && k * V[i] <= j; k++){
                   dp[i+1][j] = Math.max(dp[i+1][j], dp[i][j-k * V[i]] + k * P[i]);
               }
           }
       }

优化后:

ks(j) = max{ks(j), ks(j - Vi) + Pi}
#include <bits/stdc++.h>
using namespace std;
const int n = 3;
int P[n+1] = {2,3,4};
int V[n+1] = {3,4,5};
int M[n+1] = {4,3,2};
int C = 15;
int newResults[16] ;
int ksp(int i, int j)
{
   // 开始填表
   for (int m = 0; m < i; m++)
   {
       // 考虑第m个物品
       // 分两种情况
       // 1: M[m] * V[m] > C 则可以当做完全背包问题来处理
       if (M[m] * V[m] >= C)
       {
           for (int n = V[m]; n <= j ; n++)
           {
               newResults[n] = max(newResults[n], newResults[n - V[m]] + P[m]);
           }
       }
       else
       {
           // 2: M[m] * V[m] < C 则需要在 newResults[n-V[m]*k] + P[m] * k 中找到最大值,0 <= k <= M[m]
           for (int n = V[m]; n <= j ; n++)
           {
               int k = 1;
               while (k < M[m] && n > V[m] * k )
               {
                   newResults[n] = max(newResults[n], newResults[n - V[m] * k] + P[m] * k);
                   k++;
               }
           }
       }
       // 可以在这里输出中间结果
   }
   return newResults[C];
}
int main()
{
   int result = ksp(n+1,C);
   printf("%d",result);
}
[0,0,0,0,2,2,2,4,4,4,6,6,6,8,8,8]
[0,0,0,0,2,3,3,4,5,6,6,7,8,9,9,10]
[0,0,0,0,2,3,4,4,5,6,7,8,8,9,10,11]
11

这里有一个较大的不同点,在第二层循环中,需要分两种情况考虑,如果 M[m] * V[m] >= C ,那么第m个物品就可以当做完全背包问题来考虑,而如果 M[m] * V[m] < C,则每次选择时,需要从 newResults[n-V[m]*k] + P[m] * k(0 <= k <= M[m])中找到最大值

例题HDU2844

01,完全,多重 总结

回顾一下三个背包问题的定义:

01背包:
有N件物品和一个容量为V的背包,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品可以使得背包中总价值最大。
完全背包:
有N种物品和一个容量为V的背包,每种物品都有无限件可用,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品可以使得背包中总价值最大。
多重背包:
有N种物品和一个容量为V的背包,第i种物品最多有Mi件可用,每件物品消耗的容量为Ci,价值为Wi,求解入哪些物品可以使得背包中总价值最大。
三种背包问题都有一个共同的限制,那就是背包容量,背包的容量是有限的,这便限制了物品的选择,而三种背包问题的共同目的,便是让背包中的物品价值最大。
不同的地方在于物品数量的限制,01背包问题中,每种物品只有一个,对于每种物品而言,便只有选和不选两个选择。完全背包问题中,每种物品有无限多个,所以可选的范围要大很多。在多重背包问题中,每种物品都有各自的数量限制。
三种背包问题虽然对于物品数量的限制不一样,但都可以转化为01背包问题来进行思考。在完全背包问题中,虽然每种物品都可以选择无限个,但由于背包容量有限,实际上每种物品可以选择的数量也是有限的,那么将每种物品都看做是 V/Ci 种只有一件的不同物品,不就成了01背包问题吗?对于多重背包也是如此,只是每种物品的膨胀数量变成了 min{Mi, V/Ci}。
所以说,01背包问题是所有背包问题的基础,弄懂了01背包问题后,完全背包和多重背包就没有什么难的地方了。
下面我们来对比一下三种背包问题的状态转移方程,以便更好的理解它们之间的联系:
01背包的状态转移方程:

F[i,v] = max{F[i-1,v], F[i-1,v-Ci] + Wi}

完全背包的状态转移方程:

F[i,v] = max{F[i-1,v-kCi] + kWi | 0 <= kCi <= v}

多重背包的状态转移方程:

F[i,v] = max{F[i-1,v-kCi] + kWi | 0 <= k <= Mi}

把这三个方程放到一起,便能很清晰的看到它们之间的关系了,三种背包问题都是基于子问题来选取价值最大的一个,只是选择的范围不一样。
01背包考虑的是选和不选,所有只需要比较两种策略的最大值即可,而完全背包和多重背包要考虑的是选几个的问题。
这样说也许还是不够形象,举个栗子就能比较好的说明了:
假设背包容量为10,有两个物品可选,价值分别为:3,2,容量占用分别为,4,3。
假设背包容量为10,有两个物品可选,价值分别为:3,2,容量占用分别为,4,3。

序号 i 费用 P 价值 V
1 3 2
3 2 3
初始状态
01背包填表:
完全背包填表:
多重背包填表:
下面再来看看三种背包问题的一维数组解决方案
N代表物品数量,wi代表第i个物品占用的容量,C代表背包总容量,vi代表第i个物品的价值,Mi表示最多可选数量
01背包:
for i <- 1 to N
   for j <- C to wi
       F[j] = max{F[j],F[j-wi] + vi}

将其核心部分抽象出来:

def ZeroOneKnapsack(F,wi,W)
   for j <- C to wi
       F[j] = max{F[j],F[j-wi] + W}

则01背包问题可以表示为:

for i <- 1 to N
   ZeroOneKnapsack(F,wi,vi)

完全背包:

for i <- 1 to N
   for j <- wi to C
       F[j] = max{F[j],F[j-wi] + vi}

将其核心部分抽象出来:

def CompleteKnapsack(F,wi,W)
   for j <- wi to C
       F[j] = max{F[j],F[j-wi] + W}

则完全背包问题的解可以表示为:

for i <- 1 to N
   CompleteKnapsack(F,wi,vi)

多重背包:

for i <- 1 to N
   if j < wi * Mi
       F[j] = max{F[j],F[j-wi] + vi}
   else
       for j <- wi to C
           k <- 1
           while k < M && j > wi * k
               F[j] = max{F[j],F[j-wi*k] + vi*k}
               k++

抽象出核心逻辑:

def MultiKnapsack(F,wi,W,M)
   if wi * M >= C
       CompleteKnapsack(F,wi,W)
       return
   else
       k <- 1
       while k < M
           ZeroOneKnapsack(F,KC,KW)
           k++
       return

则多重背包问题的解可以表示为:

for i <- 1 to N
   MultiKnapsack(F,wi,vi,Mi)

混合背包

现在我们来考虑一种更为复杂的情况,如果可选的物品同时具有上述三种特性,即:有的物品只能选一个,有的物品可以选择任意多个,有的物品只能选择有限多个,那么此时该如何决策呢?
回顾一下上面的三种背包问题的抽象解,就会发现他们每次都只会考虑一种物品,区别只在于第i个物品的可选策略。所以对于混合背包问题,同样也可以一个一个物品考虑,如果这个物品是最多选一个,那么就采用01背包的解决策略,如果是可以选择任意多个,那么就使用完全背包的解决策略,如果只能选择有限多个,那么就使用多重背包的解决策略
伪代码如下:

for i <- 1 to N
   if 第i件物品属于01背包
       ZeroOneKnapsack(F,Ci,Wi)
   else if 第i件物品属于完全背包
       CompleteKnapsack(F,Ci,Wi)
   else if 第i件物品属于多重背包
       MultiKnapsack(F,Ci,Wi,Mi)

三种背包问题都是基于子问题来选取价值最大的一个,只是选择的范围不一样。
01背包考虑的是选和不选,所有只需要比较两种策略的最大值即可,而完全背包和多重背包要考虑的是选几个的问题

posted @ 2019-07-03 18:22  KelvinVS  阅读(877)  评论(0编辑  收藏  举报