形式化题目

给定货币系统 (n,a)(n, a):nn 种面额 a1,a2,…,ana_1, a_2, \dots, a_n,每种面额有无穷多张。非负整数金额 xx 叫做可表出的,如果存在非负整数 t1,…,tnt_1, \dots, t_n 使得

x=∑i=1ntiai.x = \sum_{i=1}^{n} t_i a_i.

记系统 (n,a)(n,a) 可表出的金额集合为 S(a)S(a)。两个系统 (n,a)(n, a)、(m,b)(m, b) 等价,当且仅当 S(a)=S(b)S(a) = S(b):任意金额要么同时可被两者表出,要么同时不可表出。

任务:求与 (n,a)(n, a) 等价的货币系统 (m,b)(m, b) 中最小的 mm——只求最少需要几种面额,不需求具体面额。

题面样例:(4,[3,19,10,6])(4, [3, 19, 10, 6]) 的答案是 22(等价系统 [3,10][3, 10] 已最小);(5,[11,29,13,19,17])(5, [11, 29, 13, 19, 17]) 的答案是 55,即五个面额一个都删不掉。

正解

思路

朴素想法与瓶颈。 判断一个面额能否被其余面额表出,最朴素的写法是搜索:枚举所有组合 ∑tiaj=v\sum t_i a_j = v,看能否凑出 vv;或者枚举面额子集、逐对比较可表出集合。两者都是指数级——vv 最大 2500025000、nn 最大 100100,逐个面额各搜一次完全不可行。瓶颈在于把"可表出性"当成了每次都从头算的独立搜索,而它其实可以增量维护。

关键观察一:删冗余不改变可表出集合。 若面额 vv 能被系统中其余面额表出,删掉 vv 后 SS 不变——原来表示里出现的每个 vv 都可以替换成其余面额的表示,递归展开后全部落在保留面额上。反过来,表示金额 vv 时用到的面额都不超过 vv,所以按升序处理、判断"vv 能否被更小(或相等)的面额表出"就足够:判断时更小面额的可达信息已经完全定型,重复面额也自然被先出现的同值面额覆盖。

关键观察二:不可约系统由 SS 唯一决定,因此贪心计数就是最小值。 设 B={b1<b2<⋯<bm}B = \{b_1 < b_2 < \dots < b_m\} 是"每个面额都不能被其余面额表出"的不可约系统且 S(B)=SS(B) = S。先看 b1b_1:它必须等于 s1=min⁡(S∖{0})s_1 = \min(S \setminus \{0\})——一方面 b1∈Sb_1 \in S 所以 s1⩽b1s_1 \leqslant b_1,另一方面表示 s1s_1 只能用不小于 b1b_1 的面额,所以 s1⩾b1s_1 \geqslant b_1,两头夹住取等。再归纳:由不可约性 bk∉S(b1,…,bk−1)b_k \notin S(b_1, \dots, b_{k-1})(否则它就被更小面额表出了),而对任意 s∈Ss \in S、s<bks < b_k,表示 ss 用到的面额都 ⩽s<bk\leqslant s < b_k、只能是 b1,…,bk−1b_1, \dots, b_{k-1},故 s∈S(b1,…,bk−1)s \in S(b_1, \dots, b_{k-1})。两件事合起来就是

bk=min⁡(S∖S(b1,…,bk−1)).b_k = \min\bigl(S \setminus S(b_1, \dots, b_{k-1})\bigr).

这个递推只依赖 SS,所以任何与 (n,a)(n,a) 等价的系统 (m′,b′)(m', b') 反复删冗余(每次删除都不改变 SS)后得到的不可约系统 B′B' 必然等于 BB,于是 m′⩾∣B′∣=∣B∣m' \geqslant |B'| = |B|;而 BB 自身就可达,故最小的 mm 就是 ∣B∣|B|:答案 = 升序扫描中"不能被更小面额表出"的面额个数。

关键观察三:判定是完全背包可达性。 维护可达金额集合(初始只有 00),加入面额 vv(无限张)就是完全背包的正序转移:新可达集合 =Sold∪{ p+k⋅v∣p∈Sold, k⩾1 }= S_{\text{old}} \cup \{\, p + k \cdot v \mid p \in S_{\text{old}},\ k \geqslant 1 \,\}。若 vv 已可达,这个闭包是恒等变换,直接跳过。

位集实现。 在 Python 里,可达集合打包成一个大整数:第 xx 位为 11 表示金额 xx 可达。判定就是 (reachable >> v) & 1;闭包就是把整段位集依次平移 v,2v,3v,…v, 2v, 3v, \dots 后按位或,最后 & mask 截断到 [0,A][0, A],其中 A=max⁡aiA = \max a_i。只需维护到 AA:所有判定都发生在面额值上(⩽A\leqslant A),更高的位永远不会被查询。

样例推演。 下表用样例 1(面额升序 [3,6,10,19][3, 6, 10, 19],A=19A = 19)完整复演贪心与可达集合的生长:

步骤 面额 vv 判定时 vv 已可达 决策 加入 vv 后可达金额(⩽19\leqslant 19)
0 — — 初始 {0}\{0\}
1 3 否 保留,ans=1ans=1 {0,3,6,9,12,15,18}\{0,3,6,9,12,15,18\}
2 6 是(3+33+3) 丢弃 不变
3 10 否 保留,ans=2ans=2 {0,3,6,9,10,12,13,15,16,18,19}\{0,3,6,9,10,12,13,15,16,18,19\}
4 19 是(9+109+10) 丢弃 不变

注意两处要点:66 虽不是 33 的"新组合",但被闭包 3+33+3 覆盖;1919 在步骤 3 加入 1010 后才变为可达(9+109+10),所以判定顺序必须升序。最终 ans=2ans=2,保留面额 {3,10}\{3, 10\},与题面说明一致。样例 2 中五个面额两两凑不出,每步都判"不可达",答案 55。

代码

cpp
/**
 * 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;}
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-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()

复杂度

记 A=max⁡iai⩽25000A = \max_i a_i \leqslant 25000,n⩽100n \leqslant 100,BB 为保留面额集合,ww 为 Python 大整数的单字位数(64 位平台为 30 bit)。

  • 时间:排序 O(nlog⁡n)O(n \log n);nn 次位查询;每个保留面额 vv 做 ⌈A/v⌉\lceil A/v \rceil 次长度 AA 的平移取并,单次 O(A/w)O(A/w),合计 O ⁣(Aw∑v∈B⌈A/v⌉)O\!\bigl(\frac{A}{w} \sum_{v \in B} \lceil A/v \rceil \bigr)。Python 级操作数 ∑v∈BA/v⩽nA\sum_{v \in B} A/v \leqslant nA,与标准完全背包正解 O(nA)O(nA) 同阶;位集把 ww 个状态压进一次运算,常数更小。实测单个测例(含 20 组)约 0.020.02 s。
  • 空间:O(n+A/w)O(n + A/w),位集约 ⌈25001/30⌉≈834\lceil 25001/30 \rceil \approx 834 个机器字(约 3 KB),远低于 32 MB 限制。

总结

这道题的关键是把"等价系统的最小面额数"翻译成一个可贪心计数的判定:能被更小面额表出的面额是冗余的,删掉不改变可表出集合;不可约等价系统又由可表出集合唯一决定,所以升序扫描、只数"表不出"的面额个数就是最小的 mm。判定本身是完全背包的可达性问题,Python 用一个整数当位集——>> & 1 查表、平移取并做"无限张"闭包——把逐格转移换成批量字操作。注意两点:必须按面额升序处理(大面额的判定依赖更小面额先入集合),输出的是个数而不是面额数值。实测 20 个真实测例全部一致,单测例约 0.02 s、峰值内存约 14.8 MB(限 1000 ms / 32 MB)。

图示解析

这张图串起本题从输入到答案的主线:

text
输入面额 a[1..n]
`- 按面额升序扫描 v
   |- v 已可达 → 冗余面额,丢弃
   `- v 不可达 → 必要面额,ans+1,位集平移取并做闭包
      `- 扫描结束 → ans 即最小等价系统的 m

顺着分支看:上面一支对应"删冗余不改变可表出集合",下面一支对应"不可达就必须保留"并顺手更新可达集合。整条路线只有一种循环、一种状态(位集),没有回溯和分支搜索——贪心的正确性全部来自观察一、观察二,而位集只是完全背包的实现载体。