一、01背包问题
1.1 题目
有 N N N件物品和一个容量为 V V V的背包,放入第 i i i件物品耗费的费用是 C i C_i Ci,得到的价值是 W i W_i Wi。求解将哪些物品装入背包可使得价值总和最大。
1.2 基本思路
01背包是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或者不放。
用子问题定义状态:即
F
[
i
,
v
]
F[i,v]
F[i,v] 表示前
i
i
i 件物品恰放入一个容量为
v
v
v 的背包可以获得
的最大价值。则其状态转移方程便是:
F
[
i
,
v
]
=
m
a
x
{
F
[
i
−
1
,
v
]
,
f
[
i
−
1
,
v
−
C
i
]
+
W
i
}
F[i,v]=max\{F[i-1,v],f[i-1,v-C_i]+W_i\}
F[i,v]=max{F[i−1,v],f[i−1,v−Ci]+Wi}
对于第
i
i
i件物品来说,它只有放入背包和不放入背包两种选择。其中,
m
a
x
{
}
max\{\}
max{}中的两个元素表示的含义分别为:
- F [ i − 1 , v ] F[i-1,v] F[i−1,v]:第 i i i件物品不放入背包
- f [ i − 1 , v − C i ] + W i f[i-1,v-C_i]+W_i f[i−1,v−Ci]+Wi:第 i i i件物品放入背包
如果对这个状态方程还不是很理解,那我们可以举个例子画图说明。假设背包容量为10,物品数量为4,每种物品的花费与价值如下:
花费/重量 | 价值 |
---|---|
2 | 1 |
3 | 3 |
4 | 5 |
7 | 9 |
这里用
w
[
i
]
w[i]
w[i]来表示第
i
i
i 件物品的重量,
v
[
i
]
v[i]
v[i]表示第
i
i
i 件物品的价值。
那么我们首先建立一个二维的dp数组用来保存状态信息,然后依次枚举所有状态数据:
注意我涂了颜色的格子:
- 红色格子:此时背包容量 j = 2 j=2 j=2,此时只有1号物品可以选择,该物品重量为2,价值为1,刚好可以放入该容量为2的背包,因此 d p [ 1 ] [ 2 ] = 1 dp[1][2]=1 dp[1][2]=1。这一行后面的数据自然也为1,因为背包容量一直在增加,而可选择的物品还是只有一个。
- 橙色格子:此时背包容量为3,前两件物品中,只有将2号物品放入背包时价值最大,故 d p [ 2 ] [ 3 ] = 3 dp[2][3]=3 dp[2][3]=3。
- 绿色格子:此时背包容量变为5,刚好可以容纳前两件物品,此时可以将前两件物品同时放入背包,获得最大价值4,即 d p [ 2 ] [ 5 ] = 4 dp[2][5]=4 dp[2][5]=4。
根据以上分析,我们可以得到以下结论:
有了状态方程,接下来我们用代码实现一下:
public class ZeroOnePack {
public static int method(int V, int N, int[] weight, int[] value){
//这里的dp数组就是对应于上面定义的F
int[][] dp = new int[N + 1][V + 1];
//这里索引从1开始取,即dp[1][v]表示的是将第1个物品放入容量为v的背包的最大价值
for (int i = 1; i < N + 1; i++){
for (int j = 1; j < V + 1; j++){
//如果第i件物品的花费大于背包容量j,则不装入背包
//由于weight和value数组下标都是从0开始,故第i个物品的花费为weight[i-1],价值为value[i-1]
if (weight[i - 1] > j){
dp[i][j] = dp[i - 1][j];
}else{
//上文提出的状态转移方程
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
}
}
}
return dp[N][V];
}
}
我们以华为的笔试题作为例子来测试一下结果,题目如下:
小朋友考试得到第一名可以得到奖励零食,现有价格
A
,
B
,
C
,
D
,
E
.
.
.
A,B,C,D,E...
A,B,C,D,E...元商品各
A
1
,
B
1
,
C
1
,
D
1
,
E
1...
A1,B1,C1,D1,E1...
A1,B1,C1,D1,E1...个,小朋友的喜爱度依次为
A
2
,
B
2
,
C
2
,
D
2
,
E
2
,
.
.
.
.
.
A2,B2,C2,D2,E2,.....
A2,B2,C2,D2,E2,.....请返回选取
X
X
X元零食可达到的最大喜爱度。
第一行输入为
X
X
X和
N
N
N,
X
X
X为可使用 钱的总额,
N
N
N为零食种类。
第二行开始为零食属性,每行为3个整型数,分别代表零食价格,数量以及喜爱度。
输入:
钱总额 | 零食种类 |
---|---|
6 | 7 |
价格 | 数量 | 喜爱度 |
---|---|---|
3 | 1 | 8 |
4 | 1 | 2 |
3 | 1 | 1 |
9 | 1 | 7 |
4 | 1 | 1 |
4 | 1 | 8 |
4 | 1 | 4 |
输出:9
解释:6元可以分别选取价格3元喜爱度为8的商品1件以及3元喜爱度为1的商品一件,喜爱度总和为9。
对于这个问题,我们暂时不考虑数量的限制,去掉数量限制后,这题就是一个01背包问题。那么我们直接调用上面写的方法即可解决该问题,代码如下:
public class Main {
public static void main(String[] args) {
int[] weight = {3,4,3,9,4,4,4};
int[] value = {8,2,1,7,1,8,4};
int V = 6;
int N = 7;
System.out.println(ZeroOnePack.method(V, N, weight, value));
}
}
输出:9
以上实现01背包问题的算法其实还可以优化空间复杂度。请注意我们在推导dp数组中每一行的值时,其实只用到了dp数组中上一行的值,那么我们没有必要将所有的状态值都保存起来,我们只需用一个一维数组保存上一行的状态值即可。如下图:
我们用一维数组
d
p
[
j
]
dp[j]
dp[j]来表示背包容量为
j
j
j时的最大价值。此时
上述代码变为:
从状态转移方程中可以看出,
d
p
[
j
]
dp[j]
dp[j]是由
d
p
[
j
−
w
[
i
]
]
dp[j-w[i]]
dp[j−w[i]]推导出来的,准确来说,这里的
d
p
[
j
]
dp[j]
dp[j]对应于二维数组中第
i
i
i行第
j
j
j列的数据,而
d
p
[
j
−
w
[
i
]
]
dp[j-w[i]]
dp[j−w[i]]对应于第
i
−
1
i-1
i−1行第
j
−
w
[
i
]
j-w[i]
j−w[i]的数据。而一维数组
d
p
[
j
]
dp[j]
dp[j]在推导第
i
i
i行状态数据之前的初始值就是第
i
−
1
i-1
i−1行的数据,因此我们在写代码时,遍历一维数组
d
p
[
j
]
dp[j]
dp[j]时,必须要从后往前遍历。
// 01背包的优化
public static int method2(int V,int N,int[] weight,int[] value){
int[] dp = new int[V+1];
for(int i=1;i<N+1;i++){
//遍历dp[]数组时,从后往前遍历,并顺便过滤掉容量不足的情况
for(int j=V;j>=weight[i-1];j--){
dp[j] = Math.max(dp[j-weight[i-1]]+value[i-1],dp[j]);
}
}
return dp[V];
}
参考文献:
[1] https://github.com/tianyicui/pack