背包问题:
让我们来计算一下每件物品的性价比,其结果如下:
毫无疑问,此时性价比最高的是物品4,我们把物品4放入背包当中,背包剩余的容量是8:
我们选择物品1放入背包,背包剩余的容量是4:
于是,我们选择0.8份的物品5放入背包,背包剩余的容量为0:
代码实现
public static void main(String[] args) {
int capacity = 10;
int[] weights = {4,6,3,2,5};
int[] values = {9,3,1,6,5};
System.out.println("背包最大价值:" + getHighestValue(capacity, weights, values));
}
public static double getHighestValue(int capacity, int[] weights,int[] values){
//创建物品列表并按照性价比倒序
List<Item> itemList = new ArrayList<>();
for(int i=0;i<weights.length;i++){
itemList.add(new Item(weights[i], values[i]));
}
itemList = itemList.stream().sorted(Comparator.comparing(Item::getRatio).reversed()).collect(Collectors.toList());
//背包剩余容量
int restCapacity = capacity;
//当前背包物品的最大价值
double highestValue = 0;
//按照性价比从高到低选择物品
for(Item item : itemList){
if(item.weight <= restCapacity){
highestValue += item.value;
restCapacity -= item.weight;
}else{
//背包装不下完整物品时,选择该件物品的一部分
highestValue += (double)restCapacity/(double)item.weight * item.value;
break;
}
}
return highestValue;
}
static class Item {
private int weight;
private int value;
//物品的性价比
private double ratio;
public Item (int weight, int value){
this.weight = weight;
this.value = value;
this.ratio = (double)value / (double)weight;
}
public double getRatio() {
return ratio;
}
}
01背包问题:
仍然给定一个容量是10的背包,有如下三个物品可供选择:
这一次我们有个条件限制:只允许选择整个物品,不能选择物品的一部分。
如果按照贪心算法的思路,首先选择的是性价比最高的物品1,那么背包剩余容量是4,再也装不下其他物品,而此时的总价值是6:
但这样的选择,真的能让总价值最大化吗?如果我们不选择物品1,选择物品2和物品3的话,剩余容量是0,总价值是7:
显然,7>6,依靠贪心算法得出的结果,未必是全局最优解。
使用动态规划代码实现如下:
public class BeiBao01 {
public int maxValue(int[] weight, int[] value, int W) {
//这里假定传入的weight和values数组长度总是一致的
int n = weight.length;
if (n == 0) return 0;
int[][] dp = new int[n + 1][W + 1];
for (int i = 1; i <= n; i++) {
for (int k = 1; k <= W; k++) {
// 存放 i 号物品(前提是放得下这件物品)
int valueWith_i = (k-weight[i-1] >= 0) ? (value[i-1]+dp[i-1][k-weight[i-1]]) : 0;
// 不存放 i 号物品
int valueWithout_i = dp[i - 1][k];
dp[i][k] = Math.max(valueWith_i, valueWithout_i);
}
}
return dp[n][W];
}
public static void main(String[] args) {
BeiBao01 obj = new BeiBao01();
int[] w = {1, 4, 3};
int[] v = {15, 30, 20};
int W = 4;
System.out.println(obj.maxValue(w, v, W));
}
}
下面实现的版本稍有不同:
public int maxValue(int[] weight, int[] value, int W) {
int n = weight.length;
if (n == 0) return 0;
int[][] dp = new int[n][W + 1];
// 先初始化第 0 行,也就是尝试把 0 号物品放入容量为 k 的背包中
for (int k = 1; k <= W; k++) {
if (k >= weight[0]) dp[0][k] = value[0];
else dp[0][k] = 0; // 这一步其实没必要写,因为dp[][]数组默认就是0
}
for (int i = 1; i < n; i++) {
for (int k = 1; k <= W; k++) {
// 存放 i 号物品(前提是放得下这件物品)
int valueWith_i = (k-weight[i] >= 0) ? (value[i] + dp[i-1][k-weight[i]]) : 0;
// 不存放 i 号物品
int valueWithout_i = dp[i-1][k];
dp[i][k] = Math.max(valueWith_i, valueWithout_i);
}
}
return dp[n-1][W];
}
(个人更喜欢第二种实现方式,感觉理解起来更友好)
时间复杂度:O(nW);空间复杂度:O(nW)
动态规划+压缩空间
观察上面的代码,会发现,当更新dp[i][…]时,只与dp[i-1][…]有关,也就是说,我们没有必要使用O(n*W)的空间,而是只使用O(W)的空间即可。下面先给出代码,再结合图例进行说明。
public int maxValue(int[] weight, int[] value, int W) {
int n = weight.length;
if (n == 0) return 0;
// 辅助空间只需要O(W)即可
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
// 注意这里必须从后向前!!!
for (int k = W; k >= 1; k--) {
int valueWith_i = (k - weight[i] >= 0) ? (dp[k - weight[i]] + value[i]) : 0;
int valueWithout_i = dp[k];
dp[k] = Math.max(valueWith_i, valueWithout_i);
}
}
return dp[W];
}
这里的状态转移方程变成了:dp[k](新值) = max(value[i]+dp[k-weight[i]](旧值), dp[k](旧值))
为什么说这里必须反向遍历来更新dp[]数组的值呢?
原因是索引较小的元素可能会被覆盖。我们来看例子,假设我们已经遍历完了第 i=1 个元素(即weight=3,value=30),如下图所示:
现在要更新第 i=2 个元素(即weight=1, value=20),由于我们只申请了一维空间的数组,因此对dp[]数组的修改会覆盖上一轮dp[]数组的值,这里用浅色代表上一轮的值,深色代表当前这一轮的值。
鉴于上面出现的问题,因此必须采用反向遍历来回避这个问题。仍然假设第 i=1 个元素已经更新完毕,现在更新第 i=2 个元素。示意图如下:
可以看到,反向遍历就可以避免这个问题了!
事实上,我们还可以进一步简化上面的代码,如下:
public int maxValue(int[] weight, int[] value, int W) {
int n = weight.length;
if (n == 0) return 0;
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
//只要确保 k>=weight[i] 即可,而不是 k>=1,从而减少遍历的次数
for (int k = W; k >= weight[i]; k--) {
dp[k] = Math.max(dp[k - weight[i]] + value[i], dp[k]);
}
}
return dp[W];
}
为什么可以这样简化呢?我们重新看一下这段代码:
for (int k = W; k >= 1; k--) {
int valueWith_i = (k - weight[i] >= 0) ? (dp[k - weight[i]] + value[i]) : 0;
int valueWithout_i = dp[k];
dp[k] = Math.max(valueWith_i, valueWithout_i);
}
如果k>=weight[i] 不成立,则valueWith_i 的值为0,那么显然有:dp[k] = Math.max(valueWith_i, valueWithout_i) = max(0, dp[k]) = dp[k]
也就是dp[k]没有更新过,它的值还是上一轮的值,因此就没必要执行了,可以提前退出循环!