难 · 动态规划
凑硬币
这道题你见过一次 —— 面值 25/10/5/1,当时贪心可证最优。换一套面值,那个证明就蒸发了。而动态规划不关心拿到什么硬币:它对每个子金额都问一遍诚实的最小值。
一句话答案<code>best[0] = 0</code>;对每个金额 x 和每枚硬币 c:<code>best[x] = min(best[x], best[x-c] + 1)</code>。答案就是 <code>best[amount]</code>,一直不可达则输出 -1。
题目
输入: 第一行是 n 和 amount(1 ≤ n ≤ 10,1 ≤ amount ≤ 10000)。第二行 n 种互不相同的面值(1 ≤ c ≤ 1000),每种数量无限。
输出: 凑出 amount 所需的最少硬币数;凑不出则输出 -1。
样例:
| 输入 | 输出 |
|---|---|
3 11 | 3 |
2 7 | -1 |
面值 {1, 3, 4}、金额 6 时,贪心拿 4+1+1(三枚),而 3+3 只要两枚 —— 在样例上把两种都跑一遍,看看差别。表从不选「眼下最好」的硬币,它选的是「最好的结局」。
你的解答
「运行我的输入」用的自定义输入
判题在浏览器本地运行。死循环会在 3 秒后被自动中断 —— 但还是确保每个循环都有出口。代码随打随存。
卡住了?提示
提示 1 — 先定义格子,再写循环
best[x] 的含义:恰好凑出 x 的最少硬币数;凑不出则为无穷大。定义定死,递推式几乎是被逼出来的 —— 凑出 x 的最优方案里最后一枚必是某枚 c,它之前是凑 x − c 的最优方案:best[x] = 各 c 中 best[x-c] + 1 的最小值。
提示 2 — 无穷大需要保镖
给不可达的金额用一个很大的 INF(比如 1000000000)—— 但转移处要设防:if (best[x-c] != INF && best[x-c] + 1 < best[x])。不设防的话,INF + 1 会混进表里,毒液蔓延到所有更大的金额。这个缺失的判断是本题最常见的 bug。
提示 3 — -1 也是表里的答案
填完表后,best[amount] 要么是真计数,要么还是 INF —— 后一种情况输出 -1。不要试图在循环途中判断「不可能」;表填完就是填完了,不可能只是某个格子始终没被改善。
题解(先自己试试!)
#include <iostream>
using namespace std;
int best[10005];
int main() {
int n, amount;
cin >> n >> amount;
int c[10];
for (int i = 0; i < n; i++) cin >> c[i];
const int INF = 1000000000;
best[0] = 0;
for (int x = 1; x <= amount; x++) best[x] = INF;
for (int x = 1; x <= amount; x++) {
for (int i = 0; i < n; i++) {
if (c[i] <= x && best[x - c[i]] != INF && best[x - c[i]] + 1 < best[x]) {
best[x] = best[x - c[i]] + 1;
}
}
}
cout << (best[amount] == INF ? -1 : best[amount]) << endl;
return 0;
}
这是「完全背包」最朴素的戏服,也是 最少硬币数 的直接续集 —— 同一个故事,翻过一章。值得带走的模式:贪心没有证明时,给子问题定义一张表,让转移替你做决定。同一张表加一个维度就是 0/1 背包,再往后是子集,然后是所有事。
深度解析
为什么这样做对
贪心拿最大硬币,在 {1,3,4} 上翻车 —— 所以放弃局部选择,定义 best[x] = 恰好凑出 x 的最少硬币数。任何最优支付方案的最后一枚必是某枚 c,于是 best[x] = 各硬币中 min(best[x−c] + 1)。自底向上扫 x,每个子答案在被引用时都已就绪:不需要贪心证明,因为根本没有贪心。
代价
时间 O(n × amount),空间 O(amount) —— 10 × 10000 = 10 万次转移,瞬间完成。指数级替代方案(每步试每枚硬币)会把同样的子金额重算天文数字遍。
常见错法
- 放 INF+1 进表:转移时不设
best[x-c] != INF的防,毒液蔓延到所有更大的金额。这是本题最常见的 bug,没有之一。 - 忘了
best[0] = 0—— 这个边界让「用 0 枚硬币凑 0 元」合法,所有链条都从它生长。 - 凑不出时输出 0 而不是 -1,或在循环途中判断「不可能」:表先填完,不可能只是某个格子一直没被改善。
下一步
能成立(以及为什么)的贪心对照组是 最少硬币数;这里写的 DP 就是换装后的完全背包。加一个维度就成 0/1 背包 —— 通向一整个家族的门。
记住这两句
- 贪心没有证明时,给子金额定义一张表,让转移替你做决定。
- INF 是哨兵,不是数:凡碰到它的转移都要设防。
🐞 这段是错的 —— 点开,亲手让它挂一次
又是贪心 —— 凑 6 拿了 4+1+1,而 3+3 只要两枚
#include <iostream>
using namespace std;
int main() {
int n, amount;
cin >> n >> amount;
int c[10];
for (int i = 0; i < n; i++) cin >> c[i];
int left = amount, count = 0;
while (left > 0) {
int take = -1;
for (int i = 0; i < n; i++) {
if (c[i] <= left && c[i] > take) take = c[i]; // bug: greedy
}
if (take == -1) { cout << -1 << endl; return 0; }
left -= take;
count++;
}
cout << count << endl;
return 0;
}