背包问题(0/1 背包与完全背包)解题精讲——动态规划入门

引言

新赛季备战进入白热化。2026 年南昌市中小学生信息学奥林匹克竞赛将于 10 月 25 日开赛,其普及组考察范围明确把"动态规划"列为必考基础------而动态规划入门绕不开的第一座大山,就是背包问题。

背包家族(0/1 背包、完全背包、多重背包、分组背包......)几乎每年都会以各种面目出现在各类算法竞赛的普及组、提高组里:有时是"选物品求最大价值",有时是"凑出某个金额的最少硬币数",本质都是同一套状态设计。本文用一道原创题带你把 0/1 背包吃透,再顺藤摸瓜理清完全背包与多重背包,并给出六条高频易错点与上手指引。

一、题目 / 项目目标(原创)

集训物资精打细算

集训队出发参加信息学竞赛,要把备赛物资装进一个容量为 C 的背包。共有 N 件物资,第 i 件体积为 v[i]、价值为 w[i],每件最多只能选一次 。求在总体积不超过 C 的前提下,能装下的物资最大总价值是多少?

输入样例

N = 4, C = 10 v = [2, 2, 6, 5] // 体积 w = [6, 3, 5, 4] // 价值

输出样例

14

解释:选体积为 2(价值 6)、体积 2(价值 3)、体积 6(价值 5)的三件,总体积 10、总价值 14,已达最优。

这道题就是最经典的 0/1 背包(每样东西"拿或不拿",只能选一次)。它考察的,是如何用一个"阶段 + 状态 + 转移"的框架,把指数级的暴力搜索压成多项式时间。

二、核心考点

  • 状态设计 :dp[j] 表示"在容量不超过 j 时,能得到的最大价值"。
  • 转移方程 :对于第 i 件物品,要么不选(价值不变),要么选(腾出 v[i] 容量,价值加 w[i]):
    dp[j] = max(dp[j], dp[j - v[i]] + w[i])
  • 一维空间优化 :朴素二维 dp[i][j] 可以滚动压缩成一维,内层循环逆序遍历,保证每件物品只被使用一次。
  • 初始化语义:全 0 表示"不超过容量"的最大价值;若要"恰好装满",需改初始化技巧(见易错点 2)。
  • 复杂度意识 :时间 O(N·C),空间 O(C)(一维),这是面试与竞赛的常考点。

三、解法拆解(0/1 背包)

3.1 思路

把"前 i 件物品、容量 j"的最优值递推出来。关键在于一维优化时内层必须逆序 :因为 dp[j] 的更新依赖 dp[j - v[i]],而这个旧值必须是"还没放第 i 件"的状态;若正序遍历,dp[j - v[i]] 已经被本轮(第 i 件)更新过,等于同一件物品被反复拿,就变成了完全背包。

3.2 C++ 双版解法

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    int N = 4, C = 10;
    vector<int> v = {2, 2, 6, 5};   // 体积
    vector<int> w = {6, 3, 5, 4};   // 价值

    // dp[j] = 容量不超过 j 时能获得的最大价值
    vector<int> dp(C + 1, 0);

    for (int i = 0; i < N; ++i) {
        // 一维优化:逆序遍历,保证每件物品只选一次
        for (int j = C; j >= v[i]; --j) {
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }

    cout << dp[C] << endl;  // 输出 14
    return 0;
}

3.3 Python 双版解法

python 复制代码
def knapsack_01(v, w, C):
    dp = [0] * (C + 1)
    for i in range(len(v)):
        # 逆序遍历,保证第 i 件只被考虑一次
        for j in range(C, v[i] - 1, -1):
            dp[j] = max(dp[j], dp[j - v[i]] + w[i])
    return dp[C]

v = [2, 2, 6, 5]
w = [6, 3, 5, 4]
print(knapsack_01(v, w, 10))  # 输出 14

3.4 时间与空间复杂度

  • 时间复杂度 :O(N·C),双重循环,每层物品对容量维度扫描一遍。
  • 空间复杂度 :一维写法 O(C);若保留二维 dp[N+1][C+1] 则为 O(N·C),便于回溯选了哪些物品。

四、易错点(六条命门)

  1. 一维优化内层必须逆序 :j 从 C 递减到 v[i]。写成正序会让同一物品被多次选取,悄然变成完全背包,答案偏大。
  2. "恰好装满"与"不超过"初始化不同 :求"最大价值且恰好装满"时,应设 dp[0]=0、其余为 -∞(负无穷),最后若 dp[C] < 0 说明无解;而"不超过容量"才全 0 初始化。
  3. 数组开 C+1 :容量维度从 0 到 C 共 C+1 个状态,少开一格必越界。
  4. 下标与循环范围对齐 :物品下标从 0 到 N-1,内层逆序下界是 v[i](体积大于当前容量的物品直接跳过)。
  5. 别漏掉"不选"分支 :转移是 max(dp[j], ...),dp[j] 本身代表不选第 i 件,漏写会丢失信息。
  6. 大数溢出 :价值累加到很大时 int 可能溢出,竞赛里改用 long long(C++)或 Python 原生大整数;完全背包正序循环时,dp[j - v[i]] 取的是"已含本轮"的值,这正是完全背包要的效果。

五、进阶:完全背包 / 多重背包 / 变形

5.1 完全背包(每件无限取)

只要把内层循环改为正序,物品就能被反复选取:

cpp 复制代码
// 完全背包:每件可取无限次
for (int i = 0; i < N; ++i)
    for (int j = v[i]; j <= C; ++j)
        dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
python 复制代码
def knapsack_complete(v, w, C):
    dp = [0] * (C + 1)
    for i in range(len(v)):
        for j in range(v[i], C + 1):   # 正序,可重复选
            dp[j] = max(dp[j], dp[j - v[i]] + w[i])
    return dp[C]

典型应用:硬币无限、"凑某个金额的最少/最多方案"。

5.2 多重背包(每件限 c[i] 次)------ 二进制拆分

若第 i 件最多取 c[i] 个,可把 c[i] 拆成 1, 2, 4, ... 及余数,每件当作独立的 0/1 背包物品,时间降到 O(N·C·log c[i]):

python 复制代码
def knapsack_multi(v, w, c, C):
    dv, dw = [], []
    for i in range(len(v)):
        k = 1
        rem = c[i]
        while k <= rem:
            dv.append(v[i] * k)
            dw.append(w[i] * k)
            rem -= k
            k <<= 1
        if rem > 0:
            dv.append(v[i] * rem)
            dw.append(w[i] * rem)
    dp = [0] * (C + 1)
    for i in range(len(dv)):
        for j in range(C, dv[i] - 1, -1):
            dp[j] = max(dp[j], dp[j - dv[i]] + dw[i])
    return dp[C]

5.3 更多变形

  • 背包方案数 :dp[0]=1,转移由 max 改为 +=(dp[j] = (dp[j] + dp[j - v[i]]) % MOD)。
  • 二维费用背包 :体积 + 重量双约束,dp 开二维,两重内层都逆序。
  • 第 k 优解 :状态再扩充一维记录前 k 大值。

六、小结与互动

背包问题的灵魂只有一句话:"阶段里每个状态只由'上一个阶段没被本阶段污染过的旧值'转移而来"。0/1 背包逆序保旧值,完全背包正序用新值,多重背包借二进制拆分解耦数量------搞懂这一条,整族背包都能举一反三。

你是刚啃完 0/1 背包,还是已经在刷多重背包 / 分组背包了?评论区聊聊你被背包支配(或反杀)的瞬间;想看哪类动态规划变形下一篇拆解,也欢迎点名。关注我,新赛季一起把动态规划这块硬骨头啃下来。


📚 免费少儿编程资料(夸克网盘领取)

以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:

  1. 全国青少年信息素养大赛复赛集训题目Python&C++.docx
    https://pan.quark.cn/s/93995d3cb150
  2. 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
    https://pan.quark.cn/s/da97b5dbf75d
  3. Python背记手册.pdf
    https://pan.quark.cn/s/7568ae9ca92b
  4. Python课程
    https://pan.quark.cn/s/a94bf02d00c6
  5. 2024信息素养大赛图形化复赛集训题答案3-9
    https://pan.quark.cn/s/6ccab7ec3cbc
  6. 2025年03月份电子学会考级真题
    https://pan.quark.cn/s/4403c4228912
  7. 2025全国青少年信息素养大赛赛项说明
    https://pan.quark.cn/s/d9d0df4a9f29
  8. 青少儿信息素养大赛编程资料
    https://pan.quark.cn/s/4ab6bd83be8a

资料持续更新,关注获取最新分享。

相关推荐
倒头就睡的小比特1 小时前
C++多态
c++
code2cat1 小时前
【随笔】MCP工具注解的信任边界:四个提示如何参与调用决策
python·ai agent·mcp
慢云智慧空间1 小时前
从设备智能到空间理解,慢云科技如何重新定义智慧建筑
python·科技
浮链序1 小时前
用 Claude Haiku 5.5 做子智能体路由,把 Agent 成本砍掉六成
人工智能·python·llm
冯一川1 小时前
安装Pyside 6
python
光影少年2 小时前
为什么 JavaScript 中 0.1 + 0.2 !== 0.3,如何让其相等?
前端·javascript·算法
HEJOO92 小时前
Linux 设备驱动系列——使用全局工作队列
java·linux·算法
Code_Solitude2 小时前
c语言:递归
java·c语言·算法
估值探索者2 小时前
【Python量化系统工程实战 #08】从脚本到生产:量化系统上线 checklist 的最小闭环
java·开发语言·jvm·python·数据挖掘·数据·股票数据api接口