01背包
问题:有n个重量和价值分别为wi和vi的物品,从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。
使用dfs搜索时依次对第i种物品都考虑选择或不选择,如果选择那么剩余容量为m-wi,然后在剩余的物品中继续搜索;如果不选择那么剩余容量不变,继续在后面的物品中搜索。使用动态规划的方法,则把每次开始搜索时容量和当前需要判断的物品看做一个状态,得到的转移方程为
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
w
i
]
+
v
i
)
dp[i][j]=max(dp[i-1][j],dp[i-1][j-wi]+vi)
dp[i][j]=max(dp[i−1][j],dp[i−1][j−wi]+vi)
i表示当前判断的物品,j表示剩余容量,通过观察发现转移方程中只会出现i和i+1两行,所以可以进一步优化空间复杂度。
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
w
i
]
+
v
i
)
dp[j]=max(dp[j],dp[j-wi]+vi)
dp[j]=max(dp[j],dp[j−wi]+vi)
根据转移方程得到如下代码:
for (int i = 0; i < n; i++) {
for (int j = m; j >= 0; j--) {
if (j >= w[i])
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
out.println(dp[m]);
完全背包
问题:有n个重量和价值分别为wi和vi的物品,每种物品数量无限多,从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。
与01背包的不同处在于每种物品可以无限多,因此选择一个物品之后还可继续从当前物品开始选择,转移方程为:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
w
[
i
]
]
+
v
i
)
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+vi)
dp[i][j]=max(dp[i−1][j],dp[i][j−w[i]]+vi)
空间优化后的转移方程
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
w
i
]
+
v
i
)
dp[j]=max(dp[j],dp[j-wi]+vi)
dp[j]=max(dp[j],dp[j−wi]+vi)
根据转移方程得到如下代码:
for (int i = 0; i < n; i++) {
for (int j = w[i]; j <= m; j++) {
dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
}
}
out.println(dp[m]);
注意完全背包中j的循环方向发生变化。
多重背包
问题:有n个重量和价值分别为wi和vi的物品,每种物品数量为si,从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。
使用朴素解法所以转移方程为:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
k
∗
w
i
]
+
k
∗
v
[
i
]
)
dp[i][j]=max(dp[i-1][j-k*wi]+k*v[i])
dp[i][j]=max(dp[i−1][j−k∗wi]+k∗v[i])
其中k表示当前物品选择次数,空间优化的转移方程:
d
p
[
j
]
=
m
a
x
(
d
p
[
j
−
k
∗
w
i
]
+
k
∗
v
[
i
]
)
dp[j]=max(dp[j-k*wi]+k*v[i])
dp[j]=max(dp[j−k∗wi]+k∗v[i])
根据转移方程得到如下代码:
for (int i = 0; i < n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 0; k <= s[k]; k++) {
if (k * w[i] > j) {
continue;
}
dp[j] = Math.max(dp[j], dp[j - k * w[i]] + k * v[i]);
}
}
}
out.println(dp[m]);
单调队列优化
单调队列优化的目标是优化第3层循环,第三层中是计算某几项的最大值,而且这几项是有一定规律的,
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
w
i
]
+
v
i
,
d
p
[
j
−
2
∗
w
i
]
+
2
∗
v
i
,
⋯
,
d
p
[
j
−
k
∗
w
i
]
+
k
∗
v
i
)
,
0
<
=
k
<
=
j
/
w
i
dp[j]=max(dp[j],dp[j-wi]+vi,dp[j-2*wi]+2*vi,\cdots,dp[j-k*wi]+k*vi), 0<=k<=j/wi
dp[j]=max(dp[j],dp[j−wi]+vi,dp[j−2∗wi]+2∗vi,⋯,dp[j−k∗wi]+k∗vi),0<=k<=j/wi
d p [ j ] = m a x ( d p [ j − ( k − 1 ) ∗ w i ] + k ∗ v i ) = m a x ( d p [ j ′ ] + ( j − j ′ ) w i ∗ v i ) dp[j]=max(dp[j-(k-1)*wi]+k*vi)=max(dp[j']+\frac{(j-j')}{wi} * vi) dp[j]=max(dp[j−(k−1)∗wi]+k∗vi)=max(dp[j′]+wi(j−j′)∗vi)
上面的公式看起来不太直观,公式中参与比较的索引都同余,将同余的索引用
a
j
a_j
aj表示,
d
p
[
a
j
]
=
m
a
x
(
d
p
[
a
j
′
]
+
(
j
−
j
′
)
∗
v
i
)
=
m
a
x
(
d
p
[
a
j
′
]
−
j
′
∗
v
i
)
+
j
∗
v
i
,
s
i
+
j
′
<
=
j
dp[a_{j}]=max(dp[a_{j'}]+(j-j')*vi)=max(dp[a_{j'}]-j'*vi)+j*vi,si+j'<=j
dp[aj]=max(dp[aj′]+(j−j′)∗vi)=max(dp[aj′]−j′∗vi)+j∗vi,si+j′<=j
Deque<Integer> queVal = new LinkedList<>();
Deque<Integer> queIndex = new LinkedList<>();
for (int i = 0; i < n; i++) {
for (int a = 0; a < w[i]; a++) {
queVal.clear();
for (int j = 0; a + j * w[i] <= m; j++) {
int val = dp[a + j * w[i]] - j * v[i];
while (!queVal.isEmpty() && queVal.peekLast() <= val) {
queVal.removeLast();
queIndex.removeLast();
}
queVal.addLast(val);
queIndex.addLast(j);
dp[a + j * w[i]] = queVal.peekFirst() + j * v[i];
if (queIndex.peekFirst() + s[i] == j) {
queIndex.removeFirst();
}
}
}
}
out.println(dp[m]);
其他问题
- 有一些问题需要将背包刚好装满,这时需要设置异常值用来表示无法满包,例如在leetcode322找零问题中,将初始值设置1000000007表示无法凑成所给的总金额,。
int INF = 1000000007;
int[] dp = new int[amount+1];
Arrays.fill(dp, INF);
dp[0] = 0;
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] = Math.min(dp[j], dp[j-coins[i]] + 1);
}
}
return dp[amount] >= INF ? -1 : dp[amount];
leetcode518是计数问题,初始化为0表示无法满包,即方案数为0。
int[] dp = new int[amount + 1];
Arrays.fill(dp, 0);
dp[0] = 1;
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
- 负数背包,因为dp的索引存在负数,所以需要将索引整体右移offset,直到索引全部为正数,初始索引(零点)为offset。且遍历时需要根据容量的正负考虑不同的遍历方向。
待续
多重背包二进制优化,多重背包计数,多重背包完全装满等。