把主件与它的至多两个附件组成家族,家族内枚举全部合法买法压成选项,家族间做一维倒序的分组背包,在预算内最大化价格与重要度乘积之和。
OJ: roj
题目 ID: 3549
难度:普及+/提高-
标签:动态规划背包分组背包python
创建: 2026-10-02 06:56
更新: 2026-10-10 11:30
形式化题目
给定总钱数
求一个总花费
样例输入
| 物品 | 价格 |
重要度 |
归属 | |
|---|---|---|---|---|
| 1 | 800 | 2 | 主件 | 1600 |
| 2 | 400 | 5 | 附件 |
2000 |
| 3 | 300 | 5 | 附件 |
1500 |
| 4 | 400 | 3 | 主件 | 1200 |
| 5 | 500 | 2 | 主件 | 1000 |
物品 1、2、3 因依赖关系绑在一起:买 2 或 3 就必须买 1。样例的最优解是购买物品 4 和 5,总花费 2200。
正解
思路
朴素做法及其瓶颈。 最直接的两个念头都不行:
- 枚举全部
个物品子集,逐个过滤依赖与预算—— ,指数级不可接受; - 把每件物品当成独立物品直接做 0-1 背包——花费、收益维度都对,但依赖被丢掉了,会选出"只买物品 2、3 不买物品 1"的非法方案;若给状态补上"哪些主件已买",维度又是指数级。
可见瓶颈只有一处:"买附件必须先买主件"这条依赖。再看依赖的形状——附件只依赖自己的主件,主件与主件之间、家族与家族之间没有任何依赖。于是把每个主件和它的至多两个附件看成一个家族:
- 家族内部最多 3 件物品,"主件必买、附件逐件决定买不买"的合法买法恰好只有
种; - 每种买法都可以把多件物品压成一个复合选项(总花费
,总收益 )——依赖关系被枚举方式天然满足,不必再进状态; - 各家族之间只剩"至多选一种买法(或整组不买)"的互斥选择。
这正是分组背包:第
样例的分组与选项。 家族 A 有 2 个附件,共 4 种买法;B、C 没有附件,各 1 种:
| 家族 | 买法 | 花费 |
收益 |
|---|---|---|---|
| A(物品 1、2、3) | 只买主件 1 | 800 | 1600 |
| A | 主件 1 + 物品 2 | 1200 | 3600 |
| A | 主件 1 + 物品 3 | 1100 | 3100 |
| A | 主件 1 + 物品 2、3 | 1500 | 5100 |
| B(物品 4) | 只买主件 4 | 400 | 1200 |
| C(物品 5) | 只买主件 5 | 500 | 1000 |
转移设计。 设
下表展示样例按 A → B → C 逐组处理后,几个关键容量上的
| 处理到 | |||||
|---|---|---|---|---|---|
| 初始(一个家族都没选) | 0 | 0 | 0 | 0 | 0 |
| 家族 A | 0 | 0 | 1600 | 1600 | 1600 |
| 家族 B | 1200 | 1200 | 1600 | 1600 | 1600 |
| 家族 C | 1200 | 1200 | 1600 | 2200 | 2200 |
这张表有两个观察重点。其一,A 组最便宜的买法要 800 元,所以
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Item { int v, p, q; };
Item items[65];
// 组:主件 (v, v*p) 在首位,其后是附件
vector<vector<pair<int, int> > > groups;
vector<pair<int, int> > opts[65]; // 每组的全部购买方案
// 生成一组的所有方案(必含主件,附件任选子集)
void build_options(int gi) {
vector<pair<int, int> >& g = groups[gi];
int main_v = g[0].first, main_s = g[0].second;
int na = (int)g.size() - 1;
opts[gi].clear();
// 枚举附件子集
for (int mask = 0; mask < (1 << na); mask++) {
int cost = main_v, gain = main_s;
for (int i = 0; i < na; i++)
if (mask & (1 << i)) {
cost += g[1 + i].first;
gain += g[1 + i].second;
}
opts[gi].push_back(make_pair(cost, gain));
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int budget, m;
cin >> budget >> m;
for (int i = 1; i <= m; i++)
cin >> items[i].v >> items[i].p >> items[i].q;
// 分组:主件自开一组,附件挂到 q 指向的主件组尾
int group_id[65];
memset(group_id, 0, sizeof(group_id));
for (int i = 1; i <= m; i++) {
if (items[i].q == 0) {
group_id[i] = (int)groups.size();
groups.push_back(vector<pair<int, int> >());
groups.back().push_back(make_pair(items[i].v, items[i].v * items[i].p));
}
}
for (int i = 1; i <= m; i++) {
if (items[i].q != 0) {
int gid = group_id[items[i].q];
groups[gid].push_back(make_pair(items[i].v, items[i].v * items[i].p));
}
}
int G = (int)groups.size();
for (int i = 0; i < G; i++) build_options(i);
// 分组背包
vector<int> dp(budget + 1, 0);
for (int i = 0; i < G; i++) {
for (int j = budget; j >= 0; j--) {
for (size_t k = 0; k < opts[i].size(); k++) {
int cost = opts[i][k].first, gain = opts[i][k].second;
if (cost <= j && dp[j - cost] + gain > dp[j])
dp[j] = dp[j - cost] + gain;
}
}
}
int ans = 0;
for (int j = 0; j <= budget; j++) ans = max(ans, dp[j]);
cout << ans << "\n";
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 07:06
# update_at: 2026-10-02 07:06
import sys
from itertools import combinations
Item = tuple[int, int, int] # (价格, 重要度, 归属主件编号,0 表示自己是主件)
Group = list[tuple[int, int]] # 组首是主件 (价格, 价格x重要度),其后是它的附件
def build_groups(items: list[Item]) -> list[Group]:
"""把物品按主件分组:主件自开一组,附件挂到 q 指向的主件组尾。"""
groups: list[Group] = []
slot: dict[int, Group] = {} # 主件编号 -> 它的组,附件按 q 查表
for i, (v, p, q) in enumerate(items, 1): # 物品编号 1..m,附件的 q 必指向更靠前的主件
if q == 0:
slot[i] = [(v, v * p)]
groups.append(slot[i])
else:
slot[q].append((v, v * p))
return groups
def group_options(group: Group) -> list[tuple[int, int]]:
"""主件组的全部购买方案 (花费, 收益):必含主件,附件任选子集,不含"整组不买"。"""
main_v, main_s = group[0]
atts = group[1:]
return [
(main_v + sum(v for v, _ in combo), main_s + sum(s for _, s in combo)) # 一个方案
for r in range(len(atts) + 1) # 选 r 个附件,r=0 即只买主件
for combo in combinations(atts, r)
]
def knapsack(plans: list[list[tuple[int, int]]], budget: int) -> int:
"""分组背包:每组至多选一个方案,返回不超过预算的最大收益。"""
dp = [0] * (budget + 1)
for opts in plans:
# 容量倒序遍历:dp[j-cost] 还停在上一组的结果,同组的方案不会被叠加
for j in range(budget, -1, -1):
for cost, gain in opts:
if cost <= j:
dp[j] = max(dp[j], dp[j - cost] + gain)
return max(dp)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
budget, m = next(data), next(data) # 总钱数、物品件数
items = [(next(data), next(data), next(data)) for _ in range(m)]
plans = [group_options(group) for group in build_groups(items)]
print(knapsack(plans, budget))
if __name__ == "__main__":
solve()build_groups 按 q 完成分组(附件的 q 必指向更靠前的主件,顺序读入即可挂到组尾);group_options 用 combinations 枚举"主件 + 附件子集"的全部买法;knapsack 是标准的一维分组背包,倒序容量那行注释就是互斥性的来源。
复杂度
- 时间:设家族数
,每组选项数 ,转移共 次内层比较,数值上不超过 ,与标准 C++ 分组背包解法同阶;分组与生成选项均为 。 - 空间:一维
dp数组( ),分组与选项 ,远低于 32MB 内存限制。
总结
本题的依赖"买附件必买主件"看起来像树形依赖,但由于深度只有 1 层、每主件至多 2 个附件,依赖被完全限制在家族内部。解法的核心动作是先在家族内部用枚举把依赖消掉——每个合法买法压成一个 (花费, 收益) 选项——再把家族之间的互斥选择交给分组背包的一维倒序转移。倒序容量是"每组至多选一个"的全部秘密:它让转移永远只读到上一组的结果。