背包:从入门到实战

目录

  1. 背包问题的本质
  2. 01 背包
  3. 完全背包
  4. 多重背包
  5. 混合背包
  6. 二维费用背包
  7. 分组背包
  8. 有依赖的背包
  9. 树形背包
  10. 背包方案数
  11. 输出具体方案
  12. 第 k 优解
  13. 可行性背包与 bitset 优化
  14. 恰好、不超过、至少
  15. 泛化物品背包
  16. 总结与刷题路线

1. 背包问题的本质

1.1 什么是背包问题

背包问题是一类在限制条件下做选择的优化问题。最经典的描述是:

有一个容量为 V 的背包,有 n 件物品,每件物品有体积 w[i] 和价值 v[i]。在不超过背包容量的前提下,选择一些物品,使得总价值最大。

这个模型可以套到很多实际场景:

  • 采药:时间限制 = 背包容量,草药价值 = 物品价值
  • 点菜:预算 = 背包容量,菜品价格 = 物品体积
  • 选课:学分限制 = 背包容量,课程学分 = 物品价值
  • 砝码称重:能称出的重量 = 背包容量,砝码 = 物品

1.2 为什么用动态规划

如果暴力枚举每件物品选或不选,复杂度是 O(2^n),n=100 就爆了。

背包问题的特点是:

  • 有重叠子问题:容量为 j 时的最优解会被反复用到
  • 有最优子结构:大容量的最优解可以由小容量的最优解推出

所以用 DP:把“容量”作为状态,逐步加入物品,更新每个容量下的最优值。

1.3 统一状态定义

绝大多数背包问题都可以用这个状态:

f[j] = 容量为 j 时能获得的最大价值

或者:

f[j] = 容量为 j 时的方案数

或者:

f[j] = 容量为 j 是否可达

区别只在于:

  • 转移是取 max、求和还是取或
  • 循环方向是正序还是逆序
  • 初始化是什么

1.4 核心口诀

01 背包逆序,完全背包正序,分组背包逆序,树形背包合并。

为什么?后面每种类型都会详细解释。


2. 01 背包

2.1 问题描述

有 n 件物品,每件物品有体积 w[i] 和价值 v[i]。每件物品最多选一次。背包容量为 V,求最大总价值。

2.2 状态设计

设:

f[j] = 容量为 j 时能获得的最大价值

2.3 朴素二维写法

先看最直观的二维 DP:

f[i][j] = 前 i 件物品,容量为 j 时的最大价值

转移:

  • 不选第 i 件:f[i][j] = f[i-1][j]
  • 选第 i 件(前提 j >= w[i]):f[i][j] = f[i-1][j-w[i]] + v[i]

取最大值:

for (int i = 1; i <= n; i++) {
    for (int j = 0; j <= V; j++) {
        f[i][j] = f[i-1][j];
        if (j >= w[i])
            f[i][j] = max(f[i][j], f[i-1][j-w[i]] + v[i]);
    }
}

答案:f[n][V]。

复杂度:时间 O(nV),空间 O(nV)。

2.4 一维滚动数组优化

观察转移:f[i][j] 只依赖 f[i-1][...],所以可以去掉第一维。

但如果直接写:

for (int i = 1; i <= n; i++)
    for (int j = w[i]; j <= V; j++)
        f[j] = max(f[j], f[j-w[i]] + v[i]);

这是错的。因为正序时,f[j-w[i]] 可能已经被第 i 件物品更新过,导致第 i 件被选了多次,变成了完全背包。

正确写法是容量逆序:

for (int i = 1; i <= n; i++)
    for (int j = V; j >= w[i]; j--)
        f[j] = max(f[j], f[j-w[i]] + v[i]);

为什么逆序正确?

当计算 f[j] 时,f[j-w[i]] 还没有被第 i 件物品更新过,所以它代表的是“前 i-1 件物品在容量 j-w[i] 时的最优值”。这样第 i 件物品只会被用一次。

2.5 初始化

  • 不超过容量:f[0..V] = 0,答案 f[V]
  • 恰好装满:f[0] = 0,其他 f[j] = -INF,答案 f[V](若为 -INF 则无解)

为什么恰好装满要这样?因为 f[j] 表示“恰好容量为 j”。初始时只有容量 0 是“恰好装满”的,其他容量都不可达。

2.6 完整模板

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 1005;
int f[MAXV];

int main() {
    int n, V;
    cin >> n >> V;
    for (int i = 1; i <= n; i++) {
        int w, v;
        cin >> w >> v;
        for (int j = V; j >= w; j--) {
            f[j] = max(f[j], f[j-w] + v);
        }
    }
    cout << f[V] << endl;
    return 0;
}

2.7 常见坑

  1. 循环方向写反:01 背包必须逆序,写成正序就变成完全背包。
  2. 内层循环下界:j >= w[i],不能从 0 开始,否则 j-w[i] 越界。
  3. 恰好装满初始化:别忘了 -INF。
  4. 多组数据:每次要清空 f 数组。

2.8 典型例题

  • 洛谷 P1048 采药:裸 01 背包
  • 洛谷 P1060 开心的金明:价值和体积互换
  • 洛谷 P1164 小 A 点菜:01 背包方案数
  • 洛谷 P1049 装箱问题:01 背包可行性

3. 完全背包

3.1 问题描述

有 n 种物品,每种物品有体积 w[i] 和价值 v[i]。每种物品可以选无限次。背包容量为 V,求最大总价值。

3.2 状态设计

和 01 背包一样:

f[j] = 容量为 j 时能获得的最大价值

3.3 转移

完全背包的转移和 01 背包只差一个循环方向:容量正序。

for (int i = 1; i <= n; i++)
    for (int j = w[i]; j <= V; j++)
        f[j] = max(f[j], f[j-w[i]] + v[i]);

为什么正序就变成完全背包?

当计算 f[j] 时,f[j-w[i]] 可能已经被第 i 种物品更新过。也就是说,f[j-w[i]] 里可能已经包含了若干个第 i 种物品。再加上一个,就相当于第 i 种物品被选了多次。

3.4 和 01 背包的对比

01 背包 完全背包
每件物品次数 最多 1 次 无限次
容量循环 逆序 V → w[i] 正序 w[i] → V
转移含义 用旧状态 用可能已更新的状态

3.5 初始化

和 01 背包一样:

  • 不超过:全 0
  • 恰好:f[0]=0,其他 -INF

3.6 完整模板

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 10000005;
long long f[MAXV];

int main() {
    int V, n;
    cin >> V >> n;
    for (int i = 1; i <= n; i++) {
        int w, v;
        cin >> w >> v;
        for (int j = w; j <= V; j++) {
            f[j] = max(f[j], f[j-w] + v);
        }
    }
    cout << f[V] << endl;
    return 0;
}

注意:完全背包的 V 可能很大(如 P1616 中 V=10^7),要用 long long。

3.7 常见坑

  1. 循环方向写反:完全背包必须正序,写成逆序就变成 01 背包。
  2. 数据范围:完全背包的 V 可能达到 10^7,数组要开大,用 long long。
  3. 和 01 背包混淆:记住口诀——01 逆序,完全正序。

3.8 典型例题

  • 洛谷 P1616 疯狂的采药:裸完全背包,V 很大
  • 洛谷 P2722 总分 Score Inflation:完全背包
  • 洛谷 P1853 投资的最大效益:完全背包 + 多次迭代

4. 多重背包

4.1 问题描述

有 n 种物品,每种物品有体积 w[i]、价值 v[i]、数量 c[i]。每种物品最多选 c[i] 次。背包容量为 V,求最大总价值。

4.2 朴素做法

把每种物品的 c[i] 个都当成独立的 01 背包物品,做 c[i] 次 01 背包。

复杂度:O(V * sum(c[i]))。如果 c[i] 很大(如 10^9),会超时。

4.3 二进制拆分优化

核心思想:把 c 个相同物品拆成若干个“捆绑包”,每个捆绑包的大小是 1, 2, 4, 8, ...,最后一个包是剩余数量。

例如 c = 13:

13 = 1 + 2 + 4 + 6

拆成 4 个包:1 个、2 个、4 个、6 个。用这些包可以组合出 0~13 任意数量。

证明:1, 2, 4 可以组合出 0~7;加上 6,可以组合出 6~13;合起来就是 0~13。

拆分代码:

int k = 1;
while (c >= k) {
    // 用 k 个物品做一个 01 背包
    zero_one(k * w, k * v);
    c -= k;
    k <<= 1;
}
if (c > 0) {
    zero_one(c * w, c * v);
}

复杂度:O(V * sum(log c[i])),大大优化。

4.4 完整模板

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 100005;
int f[MAXV];

void zero_one(int w, int v, int V) {
    for (int j = V; j >= w; j--)
        f[j] = max(f[j], f[j-w] + v);
}

int main() {
    int n, V;
    cin >> n >> V;
    for (int i = 1; i <= n; i++) {
        int w, v, c;
        cin >> w >> v >> c;
        int k = 1;
        while (c >= k) {
            zero_one(k * w, k * v, V);
            c -= k;
            k <<= 1;
        }
        if (c > 0)
            zero_one(c * w, c * v, V);
    }
    cout << f[V] << endl;
    return 0;
}

4.5 单调队列优化(进阶)

二进制拆分的复杂度是 O(V * sum(log c))。如果 c 很大但 n 也很大,还可以用单调队列优化到 O(nV)。

思路:对于每个物品,按容量模 w[i] 的余数分组,每组内用单调队列维护 f[j] - k*v 的最大值。

这个属于进阶内容,初学者先掌握二进制拆分即可。

4.6 常见坑

  1. 拆分时最后一个包:c 剩余部分要单独处理。
  2. 数组大小:二进制拆分后物品数量约为 sum(log c),数组要开够。
  3. 和完全背包区分:如果 c[i] * w[i] >= V,可以当成完全背包处理,减少拆分。

4.7 典型例题

  • 洛谷 P1776 宝物筛选:裸多重背包
  • 洛谷 P2347 砝码称重:多重背包可行性 + bitset
  • 洛谷 P1077 摆花:多重背包方案数

5. 混合背包

5.1 问题描述

有 n 种物品,每种物品可能是:

  • 只能用 1 次(01)
  • 只能用有限次(多重)
  • 能用无限次(完全)

求容量 V 下的最大价值。

5.2 处理方法

分类处理:

  • 01 物品:逆序 01 背包
  • 完全物品:正序完全背包
  • 多重物品:二进制拆分后做 01 背包

5.3 完整模板

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 1005;
int f[MAXV];

void zero_one(int w, int v, int V) {
    for (int j = V; j >= w; j--)
        f[j] = max(f[j], f[j-w] + v);
}

void complete(int w, int v, int V) {
    for (int j = w; j <= V; j++)
        f[j] = max(f[j], f[j-w] + v);
}

int main() {
    int n, V;
    cin >> n >> V;
    for (int i = 1; i <= n; i++) {
        int w, v, c;
        cin >> w >> v >> c;
        if (c == 1) {
            zero_one(w, v, V);
        } else if (c == 0) {
            // 假设 c=0 表示无限
            complete(w, v, V);
        } else {
            int k = 1;
            while (c >= k) {
                zero_one(k * w, k * v, V);
                c -= k;
                k <<= 1;
            }
            if (c > 0)
                zero_one(c * w, c * v, V);
        }
    }
    cout << f[V] << endl;
    return 0;
}

5.4 常见坑

  1. 类型判断:题目中 c=0 可能表示无限,也可能表示 0 个,要看题意。
  2. 完全背包和 01 背包的顺序:分类处理时不要混。
  3. 多重拆分的边界:同上。

5.5 典型例题

  • 洛谷 P1833 樱花:混合背包经典题
  • 洛谷 P1782 旅行商的背包:混合背包 + 泛化物品

6. 二维费用背包

6.1 问题描述

物品有两个费用维度,比如体积 w1[i] 和重量 w2[i]。背包有体积限制 V 和重量限制 M。每件物品有 1 个价值 v[i]。求最大价值。

6.2 状态设计

f[j][k] = 体积为 j、重量为 k 时的最大价值

6.3 01 二维费用

for (int i = 1; i <= n; i++)
    for (int j = V; j >= w1[i]; j--)
        for (int k = M; k >= w2[i]; k--)
            f[j][k] = max(f[j][k], f[j-w1[i]][k-w2[i]] + v[i]);

两个维度都逆序,因为这是 01 背包。

6.4 完全二维费用

两个维度都正序:

for (int i = 1; i <= n; i++)
    for (int j = w1[i]; j <= V; j++)
        for (int k = w2[i]; k <= M; k++)
            f[j][k] = max(f[j][k], f[j-w1[i]][k-w2[i]] + v[i]);

6.5 多重二维费用

二进制拆分后按 01 二维费用处理。

6.6 完整模板

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 1005;
const int MAXM = 1005;
int f[MAXV][MAXM];

int main() {
    int n, V, M;
    cin >> n >> V >> M;
    for (int i = 1; i <= n; i++) {
        int w1, w2, v;
        cin >> w1 >> w2 >> v;
        for (int j = V; j >= w1; j--)
            for (int k = M; k >= w2; k--)
                f[j][k] = max(f[j][k], f[j-w1][k-w2] + v);
    }
    cout << f[V][M] << endl;
    return 0;
}

6.7 常见坑

  1. 两个维度都要逆序:01 二维费用两个维度都逆序。
  2. 空间:二维数组可能很大,注意内存。
  3. 和分组背包混淆:二维费用是同一件物品有两个费用,分组背包是每组选一个。

6.8 典型例题

  • 洛谷 P1855 榨取 kkksc03:二维费用 01 背包
  • 洛谷 P1507 NASA 的食物计划:二维费用 01 背包
  • 洛谷 P1759 通天之潜水:二维费用 + 输出方案

7. 分组背包

7.1 问题描述

有 n 件物品,分成若干组。每组内的物品最多选一个。背包容量为 V,求最大价值。

7.2 状态设计

f[j] = 容量为 j 时的最大价值

7.3 转移

外层枚举组,中层枚举容量(逆序),内层枚举组内物品:

for (每组 g) {
    for (int j = V; j >= 0; j--) {
        for (auto [w, v] : g) {
            if (j >= w)
                f[j] = max(f[j], f[j-w] + v);
        }
    }
}

为什么容量要逆序?因为每组最多选一个,逆序保证每组只被选一次。

为什么组内枚举顺序无所谓?因为组内物品互斥,f[j-w] 用的是上一组的状态。

7.4 完整模板

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 1005;
int f[MAXV];
vector<pair<int,int>> groups[105];

int main() {
    int V, n;
    cin >> V >> n;
    int maxg = 0;
    for (int i = 1; i <= n; i++) {
        int w, v, g;
        cin >> w >> v >> g;
        groups[g].push_back({w, v});
        maxg = max(maxg, g);
    }
    for (int g = 1; g <= maxg; g++) {
        for (int j = V; j >= 0; j--) {
            for (auto [w, v] : groups[g]) {
                if (j >= w)
                    f[j] = max(f[j], f[j-w] + v);
            }
        }
    }
    cout << f[V] << endl;
    return 0;
}

7.5 常见坑

  1. 容量逆序:分组背包必须逆序,否则每组可能选多个。
  2. 组内物品枚举顺序:无所谓,因为互斥。
  3. 空组:有些组可能没有物品,要处理。

7.6 典型例题

  • 洛谷 P1757 通天之分组背包:裸分组背包
  • 洛谷 P1064 金明的预算方案:有依赖的分组背包
  • 洛谷 P1541 乌龟棋:多维 DP,类似分组

8. 有依赖的背包

8.1 问题描述

物品之间有依赖关系。比如选附件必须先选主件,选课必须先修先修课。

8.2 分类

  1. 依赖关系是森林:每个物品最多依赖一个物品,形成树。
  2. 依赖关系是 DAG:一般较复杂,竞赛中较少。
  3. 主件-附件模型:一个主件可以有多个附件,附件不再有附件。

8.3 主件-附件模型

对于每个主件,枚举它的附件组合:

  • 不选主件
  • 选主件
  • 选主件 + 附件 1
  • 选主件 + 附件 2
  • 选主件 + 附件 1 + 附件 2
  • ...

把每种组合看成一个“组”,做分组背包。

8.4 完整模板(主件-附件)

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 32005;
int f[MAXV];

int main() {
    int V, n;
    cin >> V >> n;
    vector<int> w(n+1), v(n+1), belong(n+1, 0);
    vector<vector<int>> attach(n+1);
    for (int i = 1; i <= n; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        w[i] = a;
        v[i] = a * b;
        if (c == 0) {
            belong[i] = i;
        } else {
            belong[i] = c;
            attach[c].push_back(i);
        }
    }
    for (int i = 1; i <= n; i++) {
        if (belong[i] != i) continue;
        // 枚举主件 i 的附件组合
        vector<pair<int,int>> options;
        int sz = attach[i].size();
        for (int mask = 0; mask < (1 << sz); mask++) {
            int tw = w[i], tv = v[i];
            for (int k = 0; k < sz; k++) {
                if (mask & (1 << k)) {
                    tw += w[attach[i][k]];
                    tv += v[attach[i][k]];
                }
            }
            options.push_back({tw, tv});
        }
        // 分组背包
        for (int j = V; j >= 0; j--) {
            for (auto [tw, tv] : options) {
                if (j >= tw)
                    f[j] = max(f[j], f[j-tw] + tv);
            }
        }
    }
    cout << f[V] << endl;
    return 0;
}

8.5 常见坑

  1. 附件数量:如果附件很多,2^sz 会爆炸。一般题目附件不超过 2 个。
  2. 归属关系:要正确处理主件和附件的编号。
  3. 价值计算:题目可能给的是价格和重要度,价值要相乘。

8.6 典型例题

  • 洛谷 P1064 金明的预算方案:经典主件-附件
  • 洛谷 P2014 选课:树形背包

9. 树形背包

9.1 问题描述

物品形成一棵树,选某个物品必须先选它的父物品。求容量 V 下的最大价值。

9.2 状态设计

dp[u][j] = 在 u 的子树中,且选了 u,容量为 j 时的最大价值

9.3 转移

对每个子节点 v,把 v 的子树合并到 u:

void dfs(int u) {
    dp[u][w[u]] = v[u];
    for (int v : g[u]) {
        dfs(v);
        for (int j = V; j >= w[u]; j--) {
            for (int k = 0; k <= j - w[u]; k++) {
                dp[u][j] = max(dp[u][j], dp[u][j-k] + dp[v][k]);
            }
        }
    }
}

其中 dp[v][0] = 0 表示不选 v 这棵子树。

9.4 森林处理

如果有多棵树,加一个虚拟根 0,所有树的根都连到 0。虚拟根的 w=0, v=0。

9.5 完整模板

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 305;
const int MAXV = 305;
int n, V;
int w[MAXN], v[MAXN];
vector<int> g[MAXN];
int dp[MAXN][MAXV];

void dfs(int u) {
    for (int j = w[u]; j <= V; j++)
        dp[u][j] = v[u];
    for (int v : g[u]) {
        dfs(v);
        for (int j = V; j >= w[u]; j--) {
            for (int k = 0; k <= j - w[u]; k++) {
                dp[u][j] = max(dp[u][j], dp[u][j-k] + dp[v][k]);
            }
        }
    }
}

int main() {
    cin >> n >> V;
    for (int i = 1; i <= n; i++) {
        int p;
        cin >> w[i] >> v[i] >> p;
        if (p == 0) g[0].push_back(i);
        else g[p].push_back(i);
    }
    dfs(0);
    cout << dp[0][V] << endl;
    return 0;
}

9.6 复杂度优化

朴素树形背包是 O(nV^2)。可以用上下界优化:

  • 上界:当前子树大小
  • 下界:w[u]

这样复杂度可以降到 O(nV)。

9.7 常见坑

  1. 虚拟根:森林要加虚拟根。
  2. 初始化:dp[u][j] 初始为 v[u],表示选了 u。
  3. 循环边界:j >= w[u],k <= j - w[u]。

9.8 典型例题

  • 洛谷 P2014 选课:树形背包经典
  • 洛谷 P2015 二叉苹果树:树形背包
  • 洛谷 P1273 有线电视网:树形背包 + 费用

10. 背包方案数

10.1 问题描述

求装满容量 V 的方案数,或者不超过容量 V 的方案数。

10.2 01 背包方案数

f[0] = 1;
for (int i = 1; i <= n; i++)
    for (int j = V; j >= w[i]; j--)
        f[j] += f[j-w[i]];

答案:f[V](恰好)或 sum(f[0..V])(不超过)。

10.3 完全背包方案数

f[0] = 1;
for (int i = 1; i <= n; i++)
    for (int j = w[i]; j <= V; j++)
        f[j] += f[j-w[i]];

10.4 多重背包方案数

二进制拆分后做 01 背包方案数。

10.5 完整模板

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 10005;
int f[MAXV];

int main() {
    int n, V;
    cin >> n >> V;
    f[0] = 1;
    for (int i = 1; i <= n; i++) {
        int w;
        cin >> w;
        for (int j = V; j >= w; j--)
            f[j] += f[j-w];
    }
    cout << f[V] << endl;
    return 0;
}

10.6 常见坑

  1. 初始化:f[0]=1,其他 0。
  2. 模数:方案数可能很大,要取模。
  3. 恰好 vs 不超过:答案不同。

10.7 典型例题

  • 洛谷 P1164 小 A 点菜:01 背包方案数
  • 洛谷 P1077 摆花:多重背包方案数
  • 洛谷 P1832 A+B Problem:完全背包方案数

11. 输出具体方案

11.1 问题描述

不仅求最大价值,还要输出选了哪些物品。

11.2 二维记录法

开二维数组 dp[i][j] 和 choose[i][j]:

for (int i = 1; i <= n; i++) {
    for (int j = 0; j <= V; j++) {
        dp[i][j] = dp[i-1][j];
        if (j >= w[i] && dp[i-1][j-w[i]] + v[i] > dp[i][j]) {
            dp[i][j] = dp[i-1][j-w[i]] + v[i];
            choose[i][j] = true;
        }
    }
}

倒推:

int j = V;
for (int i = n; i >= 1; i--) {
    if (choose[i][j]) {
        cout << i << " ";
        j -= w[i];
    }
}

11.3 一维 + 记录法

如果只用一维,可以记录每个状态是从哪里转移来的:

int pre[MAXV]; // pre[j] 表示容量 j 是由哪个物品转移来的

但这样只能记录最后一个物品,要完整方案还是二维方便。

11.4 字典序最小方案

如果要求字典序最小,经典技巧:

  1. 物品倒序做 01 背包
  2. 从前往后贪心判断能不能选

具体:先倒序做一遍 DP,然后从第 1 个物品开始,如果 f[j] == f[j-w[i]] + v[i],就选它。

11.5 典型例题

  • 洛谷 P1759 通天之潜水:二维费用 + 输出方案
  • 洛谷 P1064 金明的预算方案:输出方案

12. 第 k 优解

12.1 问题描述

求容量为 V 时的第 1 到第 k 优价值。

12.2 状态设计

f[j][1..K] = 容量 j 的前 K 优解

12.3 转移

对每个物品,把 f[j] 和 f[j-w]+v 两个有序数组合并,取前 K 个。

01 背包逆序,完全背包正序。

12.4 完整模板

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 5005;
const int MAXK = 55;
int f[MAXV][MAXK];
int a[MAXK], b[MAXK], c[MAXK];

int main() {
    int K, V, n;
    cin >> K >> V >> n;
    memset(f, 0, sizeof(f));
    for (int i = 1; i <= n; i++) {
        int w, v;
        cin >> w >> v;
        for (int j = V; j >= w; j--) {
            // 合并 f[j] 和 f[j-w]+v
            for (int k = 1; k <= K; k++) {
                a[k] = f[j][k];
                b[k] = f[j-w][k] + v;
            }
            int x = 1, y = 1, z = 1;
            while (z <= K && (x <= K || y <= K)) {
                if (x <= K && (y > K || a[x] > b[y])) {
                    c[z++] = a[x++];
                } else {
                    c[z++] = b[y++];
                }
            }
            for (int k = 1; k <= K; k++)
                f[j][k] = c[k];
        }
    }
    int ans = 0;
    for (int k = 1; k <= K; k++)
        ans += f[V][k];
    cout << ans << endl;
    return 0;
}

12.5 常见坑

  1. 合并时去重:如果 a[x] == b[y],只取一个。
  2. 初始化:f[0][1]=0,其他 -INF。
  3. 复杂度:O(nVK)。

12.6 典型例题

  • 洛谷 P1858 多人背包:第 k 优解经典
  • POJ 3181 Dollar Dayz:方案数 + 高精度

13. 可行性背包与 bitset 优化

13.1 问题描述

只问“能不能凑出某个容量”,不问最大价值。

13.2 bool 数组写法

bool f[MAXV];
f[0] = true;
for (int i = 1; i <= n; i++)
    for (int j = V; j >= w[i]; j--)
        f[j] = f[j] || f[j-w[i]];

13.3 bitset 优化

bitset<MAXV> f;
f[0] = 1;
for (int i = 1; i <= n; i++)
    f |= (f << w[i]);

复杂度除以 64,非常快。

13.4 多重背包可行性

二进制拆分后做 01 可行性:

bitset<MAXV> f;
f[0] = 1;
for (int i = 1; i <= n; i++) {
    int k = 1, c = cnt[i];
    while (c >= k) {
        f |= (f << (k * w[i]));
        c -= k;
        k <<= 1;
    }
    if (c) f |= (f << (c * w[i]));
}

13.5 典型例题

  • 洛谷 P2347 砝码称重:多重背包可行性
  • 洛谷 P1508 Likecloud-吃、吃、吃:可行性

14. 恰好、不超过、至少

14.1 恰好装满

  • 初始化:f[0]=0,其他 -INF
  • 答案:f[V]
  • 如果 f[V] = -INF,无解

14.2 不超过容量

  • 初始化:f[0..V]=0
  • 答案:f[V]

14.3 至少容量

方法一:扩大状态,把超过 V 的都当成 V。

for (int j = V; j >= w[i]; j--) {
    int nj = min(V, j + w[i]);
    f[nj] = max(f[nj], f[j] + v[i]);
}

方法二:定义 f[j] 为至少 j 的最大价值,转移时 min(V, j+w[i])。

14.4 方案数恰好 vs 不超过

  • 恰好:f[0]=1,答案 f[V]
  • 不超过:f[0]=1,答案 sum(f[0..V])

14.5 典型例题

  • 洛谷 P1049 装箱问题:不超过
  • 洛谷 P1164 小 A 点菜:恰好方案数
  • 洛谷 P1510 精卫填海:至少

15. 泛化物品背包

15.1 问题描述

物品的价值不是固定值,而是关于容量的函数 h(w)。

15.2 合并两个泛化物品

for (int j = 0; j <= V; j++)
    for (int k = 0; k <= j; k++)
        f[j] = max(f[j], h1[k] + h2[j-k]);

15.3 典型应用

  • 物品价值随容量变化
  • 多个物品组合成一个泛化物品

15.4 典型例题

  • 洛谷 P1782 旅行商的背包:泛化物品

16. 总结与刷题路线

16.1 类型总结表

类型 限制 核心 复杂度
01 背包 每件 1 次 容量逆序 O(nV)
完全背包 每件无限 容量正序 O(nV)
多重背包 每件 c 次 二进制拆分 O(V∑log c)
混合背包 混合 分类处理 同上
二维费用 两个限制 二维逆/正序 O(nVM)
分组背包 每组最多 1 个 组外容量逆序 O(V∑组内物品)
树形背包 选子先选父 树形 DP 合并 O(nV²)
方案数 计数 加法转移 O(nV)
输出方案 具体选择 记录 + 倒推 O(nV)
第 k 优 前 k 优 有序表合并 O(nVK)
可行性 能否凑出 bool / bitset O(nV/64)

16.2 刷题路线

入门:

  1. P1048 采药(01)
  2. P1616 疯狂的采药(完全)
  3. P1164 小 A 点菜(方案数)

进阶:

  1. P1776 宝物筛选(多重)
  2. P1833 樱花(混合)
  3. P1757 通天之分组背包(分组)
  4. P1064 金明的预算方案(依赖)

提高:

  1. P2014 选课(树形)
  2. P1858 多人背包(第 k 优)
  3. P2347 砝码称重(bitset)

综合:

  1. P1541 乌龟棋(多维 DP)
  2. P1782 旅行商的背包(泛化)

16.3 做题口诀

先看每件能用几次,再看有几个费用维度,再看有没有分组依赖,最后看求最大、方案数、可行性还是具体方案。

01 逆序,完全正序,分组逆序,树形合并。

16.4 最后的话

背包问题是动态规划的入门,也是竞赛中的常客。掌握这 15 种类型,你就能应对绝大多数背包题。

不要死记代码,要理解为什么这样转移。每道题都问自己:

  • 状态是什么?
  • 转移从哪来?
  • 循环方向为什么是这样?
  • 初始化为什么是这样?
posted @ 2026-10-11 11:07  Tri_Function  阅读(15)  评论(0)    收藏  举报