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背包

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

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

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

相关推荐
chao1898443 小时前
基于 SPEA2 的多目标优化算法 MATLAB 实现
开发语言·算法·matlab
沪漂阿龙3 小时前
AI大模型面试题:支持向量机是什么?间隔最大化、软间隔、核函数、LinearSVC 全面拆解
人工智能·算法·支持向量机
難釋懷3 小时前
Redis数据结构-Set结构
数据结构·redis·bootstrap
赏金术士3 小时前
Kotlin 习题集 · 高级篇
android·开发语言·kotlin
little~钰3 小时前
倍增算法和ST表
算法
楼兰公子4 小时前
buildroot 在编译rust时裁剪平台类型数量的方法
开发语言·后端·rust
知识领航员4 小时前
蘑兔AI音乐深度实测:功能拆解、实测表现与适用场景
java·c语言·c++·人工智能·python·算法·github
薛定e的猫咪4 小时前
因果推理研究方向综述笔记
人工智能·笔记·深度学习·算法
吴声子夜歌4 小时前
Go——并发编程
开发语言·后端·golang
ooseabiscuit5 小时前
Laravel4.x:现代PHP框架的奠基之作
java·开发语言·php