在算法竞赛中,货币系统问题是一个经典的动态规划应用场景。这道NOIP提高组题目要求我们找到一个与给定货币系统等价但货币种类最少的简化系统。理解这个问题需要从几个关键点入手:
首先,两个货币系统等价的定义是它们能够表示的金额集合完全相同。这意味着我们需要找到一个最小的货币子集,使得这个子集能够表示原系统中所有可表示的金额,且不能表示原系统中不能表示的金额。
举个例子,对于货币系统[2,5,9],金额1和3无法被表示,而简化后的系统[2,5]仍然无法表示1和3,但可以表示原系统能表示的所有金额(如2,4=2+2,5,7=2+5等),因此[2,5]就是一个有效的简化系统。
解决这个问题的关键在于认识到:如果一个货币的面额可以被系统中其他更小的面额组合表示出来,那么这个面额就是冗余的,可以从系统中移除。
具体来说,我们需要:
这个问题非常适合用动态规划来解决。我们可以定义一个布尔数组f,其中f[i]表示金额i能否被当前考虑的货币系统表示。初始化时f[0]=true(金额0总是可以被表示)。
算法步骤如下:
让我们仔细分析提供的C++实现代码:
cpp复制#include<stdio.h>
#include<algorithm>
#include<string.h>
using namespace std;
#define MAXAI 25005
#define MAXN 105
int f[MAXAI];
int a[MAXN];
int main() {
int i,j,n,T,ans;
scanf("%d",&T);
while(T--) {
memset(f,0,sizeof(f));
scanf("%d",&n); ans=n;
for(i=1;i<=n;i++) scanf("%d",&a[i]);
sort(a+1,a+n+1);
f[0]=1;
for(i=1;i<=n;i++) {
if(f[a[i]]) {
ans--;
continue;
}
for(j=a[i];j<=a[n];j++) {
f[j]=f[j]|f[j-a[i]];
}
}
printf("%d\n",ans);
}
return 0;
}
预处理指令:
全局变量:
主函数逻辑:
动态规划的核心在于f数组的更新:
cpp复制for(j=a[i];j<=a[n];j++) {
f[j]=f[j]|f[j-a[i]];
}
这段代码实现了完全背包问题的解法。对于每个金额j,如果j-a[i]可以被表示,那么j也可以被表示(通过加一张a[i]货币)。
注意:这里使用a[n]作为上限是因为更大的金额不会影响我们的判断,我们只需要考虑不超过最大面额的金额能否被表示。
让我们分析这个算法的时间和空间复杂度:
时间复杂度:
空间复杂度:
输入优化:
空间优化:
提前终止:
边界条件处理:
初始化问题:
排序的重要性:
提示:在竞赛中,建议使用更明显的变量名,如用canRepresent代替f,提高代码可读性
有限货币数量:
最小面额系统:
最大不可表示金额:
完全背包问题:
找零钱问题:
子集和问题:
理解问题本质:
选择合适算法:
测试用例设计:
时间管理:
在实际编程竞赛中,这类问题往往考察选手对动态规划的理解和应用能力。通过这道题目的练习,可以加深对完全背包问题及其变种的理解,为处理更复杂的动态规划问题打下基础。
虽然给出的代码已经能够正确解决问题,但从工程和竞赛角度,还可以做以下改进:
cpp复制#include <bits/stdc++.h>
using namespace std;
constexpr int MAXAI = 25005;
constexpr int MAXN = 105;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
vector<int> a(n);
for (auto& x : a) cin >> x;
sort(a.begin(), a.end());
bitset<MAXAI> f;
f[0] = true;
int ans = 0;
for (auto x : a) {
if (!f[x]) {
ans++;
for (int j = x; j <= a.back(); j++) {
f[j] = f[j] | f[j - x];
}
}
}
cout << ans << '\n';
}
return 0;
}
改进点说明:
防御性编程:
对于正在学习动态规划和算法竞赛的青少年编程爱好者,我建议:
学习路径:
调试技巧:
资源推荐:
通过系统地学习和练习这类问题,可以显著提高算法设计和实现能力,为参加更高级别的编程竞赛做好准备。