分组背包变式:每组最多选 $d_i$ 件

本篇对应源码见仓库 solutions/xcpc/algorithm/dp/knapsack-group/。

本篇从思想到代码,分步讲解「每组最多选 \(d_i\) 件物品」的分组背包如何做,然后给出两个示例习题(一个手写小例子+一个经典“选课问题”)进行巩固。


一、问题抽象与 DP 状态

  • 全局约束

    • 容量上限:背包总容量为 \(W\)。
  • 分组约束

    • 物品被分成 \(m\) 组,第 \(i\) 组有若干件物品,每件有重量 \(w_{i,t}\)、价值 \(v_{i,t}\)。

    • 每组最多可以选 \(d_i\) 件物品。

我们用 \(\mathrm{dp}[j]\) 表示当前处理完若干组后,在容量恰好为 \(j\) 时能取得的最大价值。

为处理组内「最多选 \(d_i\) 件」的约束,我们引入组内的临时二维数组

\(\mathrm{cur}[j][k]\)

— 表示在“当前组”中,已用容量 \(j\),且已选了 \(k\) 件(\(0\le k\le d_i\))时,能在“前面各组”基础上再能获得的最大附加价值。

组间转移大框架

初始化:dp[j](处理完第 i−1 组后的结果)

—— 处理第 i 组 ——
1. 构造 cur[j][k],并设所有 cur[j][k]=-∞,再令 cur[j][0]=dp[j]
2. 枚举本组每件物品 (w,v):
     ∀ j=W…w ↓, ∀ k=1…d_i ↑:
         cur[j][k] = max(cur[j][k], cur[j−w][k−1] + v)
3. 合并到全局:
     ∀ j=0…W:
         dp[j] = max_{0≤k≤d_i} cur[j][k]
  • 步骤 1:把“选 0 件” 时的价值先搬进来。

  • 步骤 2:相当于在组内做一次「0-1 背包 + 计数限制」的 DP。

  • 步骤 3:枚举该组选 \(k\) 件的所有可能性,把最优值更新回去。


二、C++ 代码模板

#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9;

struct Item { int w, v; };
int main(){
    int m, W;
    // m 组,W 容量
    cin >> m >> W;

    vector<vector<Item>> groups(m);
    vector<int> d(m);
    for(int i = 0; i < m; i++){
        int cnt;
        cin >> cnt >> d[i];
        // cnt: 第 i 组物品数, d[i]: 最多选件数
        groups[i].resize(cnt);
        for(int t = 0; t < cnt; t++){
            cin >> groups[i][t].w >> groups[i][t].v;
        }
    }

    vector<int> dp(W+1, 0);
    // 组间循环
    for(int i = 0; i < m; i++){
        // cur[j][k]: 本组容量 j,选了 k 件时的附加价值
        vector<vector<int>> cur(W+1, vector<int>(d[i]+1, -INF));
        // 选 0 件 -> 继承上一组结果
        for(int j = 0; j <= W; j++)
            cur[j][0] = dp[j];

        // 组内枚举每件物品(0-1)
        for(auto &it : groups[i]){
            int wt = it.w, vt = it.v;
            for(int j = W; j >= wt; j--){
                for(int k = 1; k <= d[i]; k++){
                    if(cur[j-wt][k-1] > -INF)
                        cur[j][k] = max(cur[j][k], cur[j-wt][k-1] + vt);
                }
            }
        }
        // 合并回 dp
        for(int j = 0; j <= W; j++){
            int best = cur[j][0];
            for(int k = 1; k <= d[i]; k++)
                best = max(best, cur[j][k]);
            dp[j] = best;
        }
    }

    // 答案:恰好装满 W 时的最大价值
    cout << dp[W] << "\n";
    return 0;
}

三、示例 1:手写小例子

  • 容量 \(W=5\)

  • 一组,最多选 \(d_0=2\) 件

  • 物品列表:

    1. 重量 2,价值 3

    2. 重量 3,价值 4

    3. 重量 4,价值 5

手算思路:

选 0 件 选 1 件 选 2 件
重量 0,价值 0 (2,3)、(3,4)、(4,5) 三种 (2+3, 3+4)= (5,7)
  • 容量 5 时,

    • 选 0 件 → 价值 0

    • 选 1 件 → 最优是选(4,5) → 5

    • 选 2 件 → 只能选(2,3)+(3,4) → 7

  • 最终答案 = \(\max(0,5,7)=\boxed{7}\)。


四、示例 2:NOIP2012 提高组「选课问题」

题目

某校要从 \(m\) 个学期的若干门课程中选课。第 \(i\) 学期有 \(c_i\) 门可选课程,每门课程占用学分 \(w_{i,t}\),学习收益 \(v_{i,t}\)。

全校总学分限额不超过 \(W\),并且每学期最多只能选 \(K\) 门课程。求最高学习收益。

  • 这是上述模型的典型应用:

    • 组数 \(m\)

    • 容量 \(W\)

    • 每组物品数 \(c_i\)

    • 每组限选件数 \(d_i\equiv K\)

只要把上面模板中 d[i] 都设为 \(K\),再读入各学期的课程列表,就能打出答案。

// 读入 m, W, K
int m, W, K;
cin >> m >> W >> K;

vector<vector<Item>> course(m);
vector<int> d(m, K);
for(int i = 0; i < m; i++){
    int ci; cin >> ci;
    course[i].resize(ci);
    for(int j = 0; j < ci; j++){
        cin >> course[i][j].w >> course[i][j].v;
    }
}
// 接下来用上面“组间、组内 DP”模板即可

小结

  1. 多选限制:引入“组内已选件数”维度 \(k\)。

  2. 组内 DP:对每组做一次「带计数的 0-1 背包」。

  3. 组间合并:枚举选 \(k\le d_i\) 件后的最优值,更新全局。

这样,任何「每组最多选 \(d_i\) 件」的变形,都能被同一个框架解决。希望通过这两道练习,你能彻底搞懂并灵活运用!如果还有疑问或想要更多变形题,随时告诉我。

为什么 \(cur\) 初始化为-INF?而不是0?

在组内 DP 里,cur[j][k] 的含义是“在前面组的基础上,当前组恰好用 j 容量且选了 k 件物品时,能额外拿到的最大价值”。

  1. 标记「不可达」状态

    • 对于 k>0 而还没放任何物品的情况,显然是不可达的——你不可能在本组里选 1 件但又没放任何东西。

    • 我们用一个非常小的数(-INF)来表示“这个状态根本不可取”;在后续的取 max() 时,不可达状态就永远不会胜出。

  2. 如果初始化成 0 会怎样?

    • 那么所有 (j,k) 都至少是 0,就算是根本不可能选出的组合,也会被误当成“选了几件但价值为 0”──这会让后面合并回全局 dp 时,把「不合法的选项」当成合法选项,污染答案。
  3. cur[j][0] 例外

    • 选 0 件时是合法的:cur[j][0] = dp[j],表示「这一组不拿任何东西」,直接继承上次组间的最优。

简而言之,-INF 就是告诉算法“这里根本到达不了”,保证只有真正可行的“放了哪些物品”组合才会参与到后面的取优里。

posted @ 2026-11-11 06:21  slowbyte  阅读(2)  评论(0)    收藏  举报