01背包与完全背包详解

👨‍💻 关于作者:会编程的土豆

"不是因为看见希望才坚持,而是坚持了才看见希望。"

你好,我是会编程的土豆,一名热爱后端技术的Java学习者。

📚 正在更新中的专栏:

💕作者简介:后端学习者

背包问题是动态规划的经典入门题型,也是面试和算法竞赛中的常客。但对于初学者来说,最困惑的不是状态转移方程,而是:为什么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背包

  • 正序 = 新值生新值 = 完全背包

理解了这一点,所有背包变种都能举一反三。

希望这篇文章能帮你彻底搞懂背包问题。如果有任何疑问,欢迎在评论区交流!

相关推荐
xiaoyaohou1121 小时前
023、数据增强改进(二):自适应数据增强与AutoAugment策略
开发语言·python
鬼圣21 小时前
Python 上下文管理器
开发语言·python
Hical_W21 小时前
为 C++ Web 框架设计三层 PMR 内存池:从原理到实战
c++·github
星空椰21 小时前
JavaScript 基础进阶:分支、循环与数组实战总结
开发语言·javascript·ecmascript
yong999021 小时前
IHAOAVOA:天鹰优化算法与非洲秃鹫优化算法的混合算法(Matlab实现)
开发语言·算法·matlab
BestOrNothing_20151 天前
C++零基础到工程实战(3.6):逻辑实战示例—日志模块
c++·命令行参数·main函数·switch case·逻辑判断·if else·enum class
t***5441 天前
有哪些常见的架构设计模式在现代C++中应用
开发语言·c++
人间打气筒(Ada)1 天前
「码动四季·开源同行」python语言:用户交互
开发语言·python·基本数据类型·注释·变量·常量·文件头
米粒11 天前
力扣算法刷题 Day 42(股票问题总结)
算法·leetcode·职场和发展
zopple1 天前
汇编、C、C++和Java核心技术对比
c语言·汇编·c++