动态规划:0-1背包问题空间复杂度优化详解
字数 1970 2025-12-07 00:11:33

动态规划:0-1背包问题空间复杂度优化详解

题目描述
0-1背包问题是经典的动态规划问题:给定n个物品,每个物品有重量w_i和价值v_i,以及一个容量为C的背包。每个物品要么完整放入(1),要么不放入(0)。目标是选择若干物品放入背包,使得总重量不超过C,且总价值最大。本专题将深入讲解如何将其空间复杂度从O(nC)优化到O(C)。

解题过程

1. 问题定义与基础状态设计
设dp[i][j]表示考虑前i个物品(物品编号从1到i),在背包容量为j时能获得的最大价值。
状态转移方程:
dp[i][j] = max(dp[i-1][j], // 不选第i个物品
dp[i-1][j - w_i] + v_i) // 选第i个物品,要求j >= w_i
基础状态:dp[0][j] = 0 (0个物品价值为0)

这是二维DP表格,空间复杂度O(nC)。

2. 空间优化核心观察
仔细看状态转移方程:dp[i][j]的计算只依赖于上一行(i-1)的数据!
具体来说,dp[i][j]只依赖于dp[i-1][j]和dp[i-1][j-w_i]。这意味着我们不需要存储完整的n行,只需要两行(当前行和上一行)就能完成计算。

3. 滚动数组优化(两行数组)
我们可以用两个一维数组:

  • prev[0..C] 表示上一行(i-1)的结果
  • curr[0..C] 表示当前行(i)的结果
    计算过程:
prev = [0] * (C+1)  # 初始化第0行
for i in range(1, n+1):
    for j in range(C+1):
        curr[j] = prev[j]  # 不选当前物品
        if j >= w[i-1]:
            curr[j] = max(curr[j], prev[j - w[i-1]] + v[i-1])
    prev, curr = curr, prev  # 交换,当前行变为下一轮的上一行

空间复杂度:O(2C) = O(C)

4. 进一步优化到单行数组(倒序更新)
关键洞察:如果我们用单个数组dp[0..C],并从后往前更新,可以避免覆盖还需要使用的"上一行"数据。

为什么必须倒序?

  • 当我们计算dp[j]时(考虑前i个物品),需要用到dp[j](相当于上一行的dp[i-1][j])和dp[j-w_i](相当于上一行的dp[i-1][j-w_i])。
  • 如果从C到0逆序更新:
    • 当我们更新dp[j]时,dp[j]还是上一轮(i-1)的值
    • dp[j-w_i]也是上一轮(i-1)的值(因为j-w_i < j,我们还没更新到它)
  • 如果从0到C正序更新:
    • 当我们更新dp[j]时,dp[j-w_i]可能已经被更新为当前轮(i)的值,这会导致错误

单数组实现:

def knapsack_01_optimized(weights, values, capacity):
    n = len(weights)
    dp = [0] * (capacity + 1)
    
    for i in range(n):  # 考虑每个物品
        w, v = weights[i], values[i]
        # 关键:从后往前遍历容量
        for j in range(capacity, w-1, -1):
            # dp[j] 不选当前物品(保持原值)
            # dp[j-w] + v 选当前物品
            dp[j] = max(dp[j], dp[j-w] + v)
    
    return dp[capacity]

5. 算法步骤分解(以示例说明)
示例:n=3, C=4, weights=[1,3,4], values=[15,20,30]

初始化:dp = [0,0,0,0,0] (容量0~4)

物品1 (w=1, v=15):

  • j=4: dp[4] = max(dp[4]=0, dp[3]=0+15) = 15
  • j=3: dp[3] = max(0, dp[2]=0+15) = 15
  • j=2: dp[2] = max(0, dp[1]=0+15) = 15
  • j=1: dp[1] = max(0, dp[0]=0+15) = 15
  • j=0: 跳过 (j < w)
    dp变为: [0,15,15,15,15]

物品2 (w=3, v=20):

  • j=4: dp[4] = max(15, dp[1]=15+20) = 35
  • j=3: dp[3] = max(15, dp[0]=0+20) = 20
  • j=2: 跳过 (j<3)
    dp变为: [0,15,15,20,35]

物品3 (w=4, v=30):

  • j=4: dp[4] = max(35, dp[0]=0+30) = 35
    dp最终: [0,15,15,20,35]

最大价值为35。

6. 时间与空间复杂度分析

  • 时间复杂度:O(nC),与二维DP相同
  • 空间复杂度:O(C),相比二维DP的O(nC)是显著优化
  • 当C很大时,可能仍会占用较多内存,但对于大多数实际问题,C通常在可接受范围

7. 优化方案的适用性与限制

  • 适用场景:标准的0-1背包问题
  • 不适用场景
    • 需要回溯具体选择了哪些物品(单数组丢失了决策路径信息)
    • 完全背包问题(物品无限个)需要正序更新
    • 多重背包问题需要特殊处理
  • 如果需要具体方案,可以用二维DP或额外记录决策

8. 常见变体与扩展

  • 恰好装满背包:初始化dp[0]=0, dp[1..C]=-∞
  • 方案计数:dp[j]表示方案数,转移时用加法
  • 最优方案回溯:用二维DP或额外数组记录选择

总结
0-1背包问题的空间优化核心是认识到状态转移的"无后效性"和"单向依赖性",通过倒序更新单数组,既保留了子问题的解,又避免了数据覆盖。这是动态规划空间优化的经典范例,在竞赛和面试中经常出现。

动态规划:0-1背包问题空间复杂度优化详解 题目描述 0-1背包问题是经典的动态规划问题:给定n个物品,每个物品有重量w_ i和价值v_ i,以及一个容量为C的背包。每个物品要么完整放入(1),要么不放入(0)。目标是选择若干物品放入背包,使得总重量不超过C,且总价值最大。本专题将深入讲解如何将其空间复杂度从O(nC)优化到O(C)。 解题过程 1. 问题定义与基础状态设计 设dp[ i][ j ]表示考虑前i个物品(物品编号从1到i),在背包容量为j时能获得的最大价值。 状态转移方程: dp[ i][ j] = max(dp[ i-1][ j ], // 不选第i个物品 dp[ i-1][ j - w_ i] + v_ i) // 选第i个物品,要求j >= w_ i 基础状态:dp[ 0][ j ] = 0 (0个物品价值为0) 这是二维DP表格,空间复杂度O(nC)。 2. 空间优化核心观察 仔细看状态转移方程:dp[ i][ j]的计算 只依赖于上一行(i-1) 的数据! 具体来说,dp[ i][ j]只依赖于dp[ i-1][ j]和dp[ i-1][ j-w_ i]。这意味着我们不需要存储完整的n行,只需要 两行 (当前行和上一行)就能完成计算。 3. 滚动数组优化(两行数组) 我们可以用两个一维数组: prev[ 0..C ] 表示上一行(i-1)的结果 curr[ 0..C ] 表示当前行(i)的结果 计算过程: 空间复杂度:O(2C) = O(C) 4. 进一步优化到单行数组(倒序更新) 关键洞察:如果我们用 单个数组dp[ 0..C] ,并从后往前更新,可以避免覆盖还需要使用的"上一行"数据。 为什么必须倒序? 当我们计算dp[ j]时(考虑前i个物品),需要用到dp[ j](相当于上一行的dp[ i-1][ j])和dp[ j-w_ i](相当于上一行的dp[ i-1][ j-w_ i ])。 如果 从C到0逆序 更新: 当我们更新dp[ j]时,dp[ j ]还是上一轮(i-1)的值 dp[ j-w_ i]也是上一轮(i-1)的值(因为j-w_ i < j,我们还没更新到它) 如果 从0到C正序 更新: 当我们更新dp[ j]时,dp[ j-w_ i ]可能已经被更新为当前轮(i)的值,这会导致错误 单数组实现: 5. 算法步骤分解(以示例说明) 示例:n=3, C=4, weights=[ 1,3,4], values=[ 15,20,30 ] 初始化:dp = [ 0,0,0,0,0 ] (容量0~4) 物品1 (w=1, v=15): j=4: dp[ 4] = max(dp[ 4]=0, dp[ 3 ]=0+15) = 15 j=3: dp[ 3] = max(0, dp[ 2 ]=0+15) = 15 j=2: dp[ 2] = max(0, dp[ 1 ]=0+15) = 15 j=1: dp[ 1] = max(0, dp[ 0 ]=0+15) = 15 j=0: 跳过 (j < w) dp变为: [ 0,15,15,15,15 ] 物品2 (w=3, v=20): j=4: dp[ 4] = max(15, dp[ 1 ]=15+20) = 35 j=3: dp[ 3] = max(15, dp[ 0 ]=0+20) = 20 j=2: 跳过 (j <3) dp变为: [ 0,15,15,20,35 ] 物品3 (w=4, v=30): j=4: dp[ 4] = max(35, dp[ 0 ]=0+30) = 35 dp最终: [ 0,15,15,20,35 ] 最大价值为35。 6. 时间与空间复杂度分析 时间复杂度:O(nC),与二维DP相同 空间复杂度:O(C),相比二维DP的O(nC)是显著优化 当C很大时,可能仍会占用较多内存,但对于大多数实际问题,C通常在可接受范围 7. 优化方案的适用性与限制 适用场景 :标准的0-1背包问题 不适用场景 : 需要回溯具体选择了哪些物品(单数组丢失了决策路径信息) 完全背包问题(物品无限个)需要正序更新 多重背包问题需要特殊处理 如果需要具体方案,可以用二维DP或额外记录决策 8. 常见变体与扩展 恰好装满背包 :初始化dp[ 0]=0, dp[ 1..C ]=-∞ 方案计数 :dp[ j ]表示方案数,转移时用加法 最优方案回溯 :用二维DP或额外数组记录选择 总结 0-1背包问题的空间优化核心是认识到状态转移的"无后效性"和"单向依赖性",通过倒序更新单数组,既保留了子问题的解,又避免了数据覆盖。这是动态规划空间优化的经典范例,在竞赛和面试中经常出现。