Version: Next
0 - 1 背包问题
贪心法
有
n
件物品和一个最大承重为W
的背包,每件物品的重量是W_i
,价值是V_i
- 问:在保证总重量不超过
W
的前提下,将哪几件物品装入背包,可以是背包的总价值最大?- 每种物品只有
1
件,即每种物品只能选择0
件或者1
件,故称为0-1 背包问题
如果采用贪心算法,有
3
个方案
- 价值主导:优先选择最值钱的放进背包
- 重量主导:优先选择重量最轻的物品放进背包
- 价值密度主导:优先选择
density = value / weight
最高的物品放入背包
- 假设背包最大承重为150,现有7个物品如表格所示
编号 | 重量 | 价值 | 价值密度 |
---|---|---|---|
1 | 35 | 10 | 0.29 |
2 | 30 | 40 | 1.33 |
3 | 60 | 30 | 0.5 |
4 | 50 | 50 | 1.0 |
5 | 40 | 35 | 0.88 |
6 | 10 | 40 | 4.0 |
7 | 25 | 30 | 1.2 |
- 价值主导法:依次放入
4、2、6、5
,总重量130
,总价值165
- 重量主导法:依次放入
6、7、2、1、5
,总重量140
,总价值155
- 价值密度主导法:依次放入
6、2、7、4、1
,总重量为150
,总价值170
背包物品实体类
package com.bsx.algorithm.pojo;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 物品
*/
@Data
@NoArgsConstructor
public class Item {
private int weight;
private int value;
private double valueDensity;
public Item(int weight, int value) {
this.weight = weight;
this.value = value;
this.valueDensity = value * 1.0 / weight;
}
@Override
public String toString() {
return "Item{" +
"重量=" + weight +
", 价值=" + value +
", 价值密度=" + valueDensity +
'}';
}
}
贪心法解01背包问题
/**
* 01背包问题,贪心法 ,接收不同的比较器来实现不同的选取规则
* 价值主导
* 重量主导
* 价值密度主导
*/
public class ZeroOneBagGreedy {
public static void main(String[] args) {
Item[] items = new Item[]{
new Item(35, 10),
new Item(30, 40),
new Item(60, 30),
new Item(50, 50),
new Item(40, 35),
new Item(10, 40),
new Item(25, 30)
};
// 按价格,从高到低排序
Comparator<Item> valueComparator = (i1, i2) -> i2.getValue() - i1.getValue();
// 按重量,从小到大排序
Comparator<Item> weightComparator = (i1, i2) -> i1.getWeight() - i2.getWeight();
// 按价格密度,从高到低排序
Comparator<Item> valueDensityComparator = (i1, i2) -> Double.compare(i2.getValueDensity(), i1.getValueDensity());
System.out.println("价值主导");
solve(items, 150, valueComparator);
System.out.println("重量主导");
solve(items, 150, weightComparator);
System.out.println("价值密度主导");
solve(items, 150, valueDensityComparator);
}
/**
* @param items 物品数组
* @param maxWeight 背包的最大承重
* @param comparator 外部传入的比较规则,决定价值主导、重量主导、价值密度主导
*/
private static void solve(Item[] items, int maxWeight, Comparator<Item> comparator) {
System.out.println("背包默认承重量为 [" + maxWeight + "]");
int remainWeight = maxWeight; // 剩余背包承重
int resultValue = 0; // 最终价值
// 按照 "规则" 排序
Arrays.sort(items, comparator);
for (Item item : items) {
int currentItemWeight = item.getWeight();
if (remainWeight >= currentItemWeight) {
remainWeight -= item.getWeight();
resultValue += item.getValue();
System.out.println("拿了重量为 [" + item.getWeight()
+ "], 价值 [" + item.getValue() + "] 的物品 | 当前共装了重 ["
+ (maxWeight - remainWeight) + "] 的物品,背包剩余承重为 ["
+ remainWeight + "],总价值为 [" + resultValue + "] ");
}
}
System.out.println("拿不下了 \n");
}
}
运行结果
价值主导
背包默认承重量为 [150]
拿了重量为 [50], 价值 [50] 的物品 | 当前共装了重 [50] 的物品,背包剩余承重为 [100],总价值为 [50]
拿了重量为 [30], 价值 [40] 的物品 | 当前共装了重 [80] 的物品,背包剩余承重为 [70],总价值为 [90]
拿了重量为 [10], 价值 [40] 的物品 | 当前共装了重 [90] 的物品,背包剩余承重为 [60],总价值为 [130]
拿了重量为 [40], 价值 [35] 的物品 | 当前共装了重 [130] 的物品,背包剩余承重为 [20],总价值为 [165]
拿不下了
重量主导
背包默认承重量为 [150]
拿了重量为 [10], 价值 [40] 的物品 | 当前共装了重 [10] 的物品,背包剩余承重为 [140],总价值为 [40]
拿了重量为 [25], 价值 [30] 的物品 | 当前共装了重 [35] 的物品,背包剩余承重为 [115],总价值为 [70]
拿了重量为 [30], 价值 [40] 的物品 | 当前共装了重 [65] 的物品,背包剩余承重为 [85],总价值为 [110]
拿了重量为 [35], 价值 [10] 的物品 | 当前共装了重 [100] 的物品,背包剩余承重为 [50],总价值为 [120]
拿了重量为 [40], 价值 [35] 的物品 | 当前共装了重 [140] 的物品,背包剩余承重为 [10],总价值为 [155]
拿不下了
价值密度主导
背包默认承重量为 [150]
拿了重量为 [10], 价值 [40] 的物品 | 当前共装了重 [10] 的物品,背包剩余承重为 [140],总价值为 [40]
拿了重量为 [30], 价值 [40] 的物品 | 当前共装了重 [40] 的物品,背包剩余承重为 [110],总价值为 [80]
拿了重量为 [25], 价值 [30] 的物品 | 当前共装了重 [65] 的物品,背包剩余承重为 [85],总价值为 [110]
拿了重量为 [50], 价值 [50] 的物品 | 当前共装了重 [115] 的物品,背包剩余承重为 [35],总价值为 [160]
拿了重量为 [35], 价值 [10] 的物品 | 当前共装了重 [150] 的物品,背包剩余承重为 [0],总价值为 [170]
拿不下了
动态规划法
- 定义状态:假设
values
是价值数组,weight
是重量数组
- 编号为
k
的物品,价值是values[k]
,重量是weights[k]
,k ∈ [0, n)
- 假设
dp(i, j)
是最大承重为j
, 有前i
件物品可选时的最大总价值,i ∈ [0, n], j ∈ [0, w]
dp(3, 7)
表示最大承重为7
,有前3
件物品可选时的最大价值- 所求解即为
dp(n, capacity)
- 初始条件
dp(i, 0) = dp(0, j) = 0
- 状态转移方程
- 如果最后一件物品不选,那么
dp(i, j) = dp(i-1, j)
- 如果最后一件物品被选了(第
i
件物品,当前它的下标是i - 1
),则最大承重减去它的重量,对应dp值加上它的价值,即dp(i, j) = dp(i-1, j-weight[i-1]) + values[i-1]
- 决策:决定到底拿了好还是不拿好: 在上述两种情况的
dp
值中,选二者较大的那个,即dp(i, j) = max { dp(i-1, j), dp(i-1, j-weight[i-1]) + values[i-1] }
- 当然,需要考虑剩余承重,
- 确保
j >= weights[i - 1]
才能拿,则dp(i, j) = max { dp(i-1, j), dp(i-1, j-weight[i - 1]) + values[i - 1] }
- 否则
j < weights[i - 1]
,表示不能再拿,那么dp(i, j) = dp(i-1, j)
/**
* 动态规划——01背包问题
*/
public class ZeroOneBagDp {
/**
* @param values 价值数组
* @param weights 重量数组
* @param capacity 背包容量
* @return
*/
private static int maxValue(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[][] dp = new int[values.length + 1][capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = 1; j <= capacity; j++) {
if (j < weights[i - 1]) // 表示不能再拿了
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = Math.max(
dp[i - 1][j],
dp[i - 1][j - weights[i - 1]] + values[i - 1]
);
}
}
return dp[values.length][capacity];
}
public static void main(String[] args) {
int[] values = {6, 3, 5, 4, 6};
int[] weights = {2, 2, 6, 5, 4};
int capacity = 10;
int res = maxValue(values, weights, capacity);
System.out.println("res = " + res);
}
}
滚动数组优化空间复杂度
- 观察上述 dp 数组中的 i,发现在状态转移方程中,只涉及
i
与i - 1
,根据无后效性,其实用一个数组就行了 - 那么整个
i
这个维度可以被移除,只剩下j
就行了,使用一个一维数组 - 由于 dp[j] 依赖于 dp[j - w[i]] ,j - w[i] < j,也就是在一个一维数组中,计算右侧的j需要用到左侧的 j 上的上一轮数据,那么从左往右递推就会令新的值覆盖旧的值,因此递推方向应当是 从右向左
- 写出滚动数组下的状态转移方程:
dp[j - weight[i]] + values[i - 1]
与dp[j]
二者的最大值- 需要限定
j >= w[i]
防止下标越界 - 递推方向为 i 从左往右, j 从右向左
- 需要限定
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}