动态规划(DP)背包问题知识点总结

动态规划(DP)背包问题知识点总结

背包问题是动态规划中最经典的一类问题,也是 C++ 算法学习、GESP、CSP-J/S、NOIP 竞赛中的重要知识点。

学习背包问题,建议按照以下顺序掌握:

1

01 背包

每种物品最多选一次

2

完全背包

每种物品可以选无限次

3

多重背包

每种物品有数量限制

4

背包变形问题

分组背包、二维费用、恰好装满、方案计数等

一、背包问题的基本模型

假设有 \(n\) 件物品,一个背包的最大容量为 \(W\)。

每件物品有两个属性:

  • \(w_i\):物品的重量(占用容量)。
  • \(v_i\):物品的价值(获得的收益)。

目标是在不超过背包容量的情况下,使选中物品的总价值最大。

例如:

物品 重量 \(w_i\) 价值 \(v_i\)
A 1 15
B 3 20
C 4 30

背包容量 \(W=4\)。

可以选择 A、B 或 C。最优选择是 A+B,总重量为 4,总价值为 35。

二、动态规划的核心思想

1. 状态定义

最经典的一维状态定义为:

\[ dp[j]=\text{容量不超过 }j\text{ 时的最大价值} \]

例如:

  • dp[0]:容量为 0 时,最大价值为 0。
  • dp[3]:容量不超过 3 时,可以获得的最大价值。
  • dp[4]:容量不超过 4 时,可以获得的最大价值。

2. 状态转移方程

对于一件重量为 \(w_i\)、价值为 \(v_i\) 的物品:

\[ dp[j]=\max(dp[j],dp[j-w_i]+v_i) \]

前提是 \(j\geq w_i\)。

这个公式表示两种选择:

  • 不选当前物品: dp[j]
  • 选择当前物品: dp[j-w[i]] + v[i]

两者取最大值。

3. 为什么需要循环顺序?

这是背包问题最关键的知识点之一。

  • 01 背包:容量从大到小遍历。
  • 完全背包:容量从小到大遍历。

原因是:遍历顺序决定当前物品能否被重复使用。

三、01 背包(每件物品最多选一次)

1. 特点

每件物品只有两种选择:

  • 选一次。
  • 不选。

不能选第二次。

2. C++ 模板


#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    int n, W;
    cin >> n >> W;

    vector<int> w(n + 1), v(n + 1);
    vector<int> dp(W + 1, 0);

    for (int i = 1; i <= n; i++) {
        cin >> w[i] >> v[i];
    }

    for (int i = 1; i <= n; i++) {
        // 从大到小,保证每件物品最多选一次
        for (int j = W; j >= w[i]; j--) {
            dp[j] = max(dp[j],
                        dp[j - w[i]] + v[i]);
        }
    }

    cout << dp[W] << '\n';
    return 0;
}

3. 为什么容量必须倒序?

假设当前物品重量为 2、价值为 3。

如果容量正序遍历,更新后的 dp[2] 可能被 dp[4] 再次使用,从而让同一件物品被重复选择。

容量倒序可以避免这种情况。

记忆口诀:01 背包,容量倒着跑。

四、完全背包(每件物品可以选无限次)

1. 特点

每种物品可以选择:

  • 不选。
  • 选一次。
  • 选两次。
  • 选很多次。

只要总重量不超过背包容量即可。

2. C++ 模板


#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    int n, W;
    cin >> n >> W;

    vector<int> w(n + 1), v(n + 1);
    vector<int> dp(W + 1, 0);

    for (int i = 1; i <= n; i++) {
        cin >> w[i] >> v[i];
    }

    for (int i = 1; i <= n; i++) {
        // 从小到大,允许重复选择当前物品
        for (int j = w[i]; j <= W; j++) {
            dp[j] = max(dp[j],
                        dp[j - w[i]] + v[i]);
        }
    }

    cout << dp[W] << '\n';
    return 0;
}

3. 为什么容量正序?

因为更新 dp[j] 时,dp[j-w[i]] 可能已经使用过当前物品,所以可以再次选择它。

记忆口诀:完全背包,容量正着跑。

五、01 背包和完全背包的对比

比较项 01 背包 完全背包
每种物品数量 最多 1 件 无限件
外层循环 遍历物品 遍历物品
内层循环 容量倒序 容量正序
转移方程 相同形式 相同形式
核心区别 防止重复使用 允许重复使用
口诀 倒序 正序

最容易记住的模板差异只有一处:

01 背包

倒序


for (int j = W;
     j >= w[i]; j--)

完全背包

正序


for (int j = w[i];
     j <= W; j++)

六、多重背包(每种物品数量有限)

1. 特点

每种物品有一个数量上限 \(c_i\)。

例如,某种物品有 5 件,最多只能选 5 件。

2. 基础模板:枚举数量


#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    int n, W;
    cin >> n >> W;

    vector<int> dp(W + 1, 0);

    for (int i = 1; i <= n; i++) {
        int w, v, c;
        cin >> w >> v >> c;

        // 01 背包倒序更新,每件物品最多使用一次
        // 对当前种类,逐件处理 c 件物品
        for (int k = 1; k <= c; k++) {
            for (int j = W; j >= w; j--) {
                dp[j] = max(dp[j],
                            dp[j - w] + v);
            }
        }
    }

    cout << dp[W] << '\n';
    return 0;
}

这个写法将每件物品分别作为一件 01 背包处理,适合数量较小的情况。数量很大时,可以学习二进制分组优化,把数量拆成若干组,将复杂度从与物品总数量相关降低到与数量的对数相关。

七、其他常见背包变形

类型 主要特点 常见应用
分组背包 每组最多选择一个 多种课程中每组选择一门
二维费用背包 同时限制两种资源 重量和体积都有上限
恰好装满背包 总重量必须等于 \(W\) 凑出指定金额、指定容量
方案数背包 计算满足条件的组合数 凑硬币、组合计数
完全背包求最小值 最小化物品数量或成本 最少硬币凑金额
混合背包 01、完全、多重背包混合 多种物品有不同数量限制

1. 分组背包

每一组中只能选一个物品或不选。

常见写法是先枚举组,再倒序遍历容量,并在每组内部枚举可选物品。

2. 恰好装满背包

如果要求恰好装满,不能把所有状态都初始化为 0,否则不可达状态也会被当作有效状态。

通常初始化:

vector<int> dp(W + 1, -1);
dp[0] = 0;

这里假设价值非负,-1 用于表示不可达。更新前还要判断前一个状态是否可达。

如果价值可能为负数,应该使用 bool 可达数组或其他不会与合法价值冲突的标记。

3. 方案数背包

如果题目要求“有多少种组合”,通常需要将最大值转移改为计数累加,并注意题目区分组合与排列。

例如完全背包求组合数,通常是:

for (int i = 1; i <= n; i++) {
    for (int j = w[i]; j <= W; j++) {
        dp[j] += dp[j - w[i]];
    }
}

这段代码适用于计算使用给定物品类型凑出容量的组合数,初始化和取模规则要根据具体题目要求设置。

八、背包问题的时间复杂度

类型 常见时间复杂度 空间复杂度
01 背包 \(O(nW)\) \(O(W)\)
完全背包 \(O(nW)\) \(O(W)\)
多重背包(逐件处理) \(O(W\sum c_i)\) \(O(W)\)
多重背包(二进制优化) \(O(W\sum \log(c_i+1))\) \(O(W)\)
二维费用背包 \(O(nW_1W_2)\) \(O(W_1W_2)\)

注意:这里的 \(W\) 是背包容量,不是输入数据的位数。若 \(W\) 非常大,\(O(nW)\) 的动态规划可能无法通过,需要考虑其他算法。

九、做题时如何判断背包类型?

背包题型判断流程

题目是否有限制容量或总和?

每种物品最多能选几次?

最多 1 次

01 背包

容量倒序

无限次

完全背包

容量正序

有限件数

多重背包

数量受限

最后再判断目标:最大价值、最小成本、方案数量,还是恰好装满。

十、练习建议

建议按下面顺序练习,不要一开始就做复杂的混合背包。

  1. 先掌握 01 背包,能够独立写出状态定义、转移方程和倒序循环。
  2. 将 01 背包的内层循环改成正序,理解完全背包为什么允许重复选择。
  3. 学习多重背包和分组背包。
  4. 再做恰好装满、方案计数、二维费用等变形。

最核心的三句话:

  • 01 背包:每件物品最多一次,容量倒序。
  • 完全背包:每种物品无限次,容量正序。
  • 多重背包:每种物品数量有限,需要限制使用次数。
posted @ 2026-10-10 17:45  kkman2000  阅读(17)  评论(0)    收藏  举报