1. 0/1背包介绍
有 n
个物品和一个容量为 m
的背包,第i件物品最多有 1
件,价值为 value[i]
, 重量为 weight[i]
,目标:使物品的价值最大,且重量总和不超过背包最大限制。
首先,介绍一下多重背包和完全背包的区别:
- 多重背包:每个物品限制个数。
- 完全背包:个数不受限制(喜欢多少拿多少),但是不能超过背包上限!
0/1背包是一种多重背包问题
假设:有三种物品:pineapple, apple,pen
item | number | value | weight |
---|---|---|---|
pineapple | 1 | 6 | 10 |
apple | 1 | 7 | 5 |
pen | 1 | 8 | 1 |
2. 解法
1) 递归
解决动态规划,一般可以从递归得到,这里先给出递归的代码:
public int dp(int i, int rest){
// base case
if (rest == 0 || i == 0){
return 0;
}
if (weight[i] > rest){
// i's weight larger than bag's rest weight, then not get item
return dp(i-1, rest - weight[i]);
}else
// left is “not get item”, right is "get item"
return Math.max(dp(i-1, rest),dp(i-1, rest -weight[i])+value[i]);
}
注意:weight 和 value 数组的下标均从 1 开始
观察上述递归方程,可以发现:
-
dp[i-1][rest]
:不取第i个物品 -
dp[i-1][rest-weight[i]] + value[i]
: 取第i个物品 -
可以发现状态转换方程可以这样写:
d p [ i ] [ r e s t ] = M a t h . m a x ( d p [ i − 1 ] [ r e s t ] , d p [ i − 1 ] [ r e s t − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][rest] = Math.max(dp[i-1][rest],\quad dp[i-1][rest-weight[i]] + value[i]) dp[i][rest]=Math.max(dp[i−1][rest],dp[i−1][rest−weight[i]]+value[i])
2) 二维数组的动规
dp[i][v]
表示前 i 个物品的最大价值
转换方程见 公式(1)
public int process2(){
int dp[][] = new int[n+1][m+1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (weight[i] > j){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]] + value[i]);
}
}
}
return dp[n][m];
}
3) 优化上述动规过程,转换为一维数组
由公式(1)可以看出,i
状态的数组可以由上一个 i-1
状态的数组得到。所以我们可以将 i-1 -> i
的转换用迭代的思想实现,从而只要保留 一维数组即可完成要求。
转换方程如下:
d
p
[
w
]
=
m
a
x
(
d
p
[
w
]
,
d
p
[
w
−
w
e
i
g
h
t
[
i
]
]
+
v
a
l
u
e
[
i
]
)
i
∈
[
1
,
n
]
,
w
∈
[
0
,
m
]
dp[w] = max(dp[w], dp[w-weight[i]] + value[i]) \\ \qquad\qquad i \in [1,n], w \in [0,m]
dp[w]=max(dp[w],dp[w−weight[i]]+value[i])i∈[1,n],w∈[0,m]
废话不多说,先上代码
public int process3(){
int dp[] = new int[m+1];
for (int i = 1; i<=n;i++){
for (int w = m; w>=0; w--){
if (w<weight[i]){
dp[w] = dp[w];
}else{
dp[w] = Math.max(dp[w],dp[w-weight[i]] + value[i]);
}
}
}
return dp[m];
}
看到代码可能会说,为什么内循环要从后往前?
先看一下如上的图片:
- 可以看出
dp[w]
和dp[w-weight[i]]
是由 i-1状态给出。 - 公式左边的
dp[w]
为 i 状态。
如果从前往后,则会造成 公式中表示的i 与 i-1 状态的混乱。
从而内循环需要从后往前,保证状态的一致性!
3. 测试用例
Input:
// 商品个数
int n = 5;
// 背包大小
int m = 8;
// weight and value
int []weight = new int[]{0,3,5,1,2,2};
int []value = new int[]{0,4,5,2,1,3};
Output:
10
next: 完全背包问题