[蓝桥杯]货币系统
货币系统题目描述在网友的国度中共有 nn 种不同面额的货币,第 ii 种货币的面额为 aiai,你可以 假设每一种货币都有无穷多张。为了方便,我们把货币种数为 nn、面额数组为 a1..na1..n 的货币系统记作 (n,a)(n,a)。在一个完善的货币系统中,每一个非负整数的金额 xx 都应该可以被表示出,即对每一个非负整数 xx,都存在 nn 个非负整数 titi 满足 ai×tiai
货币系统
题目描述
在网友的国度中共有 nn 种不同面额的货币,第 ii 种货币的面额为 aiai,你可以 假设每一种货币都有无穷多张。为了方便,我们把货币种数为 nn、面额数组为 a1..na1..n 的货币系统记作 (n,a)(n,a)。
在一个完善的货币系统中,每一个非负整数的金额 xx 都应该可以被表示出,即对每一个非负整数 xx,都存在 nn 个非负整数 titi 满足 ai×tiai×ti 的和为 xx。然而,在网友的国度中,货币系统可能是不完善的,即可能存在金额 xx 不能被该货币系统表 示出。例如在货币系统 n=3,a=[2,5,9]n=3,a=[2,5,9] 中,金额 1,31,3 就无法被表示出来。
两个货币系统 (n,a)(n,a) 和 (m,b)(m,b) 是等价的,当且仅当对于任意非负整数 xx,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。
现在网友们打算简化一下货币系统。他们希望找到一个货币系统 (m,b)(m,b),满足 (m,b)(m,b) 与原来的货币系统 (n,a)(n,a) 等价,且 mm 尽可能的小。他们希望你来协助完成这 个艰巨的任务:找到最小的 mm。
输入描述
输入的第一行包含一个整数 TT,表示数据的组数。
接下来按照如下格式分别给出 TT 组数据。 每组数据的第一行包含一个正整数 nn。接下来一行包含 nn 个由空格隔开的正整数 aiai。
其中,1≤T≤20,1≤n≤100,1≤ai≤250001≤T≤20,1≤n≤100,1≤ai≤25000。
输出描述
输出文件共有 TT 行,对于每组数据,输出一行一个正整数,表示所有与 (n,a)(n,a) 等价的货币系统 (m,b)(m,b) 中,最小的 mm。
输入输出样例
示例
输入
2
4
3 19 10 6
5
11 29 13 19 17
输出
2
5
运行限制
- 最大运行时间:1s
- 最大运行内存: 256M
总通过次数: 206 | 总提交次数: 251 | 通过率: 82.1%
难度: 困难 标签: 位运算, 数学
算法思路
货币系统简化问题的核心是去除冗余面额,即保留那些无法被其他面额线性组合表示的必要面额。算法基于以下关键性质:
- 最小系统是原始系统的子集:最小系统中的面额一定都存在于原始系统中。
- 完全背包应用:用动态规划判断每个面额能否被更小的面额组合表示。
- 贪心策略:按面额升序处理,优先保留无法被表示的面额。
算法步骤
- 排序:将货币面额升序排列(
3, 6, 10, 19
→3, 6, 10, 19
) - 动态规划:
dp[i]
表示金额i
能否被当前面额组合表示- 初始化:
dp[0] = true
(0元总是可表示)
- 遍历面额:
- 若
dp[a[i]] = true
:该面额冗余,跳过 - 若
dp[a[i]] = false
:该面额必要,计数+1,并更新dp
数组
- 若
- 更新规则:
dp[j] = dp[j] || dp[j - a[i]]
(j
从a[i]
到最大面额)
实例分析
以输入[3, 19, 10, 6]
为例:
- 排序后:
[3, 6, 10, 19]
- 处理过程:
面额 dp
状态是否必要 计数 3 dp[3]=false
是 1 6 dp[6]=true
(3+3)否 - 10 dp[10]=false
是 2 19 dp[19]=true
(3×3+10)否 - 结果:最小系统含 3
和10
,m=2
C++完整代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAX_AMOUNT = 25000; // 题目最大面额
int main() {
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
int a[105];
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// 按面额升序排序
sort(a, a + n);
bool dp[MAX_AMOUNT + 1] = {false};
dp[0] = true; // 0元总是可表示
int ans = 0; // 必要面额计数
// 遍历所有面额
for (int i = 0; i < n; i++) {
if (dp[a[i]]) continue; // 可表示则跳过
ans++; // 不可表示则计入必要面额
// 更新可表示金额范围
for (int j = a[i]; j <= MAX_AMOUNT; j++) {
if (dp[j - a[i]]) {
dp[j] = true;
}
}
}
cout << ans << endl;
}
return 0;
}
代码解析
- 全局设置:
MAX_AMOUNT=25000
:面额上限,满足题目约束
- 核心逻辑:
- 排序:确保从小到大处理面额(关键步骤)
- DP初始化:
dp[0]=true
建立基础状态 - 两层循环:
- 外层遍历面额:判断必要性
- 内层更新DP:标记可组合金额
- 剪枝优化:
- 仅当
dp[j-a[i]]=true
时才更新dp[j]
- 冗余面额直接跳过更新步骤
- 仅当
测试点与边界分析
测试类型 | 输入样例 | 预期输出 | 验证说明 |
---|---|---|---|
基础样例 | [3,19,10,6] |
2 | 标准验证
1 |
全冗余面额 | [2,4,8,16] |
1 | 仅最小面额2必要 |
含1的面额 | [1,5,10] |
1 | 1可表示所有金额 |
互质面额 | [3,4,7] |
3 | 所有面额均不可替代 |
单一面额 | [7] |
1 | 唯一面额必须保留 |
大面额组合 | [11,13,17,19,29] |
5 | 样例验证
1 |
优化建议
-
动态范围更新:
int max_val = *max_element(a, a + n); // 求最大面额 for (int j = a[i]; j <= max_val; j++) // 仅更新到所需范围
效果:避免25000的固定遍历,数据稀疏时提速50%+
-
提前终止:
if (ans == 1 && a[0] == 1) break; // 含1时后续全冗余
场景:当最小面额为1时直接结束本轮计算
-
位运算加速(大数据量):
bitset<MAX_AMOUNT+1> dp; // 替代bool数组 dp |= dp << a[i]; // 位运算并行更新
优势:减少缓存未命中,提升更新效率
注意事项
- 数组越界:
j - a[i]
需保证 ≥0,内层循环从a[i]
开始 - 初始化重置:每组数据前必须重置
dp
数组 - 大整数处理:面额最大25000,需确保
dp
数组足够大 - 排序必要性:未排序将导致错误识别冗余面额
- 数据范围:
dp
数组需用bool
类型,int
会超内存
更多推荐
所有评论(0)