按桶数递增做升序字典序组合 DFS,用增量位集完全背包验证子集能否恰好量出 Q,首个成功组合即桶数最少且字典序最小的答案。
OJ: roj
题目 ID: 2082
难度:提高
标签:USACO搜索剪枝位运算背包
创建: 2026-10-01 07:26
更新: 2026-10-07 13:50
形式化题目
给定目标量
最小; - 在所有最小的
中,把元素升序排序后字典序最小。
输出
样例:2 3 5。
暴力解法
思路
最朴素的做法是把
判定本身不难:对子集做一次完全背包,
但
代码
复杂度与瓶颈
瓶颈在于无序地枚举全部
正解
关键观察
把要求拆成三个可以逐层兑现的观察:
- 最少桶数可以逐层试:先问"1 个桶行不行",再问"2 个桶行不行"……第一个可行的
就是最小桶数,不需要枚举完所有子集。 - 字典序可以由枚举顺序天然保证:把桶按容积升序排序后,用组合式 DFS(每层下标严格递增、同一层从小容积先试)枚举大小为
的子集,生成的组合恰好按升序字典序排列——第一个成功的组合就是平局规则的胜者,无需收集所有解再比较。 - 强剪枝来自"每个桶至少倒一次":买来的桶一定都要用上(不然不买它),而桶按升序排列,所以已选容积之和
沿 DFS 路径单调递增且不超过 。一旦 ,后面所有更大的桶都装不下,整条分支直接 break。
思路
外层枚举桶数
- 用一个
位的整数(Python 大整数) bits表示可达集合,第位为 1 表示恰好量出 夸脱; - 加入桶
的效果是"对每个已可达的 ,再倒入任意整数桶 ",即与 做加法闭包。用倍增实现: bits依次左移再或回去,恰好覆盖所有倍数增量(任何 的 都能按二进制拆分),超过 的位直接截断; - 递归到第
层时,看 bits的第位:为 1 则该组合可行。
total 剪枝保证搜索树极小;位集让每次可行性传递都只是一次大整数位运算。注意这里 DFS 只负责按字典序枚举候选子集,"恰好量出
以样例
graph TD
S["k=2 从 3,5,7 中选"] --> A["先选 3 (total=3)"]
S --> B["再选 5 (total=8)"]
A --> A1["配 5: {3,5} → 16=3+3+5+5 可达 ✓"]
A --> A2["配 7: {3,7} → 3x+7y 中无 16 ✗"]
B --> B1["配 7: {5,7} → 5x+7y 中无 16 ✗"]
A1 --> R["按字典序首个成功: 3 5"]
观察重点:DFS 先在第一个桶位置试最小的 3,第二层又先试 5,{3, 5} 成为第一个被验证成功的组合——这正是"升序字典序优先"的含义;{3, 7}、{5, 7} 即使可行也会排在它后面。
位集的一步转移可以看这个例子(已选
| 步骤 | bits(可达的 |
|---|---|
| 初始 | |
| 左移 5 再或 | |
| 左移 10 再或 | 并入 |
| 左移 20( |
不变 |
观察重点:倍增步长
代码
Python 版:
#!/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-01 07:26
# update_at: 2026-10-01 07:26
import sys
def extend(bits: int, v: int, q: int) -> int:
"""在位集 bits 上并入“再倒任意整数桶 v”的效果:与 v 的倍数做加法闭包。
倍增:依次左移 v、2v、4v…并或回去,恰好补齐所有倍数增量;
超过 q 的位直接截掉(之后只会加正数,不会再用到它们)。
"""
full = (1 << (q + 1)) - 1
step = v
while step <= q:
bits |= (bits << step) & full
step <<= 1
return bits
def pick(q: int, buckets: list[int], start: int, rest: int, total: int, bits: int) -> list[int] | None:
"""按字典序在升序 buckets[start:] 里再选 rest 个桶,成功返回所选桶,失败返回 None。
bits 是已选桶能刚好量出的夸脱位集(0..q),total 是已选容积之和。
桶按升序逐个倒,每个选中的桶至少倒一次 → total+v > q 时后面的桶更大,
整条分支的剩余桶全部装不下,直接 break(这也是字典序扫描的天然截断)。
"""
if rest == 0:
return [] if bits >> q & 1 else None
for i in range(start, len(buckets) - rest + 1): # 剩余桶必须够 rest 个
v = buckets[i]
if total + v > q:
break
tail = pick(q, buckets, i + 1, rest - 1, total + v, extend(bits, v, q))
if tail is not None:
return [v] + tail
return None
def best_set(q: int, buckets: list[int]) -> list[int]:
"""桶数从 1 往上枚举:更少桶必更优;同数量下 dfs 天然按升序字典序,首个成功即答案。"""
for k in range(1, len(buckets) + 1):
chosen = pick(q, buckets, 0, k, 0, 1) # bits 初始只有 0 夸脱可达
if chosen is not None:
return chosen
raise AssertionError("题目保证有解") # 兜底,正常数据不会走到
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
q = next(data)
p = next(data)
buckets = sorted({next(data) for _ in range(p)}) # 去重:重复容积不改变可达集合
ans = best_set(q, buckets)
print(' '.join(map(str, [len(ans)] + ans)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-06 12:08
* update_at: 2026-10-06 12:08
*/
#include <cstdio>
#include <bitset>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll MAXQ = 20000; // 夸脱数上限
const ll MAXP = 100; // 桶数上限
typedef bitset<MAXQ + 1> Bits; // 第 x 位为 1 表示恰好能量出 x 夸脱
ll Q, P, m;
vector<ll> buckets; // 去重并升序排列后的桶容积
ll chosen[MAXP + 5]; // 当前搜索到的组合,chosen[0..k-1]
// 在 bits 上并入“再倒任意整数桶 v”的效果:
// 依次左移 v、2v、4v… 再或回去,恰好补齐所有 j*v 增量(j 可按二进制拆分)。
Bits extend(Bits bits, ll v) {
ll step = v;
while (step <= Q) {
bits |= (bits << step);
step <<= 1;
}
return bits;
}
// 在升序 buckets[start..m-1] 中按字典序再选 rest 个桶,depth 是已选个数。
// 每个买来的桶至少倒一次,total 为已选容积之和;桶升序,total+v>Q 时后面
// 的桶更大,整条分支直接 break。
bool dfs(ll start, ll rest, ll total, ll depth, Bits bits) {
if (rest == 0) {
return bits.test(Q); // 第 Q 位为 1 表示该组合能恰好量出 Q
}
for (ll i = start; i + rest <= m; i++) { // 剩余桶必须够 rest 个
ll v = buckets[i];
if (total + v > Q) {
break;
}
chosen[depth] = v;
if (dfs(i + 1, rest - 1, total + v, depth + 1, extend(bits, v))) {
return true;
}
}
return false;
}
int main() {
scanf("%lld", &Q);
scanf("%lld", &P);
for (ll i = 1; i <= P; i++) {
ll x;
scanf("%lld", &x);
buckets.push_back(x);
}
sort(buckets.begin(), buckets.end());
buckets.erase(unique(buckets.begin(), buckets.end()), buckets.end());
m = buckets.size();
// 桶数从 1 往上枚举:更少桶必更优;同数量下 dfs 天然按升序字典序,
// 首个成功的组合就是“桶数最少且字典序最小”的答案。
for (ll k = 1; k <= m; k++) {
if (dfs(0, k, 0, 0, Bits(1))) { // Bits(1):仅 0 夸脱可达
printf("%lld", k);
for (ll i = 0; i < k; i++) {
printf(" %lld", chosen[i]);
}
printf("\n");
return 0;
}
}
return 0;
}复杂度
- 时间:对每个
,DFS 节点数被 与下标递增双重约束;每个节点做至多 次长度 的位集或运算(Python 大整数按 64 位字压缩)。整体远低于朴素 枚举,与官方正解(同样的"桶数递增 + 字典序 DFS + 可行性判定"结构)同阶,实测 10 组数据全部毫秒级通过。 - 空间:
的位集沿递归栈保存,加桶列表 。
总结
本题的本质是"最小字典序子集选择 + 可行性判定":最少桶数靠