转载请注明原作者 @yoshino,强烈要求支持TOC目录和数学公式的输入!
更新不及时,有空慢慢写吧,原文是写在Bear上粘贴来的,可能格式有点乱。
具体的工程在我的github上。
一个正在抢劫商店的小偷发现了n个商品,第i个商品的价值vi元,重wi磅,小偷希望拿走价值尽量高的商品,但是他的背包只能容纳W磅商品,怎么选择才能拿走价值最大的商品?
根据算法导论书上原题和自定一些条件,把这题条件定义如下:
int[] n = new int[]{0, 3, 3, 3};//为了配合算法,下标都是从1开始,表示每件商品的数量
int[] w = new int[]{0, 10, 20, 30};//表示每件商品的重量
int[] v = new int[]{0, 60, 100, 120};//表示每件商品的价值
int W = 50;//背包的容积
背包问题分为两种:
- 0-1背包问题(0-1 knapsack problem):商品不可分,小偷只能完整拿走或者不拿走,或者同一个商品拿走多次
- 分数背包问题(fractional knapsack problem):商品可分,小偷可以拿走商品的部分
问题分析
分数背包问题
分数背包问题是典型的贪心算法,只要计算出商品的每磅价值vi/wi,贪心地选择每磅价值最高的商品即可
public void fracKnapsack(int[] n, int[] w, int[] v, int W) {
int[] value = new int[n.length];//计算每件商品的单磅价值大小
int[] result = new int[n.length];//存储取用结果
value[0] = 0;
result[0] = 0;
for (int i = 1; i < n.length; i++) {
value[i] = v[i] / w[i];//计算每磅价值
n[i] = n[i] * w[i];//计算每件商品一共有多少磅
result[i] = 0;
}
while (W > 0) {
int pos = getMax(value);
if (n[pos] > 0) {
while (n[pos] > 0) {
result[pos]++;
n[pos]--;
W--;
}
}
if (n[pos] == 0) {
value[pos] = 0;
continue;
}
}
int sum = 0;
System.out.println("拿走物品结果:");
for (int i = 1; i < result.length; i++) {
System.out.println("物品编号:" + i + " 数量:" + result[i] / w[i] + " 单价:" + v[i]);
sum = sum + (result[i] * v[i] / w[i]);
}
System.out.println("总价值为: " + sum);
}
private int getMax(int[] value) {//找出单磅价值最大的商品编号
int pos = 0;
int max = 0;
for (int i = 1; i < value.length; i++) {
if (value[i] != 0) {
if (max < value[i]) {
max = value[i];
pos = i;
}
}
}
return pos;
}
0-1背包问题
0-1背包问题不能用贪心算法来解决,原因很简单,使用贪心算法会留下空间,无法完全填满背包,浪费的空间会造成单磅价值的下降,所以必须采用动态规划。
0-1背包问题是动态规划中最基础的问题,后面还会有各种背包问题,这仅仅是0-1背包问题的变种.
我们假设f(i,W)是,有i个商品,背包容积为W时的最优解,对于一个新的商品ai,有两种不同的情况:
- ai的重量wi大于背包容积,则无法将其放入背包,商品的数量变为i+1
- ai的重量wi小于等于W,则将其放入背包,剩下的背包容积为W-wi,或者不放,取两者之间较大的值。
状态转移方程可以写成:
f[i][W]=max{f[i-1][W],f[i-1][W-w[i]]+v[i]}
最优子结构可以表示成下面公式
\\对应的latex公式代码,不支持latex真麻烦
$$
f(i,W)=
\begin{cases}
f(i-1,W)&&,w_i>W\\
max[f(i-1,W),&f(i-1,W-w_i)+v_i]&,w_i\le W
\end{cases}
$$
public void zeroOneKnapsack(int[] n, int[] w, int[] v, int W) {//带备忘的自顶向下法
int[][] result = new int[n.length][W + 1];//建立最优解矩阵,result[有前i种商品][背包容量W]
for (int i = 0; i <= W; i++) {
result[0][i] = 0;//初始化
}
for (int i = 1; i < n.length; i++) {//商品i种,从1开始
for (int j = 0; j <= W; j++) {//容量大小,从0到W
if (w[i] > j) {//无法放入背包,根据最优子结构可知
result[i][j] = result[i - 1][j];
} else if (w[i] <= j) {//放入背包中
int maxProfit = 0;
maxProfit = Math.max(result[i - 1][j], result[i - 1][j - w[i]] + v[i]);//选择放还是不放
result[i][j] = maxProfit;//存储最优解
}
}
}
System.out.println(result[n.length - 1][W]);
}
其它背包问题
完全背包问题
相比于0-1背包问题,每件物品的个数已经变成无限个。
求解思路
- 我们可以转化为0-1背包问题来求解,将k件一种物品拆成k种价值一样的物品,这样就可以采用上述代码写了。但是这样写增大了空间复杂度,也没有改进时间复杂度。
- 还有一种方法是转化成二进制思想,不太清楚如何写的,其实思想也是分成0-1背包问题来解决的
多重背包问题
介于完全背包问题和0-1背包问题之间,第i种物品有n[i]件,其余条件不变
求解思路
- 同样的,可以拆解成0-1背包问题,这样的话就大大地增加空间复杂度了
结语
通过对背包问题的学习,基本上困难的算法问题到最后都是可以求解成最简单的0-1背包问题,基础还是很重要。