货币系统

题目描述

在网友的国度中共有 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%

难度: 困难   标签: 位运算, 数学

算法思路

货币系统简化问题的核心是​​去除冗余面额​​,即保留那些无法被其他面额线性组合表示的必要面额。算法基于以下关键性质:

  1. ​最小系统是原始系统的子集​​:最小系统中的面额一定都存在于原始系统中。
  2. ​完全背包应用​​:用动态规划判断每个面额能否被更小的面额组合表示。
  3. ​贪心策略​​:按面额升序处理,优先保留无法被表示的面额。
算法步骤
  1. ​排序​​:将货币面额升序排列(3, 6, 10, 19 → 3, 6, 10, 19
  2. ​动态规划​​:
    • dp[i]表示金额i能否被当前面额组合表示
    • 初始化:dp[0] = true(0元总是可表示)
  3. ​遍历面额​​:
    • dp[a[i]] = true:该面额冗余,跳过
    • dp[a[i]] = false:该面额必要,计数+1,并更新dp数组
  4. ​更新规则​​:dp[j] = dp[j] || dp[j - a[i]]ja[i]到最大面额)
实例分析

以输入[3, 19, 10, 6]为例:

  1. ​排序后​​:[3, 6, 10, 19]
  2. ​处理过程​​:
    面额 dp状态 是否必要 计数
    3 dp[3]=false 1
    6 dp[6]=true (3+3) -
    10 dp[10]=false 2
    19 dp[19]=true (3×3+10) -
    ​结果​​:最小系统含310m=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;
}

代码解析

  1. ​全局设置​​:
    • MAX_AMOUNT=25000:面额上限,满足题目约束
  2. ​核心逻辑​​:
    • ​排序​​:确保从小到大处理面额(关键步骤)
    • ​DP初始化​​:dp[0]=true建立基础状态
    • ​两层循环​​:
      • 外层遍历面额:判断必要性
      • 内层更新DP:标记可组合金额
  3. ​剪枝优化​​:
    • 仅当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


优化建议

  1. ​动态范围更新​​:

    int max_val = *max_element(a, a + n);  // 求最大面额
    for (int j = a[i]; j <= max_val; j++)  // 仅更新到所需范围

    ​效果​​:避免25000的固定遍历,数据稀疏时提速50%+

  2. ​提前终止​​:

    if (ans == 1 && a[0] == 1) break;  // 含1时后续全冗余

    ​场景​​:当最小面额为1时直接结束本轮计算

  3. ​位运算加速​​(大数据量):

    bitset<MAX_AMOUNT+1> dp;  // 替代bool数组
    dp |= dp << a[i];          // 位运算并行更新

    ​优势​​:减少缓存未命中,提升更新效率


注意事项

  1. ​数组越界​​:j - a[i]需保证 ≥0,内层循环从a[i]开始
  2. ​初始化重置​​:每组数据前必须重置dp数组
  3. ​大整数处理​​:面额最大25000,需确保dp数组足够大
  4. ​排序必要性​​:未排序将导致错误识别冗余面额
  5. ​数据范围​​:dp数组需用bool类型,int会超内存
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐