动态规划算法之背包问题详细解读(附带Java代码解读)

动态规划中的背包问题(Knapsack Problem)是经典问题之一,通常用来解决选择一组物品放入背包使得背包的价值最大化的问题。根据问题条件的不同,背包问题有很多种变体,如0-1背包问题完全背包问题多重背包问题等。这里,我们详细介绍最经典的0-1背包问题,并提供代码的详细解读。

1. 0-1背包问题简介

在0-1背包问题中,有一个容量为 C 的背包和 n 件物品。每件物品有两个属性:重量 w[i]价值 v[i]。目标是选择若干件物品放入背包,使得总重量不超过 C,并且背包中物品的总价值最大化。

问题的约束:
  1. 每件物品要么选择(放入背包),要么不选择,因此称为 0-1 背包问题。
  2. 背包的总重量不能超过 C
  3. 要最大化背包中物品的总价值。

2. 动态规划的思路

动态规划适用于背包问题,因为它具有最优子结构重叠子问题的性质。解决这个问题的核心在于:针对每一个物品,都有两种选择——要么放进背包,要么不放进背包。

状态定义

定义 dp[i][j] 表示前 i 件物品放入容量为 j 的背包时所能获得的最大总价值。

状态转移方程

对于每件物品 i

  1. 如果不将第 i 件物品放入背包,则最大价值就是 dp[i-1][j],即前 i-1 件物品的最大价值。
  2. 如果将第 i 件物品放入背包,则最大价值为 dp[i-1][j-w[i]] + v[i],即前 i-1 件物品在剩余容量 j-w[i] 时的最大价值加上当前物品的价值 v[i]

综合起来,状态转移方程为:

dp[i][j]=max(dp[i−1][j],dp[i−1][j−w[i]]+v[i])

初始条件

当背包容量为0时,无论选哪件物品,最大价值都是0,即 dp[i][0] = 0

3. Java代码实现

public class Knapsack {
    public static void main(String[] args) {
        // 定义物品的重量和价值
        int[] weights = {2, 3, 4, 5}; // 每个物品的重量
        int[] values = {3, 4, 5, 6};  // 每个物品的价值
        int capacity = 8; // 背包容量
        int n = weights.length; // 物品数量

        // 计算并输出背包的最大价值
        System.out.println("Maximum value in Knapsack = " + knapsack(weights, values, n, capacity));
    }

    // 动态规划方法解决0-1背包问题
    public static int knapsack(int[] weights, int[] values, int n, int capacity) {
        // 定义DP数组:dp[i][j] 表示前 i 件物品在容量为 j 时的最大价值
        int[][] dp = new int[n + 1][capacity + 1];

        // 填充DP表
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= capacity; j++) {
                if (weights[i - 1] <= j) { // 当前物品可以放入背包
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
                } else { // 当前物品无法放入背包
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        // 返回最后一个格子的值,即最大价值
        return dp[n][capacity];
    }
}

4. 详细解释代码

4.1 输入和输出

main 方法中,定义了物品的重量数组 weights[]、价值数组 values[],以及背包的总容量 capacity 和物品数量 n。然后调用 knapsack() 方法来计算背包中可以获得的最大价值。

4.2 knapsack() 函数详解
public static int knapsack(int[] weights, int[] values, int n, int capacity) {
    // 定义DP数组:dp[i][j] 表示前 i 件物品在容量为 j 时的最大价值
    int[][] dp = new int[n + 1][capacity + 1];

首先,定义了二维数组 dp[][],其中 dp[i][j] 表示前 i 件物品在背包容量为 j 时能够获得的最大总价值。数组的大小为 [n+1][capacity+1],因为我们需要处理物品数量从0到n、容量从0到capacity的所有情况。

4.3 初始化和状态转移
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= capacity; j++) {
        if (weights[i - 1] <= j) {
            dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
        } else {
            dp[i][j] = dp[i - 1][j];
        }
    }
}

接下来,用两个嵌套的 for 循环遍历物品和背包容量,进行状态转移:

  • 外层循环 i 遍历每一件物品。
  • 内层循环 j 遍历背包的每个可能容量。

对于每个物品 i

  • 如果该物品的重量 weights[i-1] 小于或等于当前容量 j,可以选择放入背包或不放入背包,选择价值最大的方案。这就是通过状态转移方程 dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1]) 计算出的。
  • 如果该物品的重量 weights[i-1] 超过当前容量 j,则无法将其放入背包,此时只能继承前 i-1 件物品的最大价值,即 dp[i][j] = dp[i-1][j]
4.4 返回最大价值
return dp[n][capacity];

循环结束后,dp[n][capacity] 就是前 n 件物品在背包容量为 capacity 时能够获得的最大价值,因此返回该值。

5. 复杂度分析

  • 时间复杂度O(n * capacity),因为我们需要填充一个大小为 n * capacity 的表。
  • 空间复杂度O(n * capacity),因为我们使用了二维数组 dp[][] 存储中间结果。

6. 空间优化

在上述实现中,我们用了二维数组 dp[][] 来存储所有状态。但实际上,在每一行 i 的状态转移时,只依赖于上一行 i-1 的值。因此,我们可以将二维数组压缩为一维数组,从而降低空间复杂度到 O(capacity)

public static int knapsackOptimized(int[] weights, int[] values, int n, int capacity) {
    int[] dp = new int[capacity + 1];

    for (int i = 1; i <= n; i++) {
        // 从大到小遍历容量,保证每个物品只被计算一次
        for (int j = capacity; j >= weights[i - 1]; j--) {
            dp[j] = Math.max(dp[j], dp[j - weights[i - 1]] + values[i - 1]);
        }
    }

    return dp[capacity];
}
关键点:
  • 在每次迭代中,从容量 capacity 开始递减遍历,确保每个物品只更新一次。这种写法防止了在同一轮次中更新状态值时物品被重复选择。

7. 总结

0-1背包问题通过动态规划求解,有明确的状态转移方程。使用二维DP表来记录每个物品在不同容量下的最大价值,最终得到最优解。通过压缩空间,可以进一步优化到一维DP表。

你可能感兴趣的:(算法分析,算法,动态规划)