背包问题
背包问题是一类经典的动态规划问题,本文先对背包问题的几种常见类型作一个总结,期望可以用一套框架解决背包问题。
这里先简单说说动态规划。动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所
有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到了。
最优性原理是动态规划的基础,最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。
常见背包问题可分为:
- 01背包问题:有N件物品和一个容量为V的背包,第i件物品消耗的容量为Wi,价值为Vi,求解放入哪些物品可以使得背包中总价值最大。特点:每个物品只有一件供你选择放还是不放。
- 完全背包问题:有N种物品和一个容量为V的背包,每种物品都有无限件可用,第i件物品消耗的容量为Wi,价值为Vi,求解放入哪些物品可以使得背包中总价值最大。特点:每个物品可以无限选用。
- 多重背包问题:有N种物品和一个容量为V的背包,第i种物品最多有Mi件可用,每件物品消耗的容量为Wi,价值为Vi,求解入哪些物品可以使得背包中总价值最大。特点 :它与完全背包有类似,每个物品都有了一定的数量。
三种背包问题都有一个共同的限制,那就是背包容量,背包的容量是有限的,这便限制了物品的选择,而三种背包问题的共同目的,就是让背包中的物品价值最大。
不同的地方在于物品数量的限制,01背包问题中,每种物品只有一个,对于每种物品而言,便只有选和不选两个选择。完全背包问题中,每种物品有无限多个,所以可选的范围要大很多。在多重背包问题中,每种物品都有各自的数量限制。
三种背包问题虽然对于物品数量的限制不一样,但都可以转化为01背包问题来进行思考。
在完全背包问题中,虽然每种物品都可以选择无限个,但由于背包容量有限,实际上每种物品可以选择的数量也是有限的,那么将每种物品都看做是 V/Wi 种只有一件的不同物品,就转换成了01背包问题。对于多重背包也是如此,只是每种物品的膨胀数量变成了min{Mi, V/Wi}。
所以说,01背包问题是所有背包问题的基础,弄懂了01背包问题后,完全背包和多重背包问题也可以迎刃而解。
下面给出三种背包问题的状态转移方程,便于更好的理解它们之间的联系:
首先给出一些基本概念:有N件物品和一个容量为V的背包。第i件物品的容量是w[i],价值是v[i],求将哪些物品装入背包可使价值总和最大。
- 01背包的状态转移方程:
二维方程:
f[i][j] = max{f[i-1][j], f[i-1][j-w[i]]+v[i]}
// 伪代码
for(int i=1; i<=n; i++){
for(int j=m; j>0; j--){
if(a[i]<=j)
f[i][j]=max(f[i-1][j],f[i-1][j-a[i]]+b[i]);
else
f[i][j]=f[i-1][j];
}
}
一维方程:
设f[j]表示重量不超过j公斤的最大价值,状态转移方程为:
f[j] = max{f[j], f[j−a[i]]+b[i]}
// 伪代码
for(int i=1; i<=n; i++){
for(int j=V; j>=w[i]; j--){
f[j] = max(f[j], f[j-w[i]]+v[i]);
}
}
- 完全背包的状态转移方程:
设f[j]表示重量不超过j公斤的最大价值,状态转移方程为:
f[j] = maxj{f[j], f[j−a[i]]+b[i]}
// 伪代码:
for(int i=1; i<=n; i++){
for(int j=a[i]; j<=m; j++){
f[j] = max(f[j], f[j-a[i]]+b[i]);
}
}
- 多重背包的状态转移方程:
设f[j]表示重量不超过j公斤的最大价值,状态转移方程为:
f[j] = max{f[j], f[j−k∗a[i]]+k∗b[i]}
// 伪代码
for(int i=1; i<= n; i++){
for(int j=m;j>=a[i]; j--){
for(int k=0; k<=c[i]; k++){
if(j-k*a[i]<0)
break;
f[j] = max(f[j], f[j-k*a[i]]+k*b[i]);
}
}
}
1. 01背包问题
01背包问题是最基本的背包问题:有N件物品和一个容量为V的背包,第i件物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限V的情况下,求解放入哪些物品可以使得背包中的总价值最大。
01背包问题的特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][j]表示前i件物品恰放入一个容量为j的背包可以获得的最大价值。则其状态转移方程便是:
f[i][j] = max{f[i-1][j], f[i-1][j-w[i]]+v[i]}
上面的方程是背包问题最基本的方程,这里对它进行一个简单的解释:
“将前 i 件物品放入容量为 j 的背包中”这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为一个只牵扯前 i−1 件物品的问题。如果不放第 i 件物品,那么问题就转化为“前 i−1 件物品放入容量为 j 的背包中”,价值为 f[i−1][j];如果放第 i 件物品,那么问题就转化为“前 i−1 件物品放入剩下的容量为 j-w[i] 的背包中”,此时能获得的最大价值就是 f[i−1][j−w[i]] 再加上通过放入第i件物品获得的价值v[i]。
以上方法的时间和空间复杂度均为O(V*N),其中时间复杂度已经不能再优化了,但使用一位数组代替二维数组时,空间复杂度可以优化到O(N)。
二维方程:
f[i][j] = max{f[i-1][j], f[i-1][j-w[i]]+v[i]}
// 伪代码
for(int i=1; i<=n; i++){
for(int j=m; j>0; j--){
if(a[i]<=j)
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);
else
f[i][j]=f[i-1][j];
}
}
一维方程:
f[j] = max{f[j], f[j−w[i]]+v[i]}
// 伪代码
for(int i=1; i<=n; i++){
for(int j=V; j>=w[i]; j--){
f[j] = max(f[j], f[j-w[i]]+v[i]);
}
}
2. 完全背包问题
完全背包与 01 背包不同之处在于每种物品可以有无限多个:有N种物品,每种物品有无限多个,第 i种物品的重量为w[i](i从1开始),价值为v[i]。在总重量不超过背包承载上限W的情况下,求解放入哪些物品可以使得背包中的总价值最大。
01背包问题与完全背包问题的主要区别就是物品是否可以重复选取。
3. 多重背包
有N种物品和一个容量为V的背包,第i种物品最多有Mi件可用,每件物品消耗的容量为Ci,价值为Wi,求解入哪些物品可以使得背包中总价值最大。
特点 :它与完全背包有类似点 特点是每个物品都有了一定的数量。
背包问题具备的特征:
是否可以根据一个 target(直接给出或间接求出),target 可以是数字也可以是字符串,再给定一个数组 arrs,问:能否使用 arrs 中的元素做各种排列组合得到 target。
4. 背包问题的解法
01 背包问题:
如果是 01 背包,即数组中的元素不可重复使用,外循环遍历 arrs,内循环遍历 target,且内循环倒序:
完全背包问题:
(1)如果是完全背包,即数组中的元素可重复使用并且不考虑元素之间顺序,arrs 放在外循环(保证 arrs 按顺序),target在内循环。且内循环正序。
(2)如果组合问题需考虑元素之间的顺序,需将 target 放在外循环,将 arrs 放在内循环,且内循环正序。