动态规划算法之背包问题详细解读(附带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]。

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

dpij=max(dpi−1j,dpi−1j−w\[i]+vi)

初始条件

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

3. Java代码实现

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() 函数详解
java 复制代码
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 初始化和状态转移
java 复制代码
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 返回最大价值
java 复制代码
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)。

java 复制代码
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表。

相关推荐
yi01138 分钟前
DAY28: LeetCode 111. 二叉树的最小深度
人工智能·笔记·python·算法·leetcode·二叉树·递归
wuyk5551 小时前
27.LeetCode 2265. 统计值等于子树平均值的节点数 —— 后序遍历实战
stm32·单片机·嵌入式硬件·算法
xiangyun611 小时前
C++ list 模拟实现(二):const迭代器与反向迭代器,模板的黑魔法
开发语言·数据库·c++·算法·stl·list
xxwl5851 小时前
三、区域赛算法模版第三章---字符串相关算法
笔记·学习·算法
信奥卷王1 小时前
[GESP202509 六级] 划分字符串(含视频)
数据结构·算法
酒醉的胡铁2 小时前
相似度搜索的常见算法
算法·ai
局部有解2 小时前
Leetcode Hot100 - 最长连续序列
算法·leetcode·职场和发展
行者全栈架构师2 小时前
给开源 Rust 工具加一个新检查项,TRAE 帮我从想法跑到构建产物:全链路实战
前端·算法·架构
行者全栈架构师2 小时前
【鸿蒙心迹】冷启动从 2.8s 优化到 0.9s——耗时拆解与数据对比实战(HarmonyOS 7.x)
前端·算法·架构
LXY_BUAA2 小时前
《嵌入式操作系统》_framebuffer驱动_20261008
linux·运维·算法