动态规划之 二维费用的背包问题
1. 问题描述
二维费用的背包问题是指对于每件物品,具有两种不同的费用,选择这件物品必须同时付出这两种代价,对于每种代价都有一个可付出的最大值(背包容量),求选择物品可以得到最大的价值。
例如,有一个背包,它的容量为V,它的重量限制为U。有N件物品,第i件物品的体积为a[i],重量为b[i],价值为c[i]。在不超过背包容量和重量限制的情况下,如何选择物品使得背包内物品的总价值最大?
2. 算法
费用加了一维,只需状态也加一维即可
这个问题可以用动态规划来解决。我们可以定义一个状态数组dp[i][j][k],表示前i件物品放入容量为j、重量为k的背包中可以获得的最大价值。那么我们可以根据第i件物品是否放入背包来分析状态转移方程:
- 如果不放入第i件物品,那么dp[i][j][k] = dp[i-1][j][k],即前i-1件物品放入容量为j、重量为k的背包中可以获得的最大价值。
- 如果放入第i件物品,那么dp[i][j][k] = dp[i-1][j-a[i]][k-b[i]] + c[i],即前i-1件物品放入容量为j-a[i]、重量为k-b[i]的背包中可以获得的最大价值加上第i件物品的价值。但是这种情况需要满足j >= a[i] && k >= b[i],即背包能够容纳第i件物品。
综合上述两种情况,我们可以得到状态转移方程:
dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j-a[i]][k-b[i]] + c[i]) // j >= a[i] && k >= b[i]
初始状态为dp[0][0][0] = 0,表示没有任何物品放入空背包时的价值为0。
最终答案为dp[N][V][U],表示前N件物品放入容量为V、重量为U的背包中可以获得的最大价值。
如前述方法,
- 可以只使用二维的数组:当每件物品只可以取一次时变量v和u采用逆序的循环,
- 当物品有如完全背包问题时采用顺序的循环。
- 当物品有如多重背包问题时拆分物品。
这里就不再给出伪代码了,相信有了前面的基础,你能够自己实现出这个问题的程序。
3. 扩展
3.1 物品总个数的限制
“二维费用”的条件是以这样一种隐含的方式给出
有时,“二维费用”的条件是以这样一种隐含的方式给出的:
- 最多只能取M件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。
换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在f[0..V][0..M]范围内寻找答案。
3.2 复数域上的背包问题
另一种看待二维背包问题的思路是:
将它看待成复数域上的背包问题。也就是说,背包的容量以及每件物品的费用都是一个复数。
而常见的一维背包问题则是实数域上的背包问题。(注意:上面的话其实不严谨,因为事实上我们处理的都只是整数而已。)
所以说,一维背包的种种思想方法,往往可以应用于二位背包问题的求解中,因为只是数域扩大了而已。
作为这种思想的练习,
你可以尝试将P11中提到的“子集和问题”扩展到复数域(即二维),并试图用同样的复杂度解决。
4. 具体实现
4.1 使用三维数组顺序实现
import java.util.Scanner;
public class Solution {
// 定义一个方法,接收物品数量,背包容量,背包体积,物品重量,物品体积和物品价值的数组,返回最大价值
public static int knapsack(int n, int W, int V, int[] w, int[] v, int[] c) {
// 定义状态数组
int[][][] dp = new int[n + 1][W + 1][V + 1];
// 遍历所有物品
for (int i = 1; i <= n; i++) {
// 遍历所有容量和体积
for (int j = 0; j <= W; j++) {
for (int k = 0; k <= V; k++) {
// 状态转移方程
if (j < w[i] || k < v[i]) {
// 无法选择当前物品
dp[i][j][k] = dp[i - 1][j][k];
} else {
// 可以选择当前物品
dp[i][j][k] = Math.max(dp[i - 1][j][k], dp[i - 1][j - w[i]][k - v[i]] + c[i]);
}
}
}
}
// 返回最大价值
return dp[n][W][V];
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 输入物品数量,背包容量和背包体积
int n = sc.nextInt();
int W = sc.nextInt();
int V = sc.nextInt();
// 输入每个物品的重量,体积和价值
int[] w = new int[n + 1];
int[] v = new int[n + 1];
int[] c = new int[n + 1];
for (int i = 1; i <= n; i++) {
w[i] = sc.nextInt();
v[i] = sc.nextInt();
c[i] = sc.nextInt();
}
// 调用方法,输出最大价值
System.out.println(knapsack(n, W, V, w, v, c));
}
}
4.2 使用二维数组逆序实现
import java.util.Scanner;
public class Solution {
// 定义一个方法,接收物品数量,背包容量,背包体积,物品重量,物品体积和物品价值的数组,返回最大价值
public static int knapsack(int n, int W, int V, int[] w, int[] v, int[] c) {
// 定义状态数组
int[][] dp = new int[W + 1][V + 1];
// 遍历所有物品
for (int i = 1; i <= n; i++) {
// 遍历所有容量和体积,逆序更新
for (int j = W; j >= w[i]; j--) {
for (int k = V; k >= v[i]; k--) {
// 状态转移方程
dp[j][k] = Math.max(dp[j][k], dp[j - w[i]][k - v[i]] + c[i]);
}
}
}
// 返回最大价值
return dp[W][V];
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 输入物品数量,背包容量和背包体积
int n = sc.nextInt();
int W = sc.nextInt();
int V = sc.nextInt();
// 输入每个物品的重量,体积和价值
int[] w = new int[n + 1];
int[] v = new int[n + 1];
int[] c = new int[n + 1];
for (int i = 1; i <= n; i++) {
w[i] = sc.nextInt();
v[i] = sc.nextInt();
c[i] = sc.nextInt();
}
// 调用方法,输出最大价值
System.out.println(knapsack(n, W, V, w, v, c));
}
}
自有实现 带实例测试
public static void main(String[] args) {
int N = 5;
int V = 20;
int W = 20;
int[] w = new int[]{6, 3, 5, 4, 6}; // weigth
int[] v = new int[]{2, 2, 6, 5, 4}; // volume
int[] c = new int[]{2, 2, 6, 5, 4}; // 价值
System.out.println(dp(N, V, W, w, v, c));
System.out.println(dp2(N, V, W, w, v, c));
}
public static int dp(int N, int V, int W, int[] w, int[] v, int[] c) {
int[][][] dp = new int[N + 1][V + 1][W + 1];
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
for (int k = 0; k <= W; k++) {
if (j >= v[i - 1] && k >= w[i - 1]) {
dp[i][j][k] = Math.max(dp[i][j][k], dp[i - 1][j - v[i - 1]][k - w[i - 1]] + c[i - 1]);
}
}
}
}
return dp[N][V][W];
}
public static int dp2(int N, int V, int W, int[] w, int[] v, int[] c) {
int[][] dp = new int[V + 1][W + 1];
for (int i = 1; i <= N; i++) {
for (int j = V; j >= v[i - 1]; j--) {
for (int k = W; k >= w[i - 1]; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - v[i - 1]][k - w[i - 1]] + c[i - 1]);
}
}
}
return dp[V][W];
}
}
运行结果
17
17
5. 小结
当发现由熟悉的动态规划题目变形得来的题目时,在原来的状态中加一纬以满足新的限制是一种比较通用的方法。希望你能从本讲中初步体会到这种方法。