从暴力递归到动态规划:0/1背包问题全解析

0/1背包问题详解:从暴力递归到动态规划

一、问题描述

经典场景

假设有一个背包,它的容量为 W(如5kg)。有 n 个物品,每个物品有两个属性:

  • 重量(weight):如2kg、3kg等
  • 价值(value):如6元、10元等

限制条件:每个物品只能选择放入背包一次(0/1选择),且总重量不能超过背包容量。

目标:如何选择物品,使得背包中物品的总价值最大?

数学表达

给定一组物品,每个物品有重量 wi 和价值 vi,背包容量为 W,求一个子集 S⊆1,2,…,n,使得:

且max∑i∈Svi且∑i∈Swi≤W

二、暴力递归解法

思路分析

对于每个物品,有两种选择:

  1. 放入背包:前提是当前背包剩余容量足够
  2. 不放入背包

通过递归遍历所有可能的选择组合,找到价值最大的方案。

代码实现

def knapsack_recursive(weights, values, W, n):
    # 基本情况:没有物品或背包容量为0
    if n == 0 or W == 0:
        return 0
    
    # 如果当前物品重量超过背包容量,则不能放入
    if weights[n-1] > W:
        return knapsack_recursive(weights, values, W, n-1)
    
    # 否则,考虑两种情况:放入或不放入当前物品
    else:
        # 选择放入当前物品
        included = values[n-1] + knapsack_recursive(weights, values, W - weights[n-1], n-1)
        # 选择不放入当前物品
        excluded = knapsack_recursive(weights, values, W, n-1)
        
        # 返回两种情况中的较大值
        return max(included, excluded)

# 示例使用
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 8  # 背包容量
n = len(weights)

max_value = knapsack_recursive(weights, values, W, n)
print(f"最大价值: {max_value}")

复杂度分析

  • 时间复杂度:O(2n ),因为每个物品有两种选择,递归树的节点数为 2n
  • 空间复杂度:O(n ),递归栈的深度最大为 n

三、动态规划解法

思路分析

暴力递归存在大量重复计算,可以使用动态规划(DP)通过表格记录中间结果,避免重复计算。

定义二维数组 dp[i][w] 表示:

  • 前 i 个物品
  • 背包容量为 w
  • 能获得的最大价值

代码实现

def knapsack_dp(weights, values, W):
    n = len(weights)
    # 创建一个二维数组 dp,dp[i][w] 表示前i个物品在容量w下的最大价值
    dp = [[0 for _ in range(W + 1)] for _ in range(n + 1)]
    
    # 填充dp数组
    for i in range(1, n + 1):
        for w in range(1, W + 1):
            # 如果当前物品重量大于当前容量,则不能放入
            if weights[i-1] > w:
                dp[i][w] = dp[i-1][w]
            else:
                # 否则,考虑放入或不放入当前物品,取最大值
                dp[i][w] = max(dp[i-1][w],  # 不放入
                              dp[i-1][w - weights[i-1]] + values[i-1])  # 放入
    
    # 返回最终结果
    return dp[n][W]

# 示例使用
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 8

max_value = knapsack_dp(weights, values, W)
print(f"最大价值: {max_value}")

复杂度分析

  • 时间复杂度:O(nW ),其中 n 是物品数量,W 是背包容量
  • 空间复杂度:O(nW ),主要用于存储二维数组

四、空间优化的动态规划解法

思路分析

观察状态转移方程,发现 dp[i][w] 只依赖于 dp[i−1][w] 和 dp[i−1][w−wi],可以将二维数组优化为一维数组。

关键技巧

  • 使用一维数组 dp[w] 表示当前容量为 w 时的最大价值
  • 遍历容量时从大到小倒序,确保每个物品只被考虑一次

代码实现

def knapsack_dp_optimized(weights, values, W):
    n = len(weights)
    # 创建一个一维数组 dp,dp[w] 表示容量为w时的最大价值
    dp = [0 for _ in range(W + 1)]
    
    # 遍历每个物品
    for i in range(n):
        # 倒序遍历容量,从W到weights[i]
        for w in range(W, weights[i] - 1, -1):
            # 考虑放入或不放入当前物品,取最大值
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    
    # 返回最终结果
    return dp[W]

# 示例使用
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 8

max_value = knapsack_dp_optimized(weights, values, W)
print(f"最大价值: {max_value}")

复杂度分析

  • 时间复杂度:O(nW ),与二维数组解法相同
  • 空间复杂度:O(W ),仅需一维数组

五、总结

三种解法对比

解法时间复杂度空间复杂度适用场景
暴力递归O(2n )O(n )小规模问题
二维DP数组O(nW )O(nW )中等规模问题
一维DP数组O(nW )O(W )大规模问题(推荐)

0/1背包问题的变形

  • 完全背包:每个物品可以无限次选取
  • 多重背包:每个物品有固定的数量限制
  • 二维费用背包:除了重量限制,还有体积等其他限制

掌握0/1背包问题的基本解法是理解更复杂背包问题的基础,建议通过练习题加深理解!

练习题推荐

希望本文能帮助你彻底理解0/1背包问题!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值