按面额升序删冗余:用完全背包位集判定每个面额能否被更小面额表出,表不出的面额个数就是最小等价货币系统的 m。
OJ: roj
题目 ID: 3659
难度:普及+/提高-
标签:动态规划背包完全背包位运算python
创建: 2026-10-02 13:46
更新: 2026-10-10 11:30
形式化题目
给定货币系统
记系统
任务:求与
题面样例:
正解
思路
朴素想法与瓶颈。 判断一个面额能否被其余面额表出,最朴素的写法是搜索:枚举所有组合
关键观察一:删冗余不改变可表出集合。 若面额
关键观察二:不可约系统由
这个递推只依赖
关键观察三:判定是完全背包可达性。 维护可达金额集合(初始只有
位集实现。 在 Python 里,可达集合打包成一个大整数:第 (reachable >> v) & 1;闭包就是把整段位集依次平移 & mask 截断到
样例推演。 下表用样例 1(面额升序
| 步骤 | 面额 |
判定时 |
决策 | 加入 |
|---|---|---|---|---|
| 0 | — | — | 初始 | |
| 1 | 3 | 否 | 保留, |
|
| 2 | 6 | 是( |
丢弃 | 不变 |
| 3 | 10 | 否 | 保留, |
|
| 4 | 19 | 是( |
丢弃 | 不变 |
注意两处要点:
代码
/**
* 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;
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); int T; if(!(cin>>T)) return 0; while(T--){int n;cin>>n; vector<int> coins(n); for(int i=0;i<n;i++) cin>>coins[i]; sort(coins.begin(),coins.end()); int top=coins.back(), basis=0; vector<char> reach(top+1); reach[0]=1; for(int i=0;i<n;i++){int coin=coins[i]; if(!reach[coin]){basis++; for(int x=coin;x<=top;x++) if(reach[x-coin]) reach[x]=1;}} cout<<basis<<'\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 13:52
# update_at: 2026-10-02 13:52
import sys
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
T = next(data)
for _ in range(T):
n = next(data)
coins = sorted(next(data) for _ in range(n))
top = coins[-1] # 只需关心 [0, top] 内的金额:查询都落在面额上
mask = (1 << (top + 1)) - 1
reachable = 1 # 第 0 位为 1:金额 0 用 0 张货币即可表出
basis = 0
for coin in coins:
already = reachable >> coin & 1
if not already: # 更小面额表不出 coin → 等价系统必须保留它
basis += 1
# 无限张 coin 的闭包:旧可达集合整体平移 k*coin(k≥1)后并回
closed = reachable
shift = coin
while shift <= top:
closed |= reachable << shift
shift += coin
reachable = closed & mask
out.append(str(basis))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
记
- 时间:排序
; 次位查询;每个保留面额 做 次长度 的平移取并,单次 ,合计 。Python 级操作数 ,与标准完全背包正解 同阶;位集把 个状态压进一次运算,常数更小。实测单个测例(含 20 组)约 s。 - 空间:
,位集约 个机器字(约 3 KB),远低于 32 MB 限制。
总结
这道题的关键是把"等价系统的最小面额数"翻译成一个可贪心计数的判定:能被更小面额表出的面额是冗余的,删掉不改变可表出集合;不可约等价系统又由可表出集合唯一决定,所以升序扫描、只数"表不出"的面额个数就是最小的 >> & 1 查表、平移取并做"无限张"闭包——把逐格转移换成批量字操作。注意两点:必须按面额升序处理(大面额的判定依赖更小面额先入集合),输出的是个数而不是面额数值。实测 20 个真实测例全部一致,单测例约 0.02 s、峰值内存约 14.8 MB(限 1000 ms / 32 MB)。
图示解析
这张图串起本题从输入到答案的主线:
输入面额 a[1..n]
`- 按面额升序扫描 v
|- v 已可达 → 冗余面额,丢弃
`- v 不可达 → 必要面额,ans+1,位集平移取并做闭包
`- 扫描结束 → ans 即最小等价系统的 m顺着分支看:上面一支对应"删冗余不改变可表出集合",下面一支对应"不可达就必须保留"并顺手更新可达集合。整条路线只有一种循环、一种状态(位集),没有回溯和分支搜索——贪心的正确性全部来自观察一、观察二,而位集只是完全背包的实现载体。
