「题解」零钱兑换
Give me your money!!1
「我的做题历程」:
step1:观察题面。
「编写一个函数来计算可以凑成总金额」,可以得出这是一道背包 DP。
「每种硬币的数量是无限的」,进一步得出这是道完全背包。(题型:完全背包)
「最少的硬币个数」,证明这要在背包的前提下,求出最小组成数量。
「多组测试数据」,谨记多组输入 (论 Wrong Answer 与没有多组输入)。(注意:多组输入)
step2:思考解法。
第一步,思考 dp 状态:
\(dp_{i,j}\):前 \(i\) 种硬币凑出面值 \(j\) 的最少币数。
对于当前一种硬币 \(coins_{i}\) 而言,只有取或不取两种状态。
若取,取后的币数为前 \(i - 1\) 种硬币凑出面值 \(j-w_{i}\times k\) 的总币数加上当前种类所需币数 \(k\)。
若不取,则说明前 \(i - 1\) 种硬币已经能够凑出面值 \(j\),不需要再取。
第二步,思考状态转移方程:
原本完全背包的状态转移方程是:
但这里我们并不是求总金额以内最大能凑出的面值,而是求凑成总金额的最少币数,于是就有:
通过观察发现,上述方程可以降维。由于对 \(dp_{i}\) 有影响的只有 \(i - 1\),故可以把前一维抹掉,但需要保证 \(dp_{i,j}\) 可以被 \(dp_{i, j - a_{i}}\) 影响(即 \(dp_{i,j}\) 被计算时 \(dp_{i, j - a_{i}}\) 已经被算出),这才相当于物品 \(i\) 多次被放入背包,所以枚举当前面值 \(j\) 时要正序。
第三步,打出完全背包的代码,把状态转移方程换一下,于是本题的算法部分就完成啦:
for (int i = 1; i <= n; i++) {
for (int j = a[i]; j <= amount; j++) {
dp[j] = min(dp[j], dp[j - a[i]] + 1);
}
}
step3:完成代码:
通过数据范围可以发现,一种硬币的面额是可以比总金额大的,因此可以预处理浅浅优化一下(虽然没什么大的效果)。
因为找的是最小币数,所以 dp 数组要初始化成极大值,而前 \(0\) 种硬币凑成 面值 \(0\) 只需要 \(0\) 种硬币,由此可得 \(dp_{0} = 0\)。
输出时值得注意的是,「如果没有任何一种硬币组合能组成总金额,输出 \(-1\)」;在代码中,这意味着「如果 \(dp_{amount}\) 没有被更新,则输出 \(-1\)」,所以只需要输出时特判一下 \(dp_{amount}\) 若仍是初始值就输出 \(-1\)。
\
代码(抵制学术不端行为,拒绝 Ctrl + C):
#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 5, A = 1e4 + 5, INF = 0x3f3f3f3f;
int n, amount, a[N], dp[A];
/*
dp(i, j): 前 i 个硬币凑出 j 的最少硬币个数
dp(i, j) = min(dp(i - 1, j - a[i]), dp(i - 1, j));
取这个硬币 or 不取这个硬币
*/
int main() {
freopen("exchange.in", "r", stdin);
freopen("exchange.out", "w", stdout);
while (~scanf("%d %d", &n, &amount)) {
memset(dp, 0x3f, sizeof dp);
for (int i = 1; i <= n; i++) {
scanf("%d", a + i);
}
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = a[i]; j <= amount; j++) {
dp[j] = min(dp[j - a[i]] + 1, dp[j]);
}
}
printf("%d\n", dp[amount] == INF ? -1 : dp[amount]); // 可以使用三目运算符来特判
}
return 0;
}
快去 AC 『零钱兑换』 叭~ ヾ(≧▽≦*)o
Bye bye! 👋👋