动态规划——背包问题
背包问题是一类非常典型的动态规划问题,包括多种类型(01背包、完全背包、多重背包、混合背包、二维费用背包等)其基本类型为01背包问题。
一、01背包问题
N件物品,每件物品的重量和价值分别为 w[i], v[i], 把这些物品放到一个容量为W的背包中,求背包中物品的价值的最大值。
形式化定义:
分析
最直观的思路是枚举背包中出现的所有可能的物品组合,然后计算它们的价值和,求最大值。如果直接采用递归搜索,则会有大量的冗余,通过记忆化搜索的方式可以进行改进(记忆 max[i][w], 前i中物品总重量达到w时候的最大价值,其中i和w又是递归函数的参数)。通过动态规划方式可以进一步改进。
动态规划要考虑最优子结构和无后效性,对于该问题,假设前i-1种物品在重量和约束W下得到了一个最大价值和M,那么在考虑前i种物品在重量和约束W'下价值的最大值M'时,我们只关心前i-1种物品的重量和W和最大价值和M,至于这前i-1种物品时如何选择对求解M'无影响(因为M' = M + v[i],假设W + w[i] = W'),即无后效性;同时,前i种物品在重量约束W'下得到了价值的最大值,此时选取的物品集合为S,那么S中必然包含前i-1种物品在重量约束W(W = W'或W = W'-w[i])下得到最大值的组合(用反证法可以证明),即最优子结构。
因此考虑将问题规定为前i种物品总重量不超过w时,能够得到的最大价值 f[i][w]
。此时有递推公式
f[i][w] = max(f[i-1][w], f[i-1][w - w[i]] + v[i]) //情形1.不选择第i种物品,那么前i种物品可得到的最大值为 f[i-1][w]; //情形2.选择第i种物品,那么前i-1种物品可以使用 w -w[i]的空间存放,前i-1种物品可得最大价值 f[i-1][w-w[i]], 再加上第i种物品的价值 v[i] //在二者中间取最大值即可。
针对实际问题设置边界条件
显然i>=0, w>=0. f[0][w]表示不选任何物品时,总重量不超过w,取得的价值和的最大值,显然为0,即f[0][w] = 0; f[i][0] 表示前i中物品,总重量和不超过0,取得价值和的最大值,由于问题中重量均为正整数,因此,显然为0,即 f[i][0] = 0.
优化
可以看出,f[i][xx]只和f[i-1][yy]有关,假设当前有一个一维数组,存放了前i-1种物品在某约束xx下的最大值,则可以直接求出前i种物品在某约束yy下的最大值。因此可以使用一维数组,来滚动实现二维数组,进行空间优化(通过画图更直观一些)。
f[i][w] = max(f[i-1][w], f[i-1][w - w[i]] + v[i]),改变为一维数组 f[w] = max(f[w], f[w-w[i]] + v[i]),在一维数组中考虑,重量w只和w及w-w[i]有关,因此要从后向前进行递推(若从前向后,则会导致被修改值x之后的值都是在新的x基础上(即前i种物品选择)被修改,不符合在前i-1种物品中进行选择的要求。
实现
for(int i = 0; i <= W; i ++){ f[w] = 0; } for(int i = 1; i <= n; i ++){ for(int w = W; w >= w[i]; w --){ f[w] = max(f[w], f[w-w[i]] + v[i]); } }
复杂度分析
经过空间优化之后,空间复杂度为O(W), 时间复杂度为O(n*w).
二、完全背包问题
有N种物品,每种物品有个重量w[i]和价值v[i],每种物品可以选择无限多件。给定一个承重不超过 W的背包,求出背包中所放物品的价值和的最大值。
和01背包问题一样分析,设 f[i][w]表示前i种物品中选择总重不超过w的物品所能得到的价值和的最大值。有
f[i][w] = max{f[i-1][w], f[i][w - k*w[i]] + k*v[i]} | k属于[1, w/w[i]]
分析
观察f[i][w] = max{f[i-1][w - k*w[i]] + k*v[i]} | k属于[0, w/w[i]], max{f[i-1][w - k*w[i]] + k*v[i]} = max{f[i-1][w], f[i-1][w-k*w[i]] + k*v[i]} | k属于[1, w/w[i]], 令k = t+1, 则 max{f[i-1][w-k*w[i]] + k*v[i]} | k属于[1, w/w[i]], = max{f[i-1][w-w[i] - t*w[i]] + t*v[i]} + v[i] | t属于[0, (w-w[i])/w[i]], = f[i-1][w - w[i]] + v[i] 因此 f[i][w] = max{f[i-1][w], f[i][w-w[i]] + v[i]}
因此递推公式化简为了 f[i][w] = max{f[i-1][w], f[i][w-w[i]] + v[i]}
对于这个公式,其实可以这样理解:在求f[i][w]需要求 max{f[i-1][w - k*w[i]] + k*v[i]}时候,而在求f[i][w-w[i]]时,已经求了max{f[i-1][w - k*w[i]] + k*v[i]}中的前面若干项,因此,知道了 f[i-1][w-w[i]]时,就可以继续推出f[i][w],而不需要再重复求max{f[i-1][w - k*w[i]] + k*v[i]}。
同样,可以利用一维数组进行空间优化,得到递推公式 f[w] = max{f[w], f[w-w[i]] + v[i]
实现
for(int i = 0; i <= W; i ++){ f[w] = 0; } for(int i = 0; i <= n; i ++){ for(int w = w[i]; w <= W; w ++){ //这里递推从左向右!!因为就是需要前面的值更新对后面的值产生影响 f[w] = max{f[w], f[w - w[i]] + v[i]}; } }
复杂度分析
在经过空间优化之后,空间复杂度为O(W),时间复杂度为O(n*W)
对比01背包和完全背包
都可以使用一维数组来降低空间复杂度,使用一维数组时,01背包的w顺序为从后向前(w ~ w - w[i]),而完全背包是从前向后(w-w[i] ~ w)。
三、多重背包问题
N种物品,第i种物品的重量值为w[i], 价值为v[i],且第i种物品有c[i]个。给定背包的承重W,求能放入背包中的物品总价值的最大值。
分析
仿照01背包和完全背包,可以得到递推公式f[i][w] = max{f[i-1][w], f[i][w - k*w[i]] + k*v[i] | v属于[1, min(c[i], w/w[i])]
.
可以将第i种物品的c[i]个视为不同种类的物品,只不过他们的重量和价值相同,此时转化为01背包问题,即从 ∑n1c[i] 种物品,每种只有一件,选择总重量不超过W的物品组合,使得价值和最大。此时时间复杂度为 O(W*∑n1c[i])
进一步优化,考虑一个定理
给定一个正整数n,可以用1, 2,... 2^(k-1), n - 2^k + 1这些数(约log(n)个)来表示[1,n]中的任何一个整数,其中k为使得n - 2^k + 1 > 0的最大的正整数
即二进制分解。。
利用这个定理,将c[i]个重量为w[i]、价值为v[i]的第i种物品分解为 {重量,价值} = {w[i], v[i]}, {2*w[i], 2*v[i]}, ...{2^(k-1)*w[i], 2^(k-1)*v[i]},{(c[i] - 2^k + 1)*w[i], (c[i] - 2^k + 1)v[i]}的一系列物品,然后转换为01背包问题。注意若存在新转换后的{w[k], v[k]}和原来的某种物品{w[j], v[j]}相同,不能将他们进行合并。
此时时间复杂度为O(W*(∑n1log2(c[i]))。
实现
int weight[MAX]; int value[MAX]; void Expand(int w, int v, int n, int& index){ int k = 1; do{ weight[index] = k*w; value[index ++] = k*v; k*=2; }while(k*2 < n); weight[index] = (n - k + 1)*w; value[index++] = (n - k + 1)*v; }
四、混合背包问题
混合背包问题是N种物品中有些物品是只能选一件或者不选,有些物品是可以选任意件,有些物品有个数限制。在背包承重W的限制下,求能够得到的价值总和的最大值。
解决方法是将多重背包转换为01背包,然后解决01背包和完全背包的混合。
for(int i = 1; i <= n; i ++){ if(物品i为01背包物品){ for(int w = W; w >= w[i]; w --){ f[w] = max{f[w], f[w - w[i]] + v[i]}; } }else if(物品i为完全背包物品){ for(int w = w[i]; w <= W; w ++){ f[w] = max{f[w], f[w - w[i]] + v[i]}; } } }
五、二维费用的背包问题
N中物品,每件物品i都有两种不同的空间消耗,选择这件物品必须同时付出这两种代价。对于每种代价都有一个可付出的最大值(背包容量),问怎样选择物品使得总价值和最大。
分析
和一维约束没什么区别,只不过将状态表示增加一个维度,
f[i][w1][w2] = max{f[i-1][w1][w2], f[i-1][w1 - w1[i]][w2 - w2[i] + v[i]},利用空间优化之后为 f[w1][w2] = max{f[w1][w2], f[w1-w1[i]][w2-w2[i]] + v[i]},在递推的时候同样w1,w2从后向前。
六、分组的背包问题
有N件物品和一个容量为V的背包。第i件物品的费用是Ci,价值是Wi。这些物品被划分为K组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
分析
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设F [k, v]表示前k组物品花费费用v能取得的最大权值,则有:
for k = 1 to K //组 for w = W to 0 //承重 for item i in group k //组内的每个物品 f[w] = max{f[w], f[w - Ci] + Wi}
七、背包问题的相关问题
7.1 输出选取物品方案
如果不仅仅需要求出背包所承载物品的最大价值,还需要求出背包中选取的物品集合。则还需要维护另一个数组g[i][w], 若 f[i][w] == f[i-1][w]表示第i件物品没有被选择,则令g[i][w] = 0;若 f[i][w] == f[i-1[w - w[i]] + v[i],则说明选择了第i件物品,则令g[i][w] = 1,这样在求解f数组的时候顺便求出g数组。
int i = n, w = W; while(i >= 1){ if (g[i][w] == 0){ 未选择第i件物品 }else{ 选择了第i件物品 w = w - w[i] } i --; }
7.2 输出字典序最小的最优方案
字典序最小是指1...N号物品的选择方案排列出来以后字典序最小。一般而言,求一个字典序最小的最优方案,只需要在转移时注意策略。子问题的定义要修改一下:如果存在一个选了物品1的最优方案,那么答案一定包含物品1,原问题转化为一个背包容量为W-C1,物品为2....N的子问题。反之,如果答案不包括物品1,则转化成背包容量仍为W,物品为2....N的子问题。
令 f[i][w] 表示从物品i,i+1...N中选择若干物品总重量不超过w,所能得到价值的最大值。则有递推公式:f[i-1][w] = max{f[i][w], f[i][w-w[i-1]]}
,若f[i][w] == f[i][w-w[i-1]],则选择第i件物品。这样在递推完成之后,编号越小的物品就被记录下来,而同样优秀的字典序较大的方案被“覆盖”。
7.3 最优方案总数
g[0][0] = 1 for(int i = 1; i <= N; i ++){ for(int w = 0; w <= W; w ++){ f[i][w] = max{f[i-1][w], f[i-1][w-w[i]]}; g[i][w] = 0; if(f[i][w] == f[i-1][w]){ g[i][w] += g[i-1][w]; } if(f[i][w] == f[i-1][w-w[i]] + v[i]){ //可能选择不选择第i种物品得到结果相同,这样方案总数要相加 g[i][w] += g[i-1][w-w[i]]; } } }
7.4 求第k优解
对于求次优解、第K优解类的问题,如果相应的最优解问题能写出状态转移方程、用动态规划解决,那么求次优解往往可以相同的复杂度解决,第K优解则比求最优解的复杂度上多一个系数K。
基本思想是,将每个状态都表示成有序队列,将状态转移方程中的max/min转化成有序队列的合并。
用f[i][w][k]表示前i种物品中选择总重量不超过w的物品,得到的价值和的第k大的值,则可以推出
f[i][w][k] = Kth_Of_{f[i-1][w][1...k] + f[i-1][w-w[i]][1...k] + v[i]}
在两个大小为k的有序数组中,选择两个数组所有数字共同的第k大的数字。使用归并排序即可。
for(int i = 1; i <= n; i ++){ for(int w = w[i]; w <= W; w ++){ int t = 0, n1 = 0, n2 = 0; while(t < k){ if(f[i-1][w][n1] > f[i-1][w-w[i]][n2] + v[i]){ f[i][w][k] = f[i-1][w][n1++]; }else{ k_th_q = 2; f[i][w][k] = f[i-1][w - w[i]][n2 ++] + v[i]; } t ++; } } } //时间复杂度为O(nWk);
参考
《背包九讲》