完全背包问题
问题描述
有N种物品和一个容量为V的背包,每种物品有无限件可用,第i种物品的体积是v[i],价值是w[i],求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
状态定义和状态转移方程
一种常用的状态定义是:dp[i][j]表示前i种物品放入容量为j的背包中能获得的最大价值。
那么对于第i种物品,有以下几种选择:
- 不放入任何该物品,那么dp[i][j] = dp[i-1][j]
- 放入1件该物品,那么dp[i][j] = dp[i-1][j-v[i]] + w[i]
- 放入2件该物品,那么dp[i][j] = dp[i-1][j-2v[i]] + 2w[i]
- …
- 放入k件该物品,那么dp[i][j] = dp[i-1][j-kv[i]] + kw[i]
其中k的取值范围是0到j/v[i],即不超过背包容量的最大数量。
因此,状态转移方程为:
dp[i][j]=0≤k≤j/v[i]max(dp[i−1][j−k∗v[i]]+k∗w[i])
代码实现
根据状态定义和状态转移方程,可以写出二维数组的动态规划代码如下:
int knapsack(int N, int V, int[] v, int[] w) {
// dp数组初始化为0
int[][] dp = new int[N+1][V+1];
// 遍历所有物品
for (int i = 1; i <= N; i++) {
// 遍历所有容量
for (int j = 1; j <= V; j++) {
// 遍历所有选择次数
for (int k = 0; k <= j / v[i]; k++) {
// 更新最大价值
dp[i][j] = max(dp[i][j], dp[i-1][j-k*v[i]] + k*w[i]);
}
}
}
// 返回最终结果
return dp[N][V];
}
这个代码的时间复杂度是O(NVS),其中S是所有物品数量之和。
优化方法
为了优化时间复杂度,可以对状态转移方程进行化简。观察上面的方程,可以发现当k=0时,dp[i][j] = dp[i-1][j];当k=1时,dp[i][j] = dp[i-1][j-v[i]] + w[i];当k=2时,dp[i][j] = dp[i-1][j-2v[i]] + 2w[i]…以此类推。
如果将k替换为k+1,并且将j替换为j-v[i],可以得到:
dp[i][j−v[i]]=0≤k≤(j−v[i])/v[i]max(dp[i−1][j−(k+1)∗v[i]]+(k+1)∗w[i])
将两个方程相减,并化简得到:
dp[i][j]−dp[i][j−v[i]]=0≤k≤j/v[i]max(dp[i−1][j−k∗v[i]]+k∗w[i])−0≤k≤(j−v[i])/v[i]max(dp[i−1][j−(k+1)∗v[i]]+(k+1)∗w[i])
注意到,当k取最大值j/v[i]时,第一个max中的项等于dp[i-1][j];当k取最大值(j-v[i])/v[i]时,第二个max中的项等于dp[i-1][j-v[i]]。因此,上式可以化为:
dp[i][j]−dp[i][j−v[i]]=dp[i−1][j]−dp[i−1][j−v[i]]
移项得到:
dp[i][j]=dp[i−1][j]+dp[i][j−v[i]]
这就是化简后的状态转移方程,它的含义是:对于第i种物品,可以选择不放入,那么价值等于前i-1种物品放入容量为j的背包的最大价值;也可以选择放入,那么价值等于前i种物品放入容量为j-v[i]的背包的最大价值加上当前物品的价值。
根据这个方程,可以写出一维数组的动态规划代码如下:
int knapsack(int N, int V, int[] v, int[] w) {
// dp数组初始化为0
int[] dp = new int[V+1];
// 遍历所有物品
for (int i = 1; i <= N; i++) {
// 遍历所有容量
for (int j = v[i]; j <= V; j++) {
// 更新最大价值
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
}
}
// 返回最终结果
return dp[V];
}
这个代码的时间复杂度是O(N*V),空间复杂度是O(V)。
参考链接:
【动态规划/背包问题】详解「完全背包」问题 & 三种背包问题之间的内在关系 - 知乎: