0-1背包与完全背包:遍历顺序背后的秘密

引言

背包问题是动态规划中的经典问题,而0-1背包和完全背包是最基础的两个变种。很多人在学习时都会遇到这样一个困惑:为什么0-1背包必须倒序遍历 容量,而完全背包必须正序遍历容量?本文将深入剖析这背后的原理,帮助你真正理解而不是死记硬背。

问题回顾

0-1背包问题

有N件物品和一个容量为V的背包,每种物品只有一件,可以选择放或不放。第i件物品的重量是wi,价值是vi。求背包能装入的最大价值。

完全背包问题

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的重量是wi,价值是vi。求背包能装入的最大价值。

核心状态转移方程

两者都使用一维DP数组dpj表示容量为j的背包所能装的最大价值。

0-1背包的状态转移

复制代码
dp[j] = max(dp[j], dp[j - w[i]] + v[i])

完全背包的状态转移

完全一样!是的,两者的状态转移方程在形式上完全相同。那么区别到底在哪里?

遍历顺序的奥秘

为什么0-1背包必须倒序?

假设我们正序遍历容量,对于物品i,计算过程如下:

python 复制代码
# 错误的正序遍历
for i in range(N):  # 遍历物品
    for j in range(w[i], V + 1):  # 正序遍历容量
        dp[j] = max(dp[j], dp[j - w[i]] + v[i])

考虑一个具体例子:

  • 背包容量V = 4
  • 只有一件物品,重量w = 2,价值v = 3

正序遍历过程:

  1. j = 2: dp2 = max(dp2, dp0 + 3) = 3
  2. j = 3: dp3 = max(dp3, dp1 + 3) = 3
  3. j = 4: dp4 = max(dp4, dp2 + 3) = 6 ❗

发现问题了吗?在计算dp4时,我们使用的dp2已经被当前物品更新过了(从0变成了3)。这相当于在同一轮循环中,同一件物品被使用了两次,违反了0-1背包"每个物品只能用一次"的约束。

倒序遍历过程:

python 复制代码
# 正确的倒序遍历
for i in range(N):
    for j in range(V, w[i] - 1, -1):  # 倒序遍历容量
        dp[j] = max(dp[j], dp[j - w[i]] + v[i])
  1. j = 4: dp4 = max(dp4, dp2 + 3) = 3
  2. j = 3: dp3 = max(dp3, dp1 + 3) = 3
  3. j = 2: dp2 = max(dp2, dp0 + 3) = 3

倒序保证了在计算dpj时,dpj - w\[i]还是上一轮的状态(即没有考虑当前物品的状态),从而确保每个物品只被考虑一次。

为什么完全背包必须正序?

完全背包允许同一物品使用多次,这正是我们想要的效果!

python 复制代码
# 正确的正序遍历
for i in range(N):
    for j in range(w[i], V + 1):  # 正序遍历容量
        dp[j] = max(dp[j], dp[j - w[i]] + v[i])

继续上面的例子:

  1. j = 2: dp2 = max(dp2, dp0 + 3) = 3
  2. j = 3: dp3 = max(dp3, dp1 + 3) = 3
  3. j = 4: dp4 = max(dp4, dp2 + 3) = 6

在j=4时,我们使用了刚刚更新过的dp2(已经包含了当前物品),这意味着我们可以多次使用同一物品。这正是完全背包所需要的特性。

深入理解:状态依赖关系

让我们用二维DP的视角来理解这个问题:

0-1背包的二维表示

复制代码
dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i]] + v[i])

当前状态只依赖于上一行(i-1)的状态。

压缩到一维后,如果我们正序遍历,dpj - w\[i]可能会被当前行更新,破坏依赖关系。倒序遍历则确保我们使用的还是上一行的状态。

完全背包的二维表示

复制代码
dp[i][j] = max(dp[i-1][j], dp[i][j - w[i]] + v[i])

当前状态依赖于当前行(i)的较小容量状态!

压缩到一维后,我们需要dpj - w\[i]是已经考虑过当前物品的状态,这正是正序遍历能做到的。

代码对比

0-1背包完整实现

python 复制代码
def zero_one_pack(N, V, weight, value):
    dp = [0] * (V + 1)
    for i in range(N):
        for j in range(V, weight[i] - 1, -1):
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
    return dp[V]

完全背包完整实现

python 复制代码
def complete_pack(N, V, weight, value):
    dp = [0] * (V + 1)
    for i in range(N):
        for j in range(weight[i], V + 1):
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
    return dp[V]

进阶思考

为什么不能交换循环顺序?

有些同学可能会想:能不能先遍历容量,再遍历物品?

对于0-1背包,如果先遍历容量,那么每个容量j都会考虑所有物品,但无法保证每个物品只使用一次,实际上变成了"每个容量下选择最优物品"的问题,不是我们想要的背包问题。

初始化细节

  • 如果要求恰好装满背包:初始化dp0=0,其他为负无穷
  • 如果只要求最大价值:初始化所有dpj=0

总结

  1. 0-1背包倒序:防止同一物品被多次使用,保证状态转移基于上一轮结果
  2. 完全背包正序:允许同一物品被多次使用,状态转移基于本轮已更新结果
  3. 本质区别:状态依赖的对象不同,导致遍历顺序的差异

记住这个核心思想:遍历顺序决定了当前物品能否被重复使用。理解了这一点,你就不再需要死记硬背,而是能够根据问题的约束条件自然推导出正确的遍历顺序。

练习题推荐

  1. LeetCode 416: 分割等和子集(0-1背包)
  2. LeetCode 518: 零钱兑换II(完全背包)
  3. LeetCode 322: 零钱兑换(完全背包)
相关推荐
是烨笙啊6 小时前
如何获取 dify-deploy skill 所需要的三个key值
人工智能·ai编程·dify
龙腾AI白云6 小时前
智能体+大模型=新生产力
人工智能·plotly·知识图谱
智塑未来6 小时前
AI耳机哪个牌子好?EARWEISS听智慧凭硬核技术脱颖而出
人工智能
辣香牛肉面6 小时前
Stable Diffusion本地部署教程及模型包
人工智能
升鲜宝供应链及收银系统源代码服务6 小时前
升鲜宝AI助手 E-R 图与操作说明书(三)---升鲜宝生鲜配送供应链管理系统源代码服务
大数据·人工智能·机器学习·生鲜供应链源代码·供应链源代码出售·生鲜配送源代码服务·门店连锁系统源代码
财经资讯数据_灵砚智能6 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年6月5日
大数据·人工智能·python·ai·信息可视化·自然语言处理·灵砚智能
俊哥V6 小时前
每日 AI 研究简报 · 2026-06-06
人工智能·ai
米小虾6 小时前
2026年6月AI圈六大技术信号:从美团开源多模态到Anthropic千亿营收
人工智能
米小虾6 小时前
2026智博会闭幕:1.2万亿产业、具身智能爆发、AI转折之年已至
人工智能
Wenzar_6 小时前
VITS+Whisper微调:低延迟TTS实战
java·人工智能·whisper