背包DP之0/1背包

背包DP之0/1背包

    • 一、0/1背包基本模型
      • 1.1 问题定义
      • 1.2 核心特征
    • 二、基础解法:二维DP
      • 2.1 状态设计与递推关系
      • 2.2 二维DP代码实现
      • 2.3 复杂度分析
    • 三、优化解法:一维DP(空间压缩)
      • 3.1 优化原理
      • 3.2 一维DP的关键:逆序遍历
      • 3.3 一维DP代码实现
        • 代码说明:
      • 3.4 复杂度分析
    • 四、0/1背包的变种问题
      • 4.1 变种1:恰好装满背包的最大价值
      • 4.2 变种2:计数问题(装满背包的方案数)
      • 4.3 变种3:二维约束背包(重量+体积)
    • 五、0/1背包的实际应用场景

0/1背包问题是动态规划(DP)领域的经典问题,也是理解“状态转移”和“空间优化”的绝佳案例,看似简单——给定物品和背包容量,选择物品装入背包使总价值最大(每个物品只能选一次),但其中蕴含的DP设计思想可推广到大量组合优化问题。

一、0/1背包基本模型

1.1 问题定义

给定n个物品,每个物品有两个属性:重量w[i]和价值v[i];有一个容量为C的背包。每个物品只能选择装入或不装入(0/1选择),求在背包不超过容量C的前提下,能装入物品的最大总价值。

示例:

  • 输入:n=3, C=4, w=[2,1,3], v=[4,2,3]
  • 输出:6(选择第0个和第1个物品,总重量2+1=3≤4,总价值4+2=6)

1.2 核心特征

  • 物品不可分割:每个物品要么全装,要么不装(区别于“完全背包”“多重背包”)。
  • 容量约束:装入物品的总重量不能超过背包容量。
  • 优化目标:最大化装入物品的总价值。

0/1背包的本质是带约束的组合优化问题,需在“选与不选”的决策中找到最优解,适合用动态规划求解。

二、基础解法:二维DP

2.1 状态设计与递推关系

动态规划的核心是“定义子问题并找到递推关系”。

  1. 定义状态
    dp[i][j]表示“考虑前i个物品(下标0~i-1),背包容量为j时的最大价值”。
    (注:i表示“物品数量”,j表示“当前背包容量”,子问题需同时约束这两个维度)

  2. 递推关系
    对于第i-1个物品(当前考虑的物品),有两种选择:

    • 不装入:总价值 = 前i-1个物品在容量j下的最大价值 → dp[i][j] = dp[i-1][j]
    • 装入(需满足j ≥ w[i-1]):总价值 = 前i-1个物品在容量j-w[i-1]下的价值 + 当前物品价值 → dp[i][j] = dp[i-1][j - w[i-1]] + v[i-1]
    • 取两种选择的最大值:
      dp[i][j] = max(dp[i-1][j], (j >= w[i-1] ? dp[i-1][j - w[i-1]] + v[i-1] : 0))
  3. 边界条件

    • i=0(无物品):dp[0][j] = 0(无论容量多大,价值都是0)。
    • j=0(容量为0):dp[i][0] = 0(无法装入任何物品)。

2.2 二维DP代码实现

public class ZeroOneKnapsack {
    public int maxValue(int n, int C, int[] w, int[] v) {
        // 二维DP数组:dp[i][j] = 前i个物品,容量j时的最大价值
        int[][] dp = new int[n + 1][C + 1];

        // 填充DP表:从1个物品开始遍历
        for (int i = 1; i <= n; i++) {
            // 当前物品的重量和价值(i-1对应原数组下标)
            int currW = w[i - 1];
            int currV = v[i - 1];

            // 遍历所有可能的容量
            for (int j = 1; j <= C; j++) {
                // 情况1:不装入当前物品
                int notTake = dp[i - 1][j];
                // 情况2:装入当前物品(需满足容量约束)
                int take = (j >= currW) ? (dp[i - 1][j - currW] + currV) : 0;

                dp[i][j] = Math.max(notTake, take);
            }
        }

        return dp[n][C]; // 考虑所有物品,容量为C时的最大价值
    }

    public static void main(String[] args) {
        ZeroOneKnapsack solution = new ZeroOneKnapsack();
        int n = 3; // 3个物品
        int C = 4; // 背包容量4
        int[] w = {2, 1, 3}; // 物品重量
        int[] v = {4, 2, 3}; // 物品价值
        System.out.println(solution.maxValue(n, C, w, v)); // 输出6
    }
}

2.3 复杂度分析

  • 时间复杂度 O ( n × C ) O(n \times C) O(n×C)。外层循环遍历n个物品,内层循环遍历C种容量,总操作次数为 n × C n \times C n×C
  • 空间复杂度 O ( n × C ) O(n \times C) O(n×C)。二维DP数组需要存储(n+1) × (C+1)个状态。

适用场景:背包容量C较小(如C ≤ 1000)时,二维DP简单直观,易于理解和实现。

三、优化解法:一维DP(空间压缩)

二维DP的空间复杂度可优化至 O ( C ) O(C) O(C),核心是利用“状态依赖关系”删除冗余维度。

3.1 优化原理

观察二维DP的递推关系:dp[i][j]仅依赖dp[i-1][j](上一行同列)和dp[i-1][j - w[i-1]](上一行左侧列)。这意味着:

  • 无需存储完整的二维数组,只需保留“上一行”的状态即可。
  • 可将二维数组压缩为一维数组dp[j],代表“当前行(第i个物品)容量j时的最大价值”。

3.2 一维DP的关键:逆序遍历

若直接复用一维数组并按正序遍历容量j,会导致“当前物品被多次装入”(违背0/1背包“每个物品只能选一次”的约束)。例如:

  • 正序遍历j时,计算dp[j]用到的dp[j - w[i-1]]可能已被更新(属于当前物品的状态),相当于多次装入。

解决方案逆序遍历容量j(从Cw[i-1])。

  • 逆序遍历确保计算dp[j]时,dp[j - w[i-1]]仍是“上一行”的状态(未被当前物品更新),符合0/1背包的约束。

3.3 一维DP代码实现

public class ZeroOneKnapsackOptimized {
    public int maxValue(int n, int C, int[] w, int[] v) {
        // 一维DP数组:dp[j] = 容量j时的最大价值(滚动更新)
        int[] dp = new int[C + 1];

        for (int i = 0; i < n; i++) { // 遍历每个物品
            int currW = w[i];
            int currV = v[i];
            // 逆序遍历容量(避免当前物品被多次选择)
            for (int j = C; j >= currW; j--) {
                // 不装入:dp[j](上一行状态);装入:dp[j - currW] + currV
                dp[j] = Math.max(dp[j], dp[j - currW] + currV);
            }
        }

        return dp[C];
    }

    public static void main(String[] args) {
        ZeroOneKnapsackOptimized solution = new ZeroOneKnapsackOptimized();
        int n = 3;
        int C = 4;
        int[] w = {2, 1, 3};
        int[] v = {4, 2, 3};
        System.out.println(solution.maxValue(n, C, w, v)); // 输出6
    }
}
代码说明:

以示例数据为例,一维dp数组的更新过程如下:

  1. 初始:dp = [0,0,0,0,0](容量0~4)
  2. 处理第0个物品(w=2, v=4):
    逆序遍历j=4→2
    • j=4dp[4] = max(0, dp[2]+4) = 4
    • j=3dp[3] = max(0, dp[1]+4) = 0(dp[1]=0)
    • j=2dp[2] = max(0, dp[0]+4) = 4
      此时dp = [0,0,4,0,4]
  3. 处理第1个物品(w=1, v=2):
    逆序遍历j=4→1
    • j=4max(4, dp[3]+2)=4(dp[3]=0)
    • j=3max(0, dp[2]+2)=6(dp[2]=4)
    • j=2max(4, dp[1]+2)=4
    • j=1max(0, dp[0]+2)=2
      此时dp = [0,2,4,6,4]
  4. 处理第2个物品(w=3, v=3):
    逆序遍历j=4→3
    • j=4max(4, dp[1]+3)=max(4,5)=5(dp[1]=2)
    • j=3max(6, dp[0]+3)=6
      最终dp = [0,2,4,6,5]dp[4]=5?不,示例输出应为6——注意:最终结果取dp[C],但此时dp[3]=6(容量3时价值6),而背包容量4允许装容量3的物品,因此最大价值为6。

3.4 复杂度分析

  • 时间复杂度 O ( n × C ) O(n \times C) O(n×C)(与二维DP相同,未改变计算次数)。
  • 空间复杂度 O ( C ) O(C) O(C)(从二维的 O ( n × C ) O(n \times C) O(n×C)优化为一维的 O ( C ) O(C) O(C))。

适用场景:背包容量C较大(如C ≤ 10^4)时,空间优化可显著减少内存占用,避免内存溢出。

四、0/1背包的变种问题

4.1 变种1:恰好装满背包的最大价值

问题:要求背包恰好装满,求最大价值(若无法装满,返回-1或其他标记)。

解法:

  • 初始化dp[j]-∞(表示无法装满),仅dp[0] = 0(容量0恰好装满,价值0)。
  • 递推时,仅当dp[j - currW]不是-∞(即j - currW可装满)时,才考虑装入当前物品。
public int maxValueExactlyFull(int n, int C, int[] w, int[] v) {
    int[] dp = new int[C + 1];
    Arrays.fill(dp, Integer.MIN_VALUE);
    dp[0] = 0; // 容量0恰好装满

    for (int i = 0; i < n; i++) {
        int currW = w[i];
        int currV = v[i];
        for (int j = C; j >= currW; j--) {
            if (dp[j - currW] != Integer.MIN_VALUE) {
                dp[j] = Math.max(dp[j], dp[j - currW] + currV);
            }
        }
    }

    return dp[C] == Integer.MIN_VALUE ? -1 : dp[C];
}

4.2 变种2:计数问题(装满背包的方案数)

问题:求恰好装满背包的方案总数(每个物品只能用一次)。

解法:

  • 初始化dp[0] = 1(容量0有1种方案:不装任何物品),其他dp[j] = 0
  • 递推时,dp[j] += dp[j - currW](装入当前物品的方案数 = 不装的方案数 + 装的方案数)。
public int countWays(int n, int C, int[] w) {
    int[] dp = new int[C + 1];
    dp[0] = 1; // 初始状态:容量0有1种方案

    for (int i = 0; i < n; i++) {
        int currW = w[i];
        for (int j = C; j >= currW; j--) {
            dp[j] += dp[j - currW];
        }
    }

    return dp[C];
}

4.3 变种3:二维约束背包(重量+体积)

问题:物品有重量w[i]和体积s[i]两个约束,背包有最大重量C和最大体积S,求最大价值。

解法:

  • 使用二维容量的DP数组dp[j][k]j为重量,k为体积)。
  • 递推时需同时满足重量和体积约束:dp[j][k] = max(dp[j][k], dp[j - w[i]][k - s[i]] + v[i])

五、0/1背包的实际应用场景

  1. 资源分配:有限预算下选择项目投资,最大化收益(预算=容量,项目成本=重量,项目收益=价值)。
  2. 任务选择:有限时间内选择任务,最大化任务总价值(时间=容量,任务耗时=重量,任务价值=价值)。
  3. 代码优化:作为其他DP问题的子结构(如“分割等和子集”“目标和”等问题可转化为0/1背包)。

总结与最佳实践
0/1背包是动态规划的入门经典,核心要点在于:

  • 状态设计:如何用“维度”刻画子问题(物品数+容量)。
  • 空间优化:利用状态依赖关系压缩空间(二维→一维,逆序遍历)。
  • 变种迁移:同一模型可解决不同场景的组合优化问题。

That’s all, thanks for reading~~
觉得有用就点个赞、收进收藏夹吧!关注我,获取更多干货~

你可能感兴趣的:(数据结构与算法分析,#,算法分析与设计,动态规划)