
👨💻 关于作者:会编程的土豆
"不是因为看见希望才坚持,而是坚持了才看见希望。"
你好,我是会编程的土豆,一名热爱后端技术的Java学习者。
📚 正在更新中的专栏:
-
《数据结构与算法》😊😊😊
-
《leetcode hot 100》🥰🥰🥰🤩🤩🤩
-
《数据库mysql》
💕作者简介:后端学习者
背包问题是动态规划的经典入门题型,也是面试和算法竞赛中的常客。但对于初学者来说,最困惑的不是状态转移方程,而是:为什么01背包要倒序遍历?为什么完全背包要正序遍历?
本文将用最通俗的语言和图解,帮你彻底理解这两种背包的核心区别。读完这篇文章,你将掌握:
-
01背包与完全背包的标准模板
-
正序与倒序的本质原理
-
P1048 采药 和 P1616 疯狂的采药 的 AC 代码
什么是背包问题?
问题描述
有一个容量为
m的背包,有n件物品,每件物品有重量w[i]和价值v[i]。问:如何选择物品装入背包,使得总重量不超过容量,且总价值最大?
现实场景
-
旅行打包:行李箱空间有限,怎么装最值钱?
-
投资理财:资金有限,怎么投收益最高?
-
时间管理:时间有限,怎么安排收获最大?
二、01背包(每个物品最多选一次)

经典例题:P1048 [NOIP2005 普及组] 采药
题目大意 :有 M 株草药,每株有采摘时间 time 和价值 value,总时间 T 内,每株最多采一次,求最大总价值。
cpp
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main()
{
int T, M;
cin >> T >> M;
vector<int>dp(T + 1, 0);
for (int i = 0; i < M; i++)
{
int time, value;
cin >> time >> value;
for (int j = T; j >= 0; j--)
{
if(j>=time)
dp[j] = max(dp[j], dp[j - time] + value);
}
}
cout << dp[T] << endl;
return 0;
}
状态定义
dp[j]表示:在时间不超过j的前提下,能获得的最大价值。
状态转移方程
cpp
dp[j] = max(dp[j], dp[j - time] + value)
-
dp[j]:不采这株草药 -
dp[j - time] + value:采这株草药(花time时间,得value价值)
核心代码
cpp
for (int i = 0; i < M; i++) {
int time, value;
cin >> time >> value;
for (int j = T; j >= time; j--) { // 倒序遍历
dp[j] = max(dp[j], dp[j - time] + value);
}
}
为什么必须倒序?
正序会导致同一物品被多次使用。
举例:T = 3,物品 time = 1, value = 10
正序(错误):
cpp
j = 1: dp[1] = max(0, dp[0]+10) = 10
j = 2: dp[2] = max(0, dp[1]+10) = 20 ← 用了刚更新的 dp[1]
j = 3: dp[3] = max(0, dp[2]+10) = 30 ← 又用了刚更新的 dp[2]
一个物品被用了 3次!
倒序(正确):
cpp
j = 3: dp[3] = max(0, dp[2]+10) = 10 ← dp[2] 还是旧值 0
j = 2: dp[2] = max(0, dp[1]+10) = 10 ← dp[1] 还是旧值 0
j = 1: dp[1] = max(0, dp[0]+10) = 10
每个物品只用了 1次。
一句话理解
倒序用"旧值"生"新值",各容量状态相互隔离,保证物品只用一次。
三、完全背包(每个物品可选无限次)
经典例题:P1616 疯狂的采药

题目大意 :与采药相同,但每种草药可以无限次采摘。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int T, M;
cin >> T >> M;
vector<long long> dp(T + 1, 0); // 注意:可能很大,用 long long
for (int i = 0; i < M; i++) {
int time, value;
cin >> time >> value;
// 完全背包:正序遍历
for (int j = time; j <= T; j++) {
dp[j] = max(dp[j], dp[j - time] + value);
}
}
cout << dp[T] << endl;
return 0;
}
核心代码
cpp
for (int i = 0; i < M; i++) {
int time, value;
cin >> time >> value;
for (int j = time; j <= T; j++) { // 正序遍历
dp[j] = max(dp[j], dp[j - time] + value);
}
}
为什么正序就能无限用?
正序让"新值"立刻变成"基础值"供后续使用,形成链式叠加。
举例:T = 3,物品 time = 1, value = 10
正序(正确):
cpp
j = 1: dp[1] = max(0, dp[0]+10) = 10
j = 2: dp[2] = max(0, dp[1]+10) = 20 ← 用了刚更新的 dp[1],叠加一次
j = 3: dp[3] = max(0, dp[2]+10) = 30 ← 用了刚更新的 dp[2],再叠加一次
物品被用了 3次,正是完全背包想要的效果!
一句话理解
正序让"新值生新值",形成滚雪球效应,允许物品无限次使用。
四、对比总结表
| 背包类型 | 限制 | 遍历顺序 | 核心原理 | 口诀 |
|---|---|---|---|---|
| 01背包 | 每件最多1次 | 倒序 j = T; j >= time; j-- |
旧值生新值,相互隔离 | "01 倒着走" |
| 完全背包 | 每件无限次 | 正序 j = time; j <= T; j++ |
新值生新值,链式叠加 | "完全正着来" |
五、变种:不选也有价值(P1802 5倍经验日)
有些题目中,"不选"这个决策也会产生价值。例如 P1802,打不过好友也能获得失败经验。
状态转移
cpp
for (int j = x; j >= 0; j--) {
long long no_use = dp[j] + lose; // 不打,得失败经验
long long use = (j >= num) ? dp[j - num] + win : -1; // 打,得胜利经验
dp[j] = max(no_use, use);
}
本质仍是 01 背包,只是"不选"的价值从 0 变成了 lose。
六、常见问题 FAQ
Q1:为什么循环可以从 time 开始而不是 0?
因为当 j < time 时,物品装不下,dp[j] 不会改变,所以可以跳过。两种写法等价
cpp
// 写法1(简洁)
for (int j = T; j >= time; j--)
// 写法2(直观)
for (int j = T; j >= 0; j--) {
if (j >= time) dp[j] = max(dp[j], dp[j - time] + value);
}
Q2:j - time 会不会越界?
不会。因为循环条件是 j >= time,保证了 j - time >= 0。
Q3:dp 数组为什么要用 long long?
当数据范围较大时,最大价值可能超过 int 上限(约 21 亿),用 long long 更安全。
八、结语
背包问题的核心,不在于背模板,而在于理解 "正序与倒序" 背后的原理:
-
倒序 = 旧值生新值 = 01背包
-
正序 = 新值生新值 = 完全背包
理解了这一点,所有背包变种都能举一反三。
希望这篇文章能帮你彻底搞懂背包问题。如果有任何疑问,欢迎在评论区交流!