OneCoder Avatar
OneCodercoderli.com · 936 篇博文
六级

C++ 算法考级专栏

真题分析、矩阵探测、递归回溯与基础语法

🎨 视觉封面

【GESP】C++六级练习 luogu-B2173, 多重背包

📅 2026-07-13·✍️ OneCoder·计算中...·⏱️ 17 分钟
#GESP#C++#动态规划#背包DP

GESP C++六级练习。多重背包DP模板题,是在 01 背包和完全背包基础上的进阶。每种物品有有限的件数限制,介于"最多1件"和"无限件"之间,需要掌握朴素枚举和二进制拆分两种解法。难度⭐⭐。洛谷难度等级普及-

B2173 多重背包

题目要求

题目描述

你有一个容量为 VV 的背包,以及 nn 种物品。第 ii 种物品的体积为 wiw_i,每件价值为 valival_i,最多有 cic_i 件。

你可以选择每种物品若干件放入背包,但同一种物品选择的件数不能超过 cic_i,且总容量不能超过 VV

请你求出在不超过背包容量的前提下,能获得的最大总价值。

形式化题意:设第 ii 种物品选择 xix_i 件,则需要满足: i=1nxiwiV,0xici\sum_{i=1}^{n} x_i \cdot w_i \le V,\quad 0 \le x_i \le c_i 最大化目标: maxi=1nxivali\max \sum_{i=1}^{n} x_i \cdot val_i

输入格式

第一行两个整数 n,Vn, V,表示物品种类数与背包容量。

接下来 nn 行,每行三个整数 wi,vali,ciw_i, val_i, c_i,分别表示第 ii 种物品的体积、价值与最多件数。

输出格式

输出一个整数,表示最大总价值。

输入输出样例 #1

输入 #1
PLAINTEXT
3 10
3 4 2
4 5 3
2 3 4
输出 #1
PLAINTEXT
14

说明/提示

样例解释 #1

一种可行的最优方案是:

  • 选第 1 种物品 22 件:体积 2×3=62 \times 3 = 6,价值 2×4=82 \times 4 = 8
  • 选第 3 种物品 22 件:体积 2×2=42 \times 2 = 4,价值 2×3=62 \times 3 = 6

总容量 6+4=10106 + 4 = 10 \le 10,总价值 8+6=148 + 6 = 14,为最大值。

数据范围

对于 40%40\% 的数据,1n91\le n \le 91V10001\le V \le 10001ci51\le c_i \le 5

对于 100%100\% 的数据,1n5001\le n \le 5001V10001\le V \le 10001ci1001\le c_i \le 100


题目分析

本题是多重背包问题的标准模板题。如果你已经掌握了 01 背包(P1048 采药)和完全背包(B2174),那么多重背包就是两者之间的"中间地带":

背包类型 每种物品可选数量 一维 DP 遍历方式
01 背包 最多 11 逆序遍历容量
多重背包 最多 cic_i 需要特殊处理
完全背包 无限件 正序遍历容量

多重背包的核心难点在于:每种物品既不是只有 1 件,也不是无限件,而是有一个上限 cic_i。不能简单地套用 01 背包或完全背包的遍历方向,需要额外处理件数限制。

1. 解法一:朴素三重循环(直接枚举件数)

最直观的思路是:对每种物品,枚举选 0 件、1 件、2 件……直到 cic_i,在所有合法方案中取最大值。

状态定义

dp[j]dp[j] 表示:背包容量为 jj 时,能获得的最大总价值。

状态转移方程

对于第 ii 种物品(体积 wiw_i,价值 valival_i,最多 cic_i 件),对每个容量 jj(从 VVwiw_i逆序):

dp[j]=max0kci, kwij{dp[jkwi]+kvali}dp[j] = \max_{0 \le k \le c_i,\ k \cdot w_i \le j}\{dp[j - k \cdot w_i] + k \cdot val_i\}

其中 kk 表示第 ii 种物品选取的件数。

为什么要逆序遍历?

这里使用的是逆序遍历(和 01 背包一样)。原因是:我们在最内层循环中手动枚举了件数 kk,已经完整地考虑了"选 0 到 cic_i 件"的所有情况。因此需要确保 dp[jkwi]dp[j - k \cdot w_i] 读到的是上一轮(尚未考虑第 ii 种物品时)的旧值,否则同一种物品会被多算。

换句话说:逆序遍历保证了"选几件"的决策由我们的 kk 循环精确控制,而不会因为正序遍历产生的"叠加效应"而失控。

复杂度分析
  • 时间复杂度O(n×V×max(ci))O(n \times V \times \max(c_i))。三层循环:nn 种物品 × 容量 VV × 最多 cic_i
  • 空间复杂度O(V)O(V),一维滚动数组

本题数据范围:n500,V1000,ci100n \le 500, V \le 1000, c_i \le 100,最坏约 5×1075 \times 10^7,在时间限制内可以通过。

2. 解法二:二进制拆分优化

朴素解法在数据范围更大时可能超时。二进制拆分是多重背包的经典优化技巧,核心思想是:cic_i 件相同物品拆分成若干"捆",每捆是 1、2、4、8……件,然后对这些"捆"做 01 背包

为什么二进制拆分是正确的?

任意一个非负整数都可以用若干个不重复的 2 的幂次之和来表示(这就是二进制表示的本质)。例如 ci=13c_i = 13

13=1+2+4+613 = 1 + 2 + 4 + 6

我们把 13 件物品拆分成 4 捆:1 件、2 件、4 件、6 件(最后一捆是余数 13124=613 - 1 - 2 - 4 = 6)。

对于每一捆,我们只需要决定"选"或"不选"(01 背包),而通过这 4 捆的不同选取组合,可以精确地表示 001313 之间的任意件数

选取的捆 总件数
00
{1}\{1\} 11
{2}\{2\} 22
{1,2}\{1, 2\} 33
{4}\{4\} 44
{1,4}\{1, 4\} 55
{2,4}\{2, 4\} 66
{1,2,4}\{1, 2, 4\} 77
{6}\{6\} 66
……(更多组合) 8138 \sim 13

这样,原本需要枚举 cic_i 次的内层循环,被压缩到了 log2ci+1\lfloor \log_2 c_i \rfloor + 1 个"虚拟物品"的 01 背包,大幅减少计算量。

拆分规则

对于 cic_i 件物品,按如下方式拆分:

  1. 依次拆出 1,2,4,8,1, 2, 4, 8, \ldots 件的捆,直到剩余件数不足下一个 2 的幂次
  2. 把剩余件数作为最后一捆

例如 ci=10c_i = 10:拆分为 1,2,4,31, 2, 4, 3(因为 1+2+4=71 + 2 + 4 = 7,剩余 107=310 - 7 = 3)。

每一捆的体积和价值相应地乘以捆内件数,然后作为一个独立的"虚拟物品"参与 01 背包。

复杂度分析
  • 时间复杂度O(V×log2ci)O(V \times \sum \log_2 c_i),远优于朴素解法
  • 空间复杂度O(V)O(V)

样例验证

样例输入:n=3,V=10n = 3, V = 10

物品编号 体积 ww 价值 valval 最多 cc
11 33 44 22
22 44 55 33
33 22 33 44

以朴素解法为例,处理完所有物品后 DP 数组的最终结果:

jj 00 11 22 33 44 55 66 77 88 99 1010
dp[j]dp[j] 00 00 33 44 66 77 88 99 1111 1212 1414

最终 dp[10]=14dp[10] = 14,对应选择物品 1122 件(体积 66,价值 88)+ 物品 3322 件(体积 44,价值 66),与样例输出一致。


示例代码

方法一:朴素三重循环

通过逆序遍历容量 + 枚举件数 kk,直接实现多重背包。思路最直观,适合初学者理解。

CPP
#include <iostream>
#include <algorithm>

// dp[j] 表示背包容量为 j 时能获得的最大总价值
int dp[1005];

int main() {
    int n, V;
    std::cin >> n >> V;

    // 外层循环:枚举每一种物品
    for (int i = 0; i < n; i++) {
        int w, val, c;
        std::cin >> w >> val >> c;

        // 中层循环:逆序遍历容量(与 01 背包相同)
        // 逆序保证 dp[j - k*w] 读到的是上一轮的旧值
        for (int j = V; j >= w; j--) {
            // 内层循环:枚举第 i 种物品选取的件数 k(从 1 到 c)
            // k=0 对应"不选",dp[j] 保持不变,无需显式处理
            for (int k = 1; k <= c && k * w <= j; k++) {
                dp[j] = std::max(dp[j], dp[j - k * w] + k * val);
            }
        }
    }

    // dp[V] 即为背包容量为 V 时的最大总价值
    std::cout << dp[V] << std::endl;
    return 0;
}

方法二:二进制拆分优化

将每种物品的 cic_i 件按二进制拆分为若干"捆",转化为 01 背包问题求解。在数据范围较大时效率更优。

CPP
#include <iostream>
#include <algorithm>

// dp[j] 表示背包容量为 j 时能获得的最大总价值
int dp[1005];

int main() {
    int n, V;
    std::cin >> n >> V;

    for (int i = 0; i < n; i++) {
        int w, val, c;
        std::cin >> w >> val >> c;

        // 二进制拆分:将 c 件物品拆分为若干"捆"
        // 每捆的件数依次为 1, 2, 4, 8, ...,最后一捆为余数
        int rest = c; // 剩余待拆分的件数
        for (int bundle = 1; rest > 0; bundle *= 2) {
            // 当前这捆的件数:取 bundle 和剩余件数的较小值
            int cnt = std::min(bundle, rest);
            rest -= cnt;

            // 当前这捆的总体积和总价值
            int bw = cnt * w;
            int bv = cnt * val;

            // 对这一捆做 01 背包(逆序遍历容量)
            for (int j = V; j >= bw; j--) {
                dp[j] = std::max(dp[j], dp[j - bw] + bv);
            }
        }
    }

    std::cout << dp[V] << std::endl;
    return 0;
}

三种背包的代码对比

将 01 背包、完全背包和多重背包的一维 DP 核心代码放在一起,差异一目了然:

CPP
// === 01 背包:每种物品最多选 1 件 ===
for (int j = V; j >= w; j--) {                  // 逆序
    dp[j] = max(dp[j], dp[j - w] + val);
}

// === 完全背包:每种物品可选无限件 ===
for (int j = w; j <= V; j++) {                   // 正序
    dp[j] = max(dp[j], dp[j - w] + val);
}

// === 多重背包(朴素):每种物品最多选 c 件 ===
for (int j = V; j >= w; j--) {                   // 逆序
    for (int k = 1; k <= c && k * w <= j; k++) { // 枚举件数
        dp[j] = max(dp[j], dp[j - k * w] + k * val);
    }
}

规律总结

  • 01 背包:逆序遍历,天然保证每种物品只选 1 次
  • 完全背包:正序遍历,天然允许无限次叠加
  • 多重背包:逆序遍历(防止叠加)+ 手动枚举件数(精确控制上限)

拓展思考

掌握了 01 背包、完全背包和多重背包这三大基础模型后,可以从以下方向继续深入:

推荐练习

题目 类型 要点
P1048 采药 01 背包 逆序遍历,每件物品最多选一次
B2174 完全背包 完全背包 正序遍历,每种物品可选无限件
B2173 多重背包(本题) 多重背包 朴素枚举或二进制拆分
P5662 纪念品 完全背包应用 多天交易转化为每日独立的完全背包
P17012 条形蛋糕 一维 DP / 完全背包 切割问题,完全背包的等价建模

进阶方向

  • 混合背包:同一题中可能混合出现 01、完全、多重三种类型的物品,需要根据物品类型选择不同的处理方式
  • 分组背包:物品分为若干组,每组至多选一件,外层枚举组、中层逆序遍历容量、内层枚举组内物品
  • 二维费用背包:每件物品有两种费用(如体积和重量),需要同时满足两个约束条件

💡 OneCoder 资源指引

所有代码开源上传至 GitHub:yummy-code 仓库 · GESP 专题站:GESP WIKI

🤝 技术交流与答疑

欢迎加入:C++ GESP/CSP 考级答疑群(688906745)Java/Python群(982860385),群内日常讨论,互帮互助。

📚

猜你想读 · 相关文章推荐

OneCoder

OneCoder (lihongzheshuai)

一个中年人的自留地,记录学习 C++、GESP/NOI、Java、Python 与算法架构的心得体会。本站唯一网址:coderli.com

💬 读者留言与交流

0 条讨论
✨ 支持 Markdown 语法格式
还没有留言,快来成为第一个讨论者吧!