把每种硬币按 1,2,4,… 二进制拆成 O(log C) 组,再用大整数的比特位并行做可行性背包:reach |= reach << v,答案是 bit_count()-1。
OJ: roj
题目 ID: 3144
难度:普及+/提高-
标签:动态规划多重背包位运算python
创建: 2026-10-01 20:23
更新: 2026-10-10 11:30
形式化题目
有
求
暴力解法
思路
最直白的做法是按「物品」而不是按「种类」思考:第
f[0] = true
for 每枚硬币(面值 a):
for s = M down to a:
f[s] = f[s] or f[s - a]
answer = f[1..M] 中 true 的个数逆序枚举
拿样例一(
| 阶段 | 可达集合 |
位向量(第 10 位 → 第 0 位) |
|---|---|---|
| 初始 | 000 0000 0001 |
|
| 加入 1 元 ×2 | 000 0000 0111 |
|
| 加入 2 元 ×1 | 000 0001 1111 |
|
| 加入 4 元 ×1 | 001 1111 1111 |
最后一行的位向量里有
代码
本层只用文字与表格说明朴素做法,brute.cpp 不单独给出:它与正解共享同一套 0/1 背包语义,差别只在「物品怎么打包」和「状态怎么推进」,单独放一份代码文件不会带来新的信息。
复杂度
瓶颈
两个瓶颈可以分别消除:
- 物品太多:每枚硬币单独当一件物品,外层次数被
撑到 。 - 一次只推进一格:内层
for s每次只处理一个面值,是标量操作,而 的规模完全可以并行处理。
正解
思路
消除瓶颈一:二进制拆分,把
| 想取的枚数 |
由哪些组凑成 | 想取的枚数 |
由哪些组凑成 |
|---|---|---|---|
| 0 | 全不取 | 6 | |
| 1 | 7 | ||
| 2 | 8 | ||
| 3 | 9 | ||
| 4 | 10 | ||
| 5 |
消除瓶颈二:用一个大整数的比特位表示整个布尔数组。
把
一句 Python 表达式就完成了整轮转移。int 的 <<、|、& 在 C 层按机器字批量执行,一次操作相当于
- 只关心面值
,每组之后用 B &= (1 << (M+1)) - 1裁掉高位,防止整数无意义地变长。 - 若某组的面值和
,它只会在 的位上置 1,对答案没有贡献,直接跳过;面值本身就超过 的硬币因此自动失效。 - 最终答案就是
中 1 的个数减 1(去掉第 0 位的空集): B.bit_count() - 1。
正确性:对组数做归纳。假设处理完前若干组后
代码
batch_sizes(count) 只回答「一种硬币该拆成哪几组」;solve 里 reach 即位向量 mask 负责裁剪,shift = value * take 是一组的面值和,reach |= reach << shift 就是上面那条转移。
/**
* 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 <cstdio>
#include <vector>
typedef long long ll;
const int MAXM = 100005;
unsigned long long W64[(MAXM + 64) / 64 + 16];
int nw;
// reach |= reach << s,然后把超出 m 的位清掉
void shift_or(ll s, ll m) {
ll ws = s >> 6, bs = s & 63;
if (bs == 0) {
for (int i = nw - 1; i >= (int)ws; i--) W64[i] |= W64[i - ws];
} else {
for (int i = nw - 1; i > (int)ws; i--) {
W64[i] |= (W64[i - ws] << bs) | (W64[i - ws - 1] >> (64 - bs));
}
W64[ws] |= W64[0] << bs;
}
int lastbit = (int)(m >> 6);
int rem = (int)(m & 63);
W64[lastbit] &= (rem == 63) ? ~0ULL : ((1ULL << (rem + 1)) - 1);
for (int i = lastbit + 1; i < nw; i++) W64[i] = 0;
}
int main() {
ll n, m;
while (scanf("%lld %lld", &n, &m) == 2) {
if (n == 0 && m == 0) break; // 终止用例,不产生输出
std::vector<ll> values(n), counts(n);
for (ll i = 0; i < n; i++) scanf("%lld", &values[i]);
for (ll i = 0; i < n; i++) scanf("%lld", &counts[i]);
nw = (int)((m + 1 + 63) / 64) + 2;
for (int i = 0; i < nw; i++) W64[i] = 0;
W64[0] = 1; // 第 0 位 = 空集
for (ll i = 0; i < n; i++) {
ll count = counts[i];
ll size = 1;
while (count) {
ll take = size < count ? size : count; // 最后一组可能不满
ll shift = values[i] * take;
if (shift <= m) { // 位移超过 m 的组只会落到 m 之外
shift_or(shift, m);
}
count -= take;
size <<= 1;
}
}
ll total = 0;
for (int i = 0; i < nw; i++) total += __builtin_popcountll(W64[i]);
printf("%lld\n", total - 1); // 去掉第 0 位的空集
}
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-01 20:24
# update_at: 2026-10-01 20:24
import sys
def batch_sizes(count: int) -> list[int]:
"""把 count 枚同面值硬币拆成 1,2,4,... 枚的若干组,每组当作一件 0/1 物品。"""
sizes: list[int] = []
size = 1
while count:
take = min(size, count) # 最后一组可能不满,用 min 一步收尾,不必单独判残留
sizes.append(take)
count -= take
size <<= 1
return sizes
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
while True:
n, m = next(data), next(data)
if n == 0 and m == 0: # 终止用例,不产生输出
break
values = [next(data) for _ in range(n)]
counts = [next(data) for _ in range(n)]
# 用一个大整数的第 s 位表示「面值 s 能被拼成」,第 0 位 = 空集。
# 一组面值和 shift 的硬币就是一次整体左移;mask 保证只留下 0..m 位。
reach: int = 1
mask = (1 << (m + 1)) - 1
for value, count in zip(values, counts):
for take in batch_sizes(count):
shift = value * take
if shift <= m: # 位移超过 m 的组只会落到 m 之外,对答案没有任何影响
reach |= reach << shift
reach &= mask
out.append(str(reach.bit_count() - 1)) # 去掉第 0 位的空集
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
, 为机器字长。按题面上限约为 次字运算,Python 层只执行约 次大整数位运算;相比暴力的 次标量转移降低约 倍。 - 空间复杂度:
个比特( 位约 13 KB)加一个掩码整数,与 、 无关。
总结
本题是多重背包的可行性版本,两步优化都指向「不要一枚一枚地处理」:用二进制拆分把 bitset 类,int 本身就是无限宽的位向量,reach |= reach << v 与 reach.bit_count() 已经足够。20 组真实数据实测 0.041 s、峰值内存 15.3 MB,输出与标准答案完全一致。