lotus

贵有恒何必三更眠五更起 最无益只怕一日曝十日寒

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

动态规划之 二维费用的背包问题

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. 小结

当发现由熟悉的动态规划题目变形得来的题目时,在原来的状态中加一纬以满足新的限制是一种比较通用的方法。希望你能从本讲中初步体会到这种方法。

 

6. 参考资料

posted on 2023-06-30 20:57  白露~  阅读(307)  评论(0编辑  收藏  举报