8-2 找零
找零问题(Making Change / Coin Change)
找零问题是经典的动态规划(Dynamic Programming)问题。给定一组硬币面额(coin denominations)和一个目标金额(target amount),求用最少的硬币凑出该金额。例如:硬币面额为 {1, 5, 10, 25},金额为 30,则最少需要 2 枚硬币(25 + 5)。
找零问题有两个常见变体:
- 最少硬币数(优化问题):求凑出目标金额所需的最少硬币数量
- 组合数(计数问题):求凑出目标金额共有多少种不同的方式
贪心法(Greedy) — 仅适用于特定币种
贪心法(Greedy Algorithm)的核心思想是:每次选择面额最大的硬币,直到凑满目标金额。对于标准的美分系统 {1, 5, 10, 25},贪心法可以得到最优解。但对于任意面额的硬币组合,贪心法可能失败。
反例:硬币 = {1, 3, 4},金额 = 6。
- 贪心法:先选 4,剩余 2,再选两个 1,共 3 枚(4 + 1 + 1)
- 最优解:选两个 3,共 2 枚(3 + 3)
因此,贪心法仅在特定币种系统(canonical coin systems)下有效,不具有通用性。
C++ 实现
#include <iostream>
#include <vector>
#include <algorithm>
int greedyChange(const std::vector<int>& coins, int amount)
{
// sort coins in descending order
std::vector<int> sorted = coins;
std::sort(sorted.begin(), sorted.end(), std::greater<int>());
int count = 0;
int remaining = amount;
for (int coin : sorted)
{
// use as many of this coin as possible
while (remaining >= coin)
{
remaining -= coin;
count++;
}
}
// if remaining > 0, change cannot be made
if (remaining > 0)
{
return -1;
}
return count;
}
int main()
{
std::vector<int> coins = {1, 5, 10, 25};
int amount = 30;
int result = greedyChange(coins, amount);
std::cout << "Greedy: minimum coins for " << amount
<< " = " << result << "\n";
// counterexample: greedy fails
std::vector<int> coins2 = {1, 3, 4};
int amount2 = 6;
int greedyResult = greedyChange(coins2, amount2);
std::cout << "Greedy for {1,3,4} amount=" << amount2
<< " = " << greedyResult << " (optimal is 2)\n";
return 0;
}
运行该程序将输出
Greedy: minimum coins for 30 = 2
Greedy for {1,3,4} amount=6 = 3 (optimal is 2)
C 实现
#include <stdio.h>
#include <stdlib.h>
// comparison function for descending sort
int cmpDesc(const void* a, const void* b)
{
return *(int*)b - *(int*)a;
}
int greedyChange(int coins[], int n, int amount)
{
// sort coins in descending order
qsort(coins, n, sizeof(int), cmpDesc);
int count = 0;
int remaining = amount;
for (int i = 0; i < n; i++)
{
// use as many of this coin as possible
while (remaining >= coins[i])
{
remaining -= coins[i];
count++;
}
}
// if remaining > 0, change cannot be made
if (remaining > 0)
{
return -1;
}
return count;
}
int main()
{
int coins1[] = {1, 5, 10, 25};
int amount1 = 30;
int result1 = greedyChange(coins1, 4, amount1);
printf("Greedy: minimum coins for %d = %d\n", amount1, result1);
// counterexample: greedy fails
int coins2[] = {1, 3, 4};
int amount2 = 6;
int greedyResult = greedyChange(coins2, 3, amount2);
printf("Greedy for {1,3,4} amount=%d = %d (optimal is 2)\n",
amount2, greedyResult);
return 0;
}
运行该程序将输出
Greedy: minimum coins for 30 = 2
Greedy for {1,3,4} amount=6 = 3 (optimal is 2)
Python 实现
def greedy_change(coins, amount):
# sort coins in descending order
sorted_coins = sorted(coins, reverse=True)
count = 0
remaining = amount
for coin in sorted_coins:
# use as many of this coin as possible
while remaining >= coin:
remaining -= coin
count += 1
# if remaining > 0, change cannot be made
if remaining > 0:
return -1
return count
coins1 = [1, 5, 10, 25]
amount1 = 30
print(f"Greedy: minimum coins for {amount1} = {greedy_change(coins1, amount1)}")
# counterexample: greedy fails
coins2 = [1, 3, 4]
amount2 = 6
greedy_result = greedy_change(coins2, amount2)
print(f"Greedy for {{1,3,4}} amount={amount2} = {greedy_result} (optimal is 2)")
运行该程序将输出
Greedy: minimum coins for 30 = 2
Greedy for {1,3,4} amount=6 = 3 (optimal is 2)
Go 实现
package main
import (
"fmt"
"sort"
)
func greedyChange(coins []int, amount int) int {
// sort coins in descending order
sort.Sort(sort.Reverse(sort.IntSlice(coins)))
count := 0
remaining := amount
for _, coin := range coins {
// use as many of this coin as possible
for remaining >= coin {
remaining -= coin
count++
}
}
// if remaining > 0, change cannot be made
if remaining > 0 {
return -1
}
return count
}
func main() {
coins1 := []int{1, 5, 10, 25}
amount1 := 30
fmt.Printf("Greedy: minimum coins for %d = %d\n", amount1,
greedyChange(coins1, amount1))
// counterexample: greedy fails
coins2 := []int{1, 3, 4}
amount2 := 6
greedyResult := greedyChange(coins2, amount2)
fmt.Printf("Greedy for {1,3,4} amount=%d = %d (optimal is 2)\n",
amount2, greedyResult)
}
运行该程序将输出
Greedy: minimum coins for 30 = 2
Greedy for {1,3,4} amount=6 = 3 (optimal is 2)
Go 使用 sort.Reverse(sort.IntSlice(...)) 实现降序排序,简洁直观。sort.IntSlice 将切片包装为可排序接口,sort.Reverse 反转排序方向。注意 greedyChange 函数会修改传入的切片顺序。
动态规划法 — 最少硬币数
动态规划(Dynamic Programming,简称 DP)是解决找零问题的标准方法。核心思路是将问题分解为子问题,并通过记忆化避免重复计算。
定义状态转移方程(state transition equation):
dp[i]表示凑出金额i所需的最少硬币数dp[0] = 0(金额为 0 不需要硬币)dp[i] = min(dp[i - coin] + 1),对每个coin遍历取最小值
以硬币 {1, 3, 4}、金额 6 为例,DP 表如下:
金额: 0 1 2 3 4 5 6
dp值: 0 1 2 1 1 2 2
解释:
- dp[0] = 0(基准)
- dp[1] = dp[0] + 1 = 1(用 1 枚 1)
- dp[2] = dp[1] + 1 = 2(用 2 枚 1)
- dp[3] = min(dp[2]+1, dp[0]+1) = 1(用 1 枚 3)
- dp[4] = min(dp[3]+1, dp[1]+1, dp[0]+1) = 1(用 1 枚 4)
- dp[5] = min(dp[4]+1, dp[2]+1, dp[1]+1) = 2(用 4+1 或 3+1+1)
- dp[6] = min(dp[5]+1, dp[3]+1, dp[2]+1) = 2(用 3+3)
C++ 实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
int minCoins(const std::vector<int>& coins, int amount)
{
// dp[i] = minimum coins to make amount i
std::vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
for (int coin : coins)
{
if (coin <= i && dp[i - coin] != INT_MAX)
{
dp[i] = std::min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
int main()
{
std::vector<int> coins1 = {1, 3, 4};
int amount1 = 6;
std::cout << "Minimum coins for " << amount1
<< " = " << minCoins(coins1, amount1) << "\n";
std::vector<int> coins2 = {1, 5, 10, 25};
int amount2 = 30;
std::cout << "Minimum coins for " << amount2
<< " = " << minCoins(coins2, amount2) << "\n";
return 0;
}
运行该程序将输出
Minimum coins for 6 = 2
Minimum coins for 30 = 2
C 实现
#include <stdio.h>
#include <limits.h>
int minCoins(int coins[], int n, int amount)
{
// dp[i] = minimum coins to make amount i
int dp[amount + 1];
for (int i = 0; i <= amount; i++)
{
dp[i] = INT_MAX;
}
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
for (int j = 0; j < n; j++)
{
if (coins[j] <= i && dp[i - coins[j]] != INT_MAX)
{
int val = dp[i - coins[j]] + 1;
if (val < dp[i])
{
dp[i] = val;
}
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
int main()
{
int coins1[] = {1, 3, 4};
printf("Minimum coins for 6 = %d\n", minCoins(coins1, 3, 6));
int coins2[] = {1, 5, 10, 25};
printf("Minimum coins for 30 = %d\n", minCoins(coins2, 4, 30));
return 0;
}
运行该程序将输出
Minimum coins for 6 = 2
Minimum coins for 30 = 2
Python 实现
def min_coins(coins, amount):
# dp[i] = minimum coins to make amount i
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i:
dp[i] = min(dp[i], dp[i - coin] + 1)
return -1 if dp[amount] == float('inf') else dp[amount]
coins1 = [1, 3, 4]
print(f"Minimum coins for 6 = {min_coins(coins1, 6)}")
coins2 = [1, 5, 10, 25]
print(f"Minimum coins for 30 = {min_coins(coins2, 30)}")
运行该程序将输出
Minimum coins for 6 = 2
Minimum coins for 30 = 2
Go 实现
package main
import (
"fmt"
"math"
)
func minCoins(coins []int, amount int) int {
// dp[i] = minimum coins to make amount i
dp := make([]int, amount+1)
for i := range dp {
dp[i] = math.MaxInt32
}
dp[0] = 0
for i := 1; i <= amount; i++ {
for _, coin := range coins {
if coin <= i && dp[i-coin] != math.MaxInt32 {
if dp[i-coin]+1 < dp[i] {
dp[i] = dp[i-coin] + 1
}
}
}
}
if dp[amount] == math.MaxInt32 {
return -1
}
return dp[amount]
}
func main() {
coins1 := []int{1, 3, 4}
fmt.Printf("Minimum coins for 6 = %d\n", minCoins(coins1, 6))
coins2 := []int{1, 5, 10, 25}
fmt.Printf("Minimum coins for 30 = %d\n", minCoins(coins2, 30))
}
运行该程序将输出
Minimum coins for 6 = 2
Minimum coins for 30 = 2
Go 中使用 math.MaxInt32 表示无穷大,通过 make([]int, amount+1) 创建 DP 数组。Go 的 range 遍历切片语法简洁,无需手动管理索引。
动态规划法 — 找零方案还原
仅仅知道最少硬币数还不够,通常还需要知道具体使用了哪些硬币。我们可以通过一个额外的数组 parent 来追踪每一步使用了哪个硬币,然后反向还原出具体的硬币组合。
思路:
parent[i]记录凑出金额i时最后使用的那枚硬币面额- 从
amount出发,每次减去parent[amount],直到金额为 0 - 收集经过的所有硬币即为所求方案
C++ 实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
#include <map>
std::vector<int> minCoinsWithChange(const std::vector<int>& coins, int amount)
{
// dp[i] = minimum coins to make amount i
std::vector<int> dp(amount + 1, INT_MAX);
// parent[i] = coin used to reach amount i
std::vector<int> parent(amount + 1, -1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
for (int coin : coins)
{
if (coin <= i && dp[i - coin] != INT_MAX)
{
if (dp[i - coin] + 1 < dp[i])
{
dp[i] = dp[i - coin] + 1;
parent[i] = coin;
}
}
}
}
// reconstruct the coin combination
std::vector<int> result;
if (dp[amount] == INT_MAX)
{
return result; // empty = no solution
}
int curr = amount;
while (curr > 0)
{
result.push_back(parent[curr]);
curr -= parent[curr];
}
return result;
}
int main()
{
std::vector<int> coins = {1, 5, 10, 25};
int amount = 63;
std::vector<int> used = minCoinsWithChange(coins, amount);
// count each coin type
std::map<int, int, std::greater<int>> coinCount;
for (int c : used)
{
coinCount[c]++;
}
std::cout << "Amount = " << amount << "\n";
std::cout << "Minimum coins = " << used.size() << "\n";
std::cout << "Coins used: ";
for (auto& [coin, cnt] : coinCount)
{
std::cout << coin << " x " << cnt << " ";
}
std::cout << "\n";
return 0;
}
运行该程序将输出
Amount = 63
Minimum coins = 6
Coins used: 25 x 2 10 x 1 1 x 3
C 实现
#include <stdio.h>
#include <limits.h>
void minCoinsWithChange(int coins[], int n, int amount)
{
int dp[amount + 1];
int parent[amount + 1];
for (int i = 0; i <= amount; i++)
{
dp[i] = INT_MAX;
parent[i] = -1;
}
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
for (int j = 0; j < n; j++)
{
if (coins[j] <= i && dp[i - coins[j]] != INT_MAX)
{
if (dp[i - coins[j]] + 1 < dp[i])
{
dp[i] = dp[i - coins[j]] + 1;
parent[i] = coins[j];
}
}
}
}
if (dp[amount] == INT_MAX)
{
printf("No solution\n");
return;
}
// count each coin type
int count[amount + 1];
for (int i = 0; i <= amount; i++) count[i] = 0;
int totalCoins = 0;
int curr = amount;
while (curr > 0)
{
count[parent[curr]]++;
curr -= parent[curr];
totalCoins++;
}
printf("Amount = %d\n", amount);
printf("Minimum coins = %d\n", totalCoins);
printf("Coins used: ");
for (int i = amount; i >= 1; i--)
{
if (count[i] > 0)
{
printf("%d x %d ", i, count[i]);
}
}
printf("\n");
}
int main()
{
int coins[] = {1, 5, 10, 25};
minCoinsWithChange(coins, 4, 63);
return 0;
}
运行该程序将输出
Amount = 63
Minimum coins = 6
Coins used: 25 x 2 10 x 1 1 x 3
Python 实现
def min_coins_with_change(coins, amount):
# dp[i] = minimum coins to make amount i
dp = [float('inf')] * (amount + 1)
# parent[i] = coin used to reach amount i
parent = [-1] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i and dp[i - coin] + 1 < dp[i]:
dp[i] = dp[i - coin] + 1
parent[i] = coin
if dp[amount] == float('inf'):
return [], -1
# reconstruct the coin combination
used = []
curr = amount
while curr > 0:
used.append(parent[curr])
curr -= parent[curr]
return used, dp[amount]
coins = [1, 5, 10, 25]
amount = 63
used, total = min_coins_with_change(coins, amount)
# count each coin type
coin_count = {}
for c in used:
coin_count[c] = coin_count.get(c, 0) + 1
print(f"Amount = {amount}")
print(f"Minimum coins = {total}")
print("Coins used:", " ".join(f"{c} x {cnt}" for c, cnt in sorted(coin_count.items(), reverse=True)))
运行该程序将输出
Amount = 63
Minimum coins = 6
Coins used: 25 x 2 10 x 1 1 x 3
Go 实现
package main
import (
"fmt"
"math"
"sort"
)
func minCoinsWithChange(coins []int, amount int) ([]int, int) {
// dp[i] = minimum coins to make amount i
dp := make([]int, amount+1)
// parent[i] = coin used to reach amount i
parent := make([]int, amount+1)
for i := range dp {
dp[i] = math.MaxInt32
parent[i] = -1
}
dp[0] = 0
for i := 1; i <= amount; i++ {
for _, coin := range coins {
if coin <= i && dp[i-coin] != math.MaxInt32 {
if dp[i-coin]+1 < dp[i] {
dp[i] = dp[i-coin] + 1
parent[i] = coin
}
}
}
}
if dp[amount] == math.MaxInt32 {
return nil, -1
}
// reconstruct the coin combination
var used []int
curr := amount
for curr > 0 {
used = append(used, parent[curr])
curr -= parent[curr]
}
return used, dp[amount]
}
func main() {
coins := []int{1, 5, 10, 25}
amount := 63
used, total := minCoinsWithChange(coins, amount)
// count each coin type
coinCount := make(map[int]int)
for _, c := range used {
coinCount[c]++
}
// sort keys in descending order for display
var keys []int
for k := range coinCount {
keys = append(keys, k)
}
sort.Sort(sort.Reverse(sort.IntSlice(keys)))
fmt.Printf("Amount = %d\n", amount)
fmt.Printf("Minimum coins = %d\n", total)
fmt.Print("Coins used: ")
for _, k := range keys {
fmt.Printf("%d x %d ", k, coinCount[k])
}
fmt.Println()
}
运行该程序将输出
Amount = 63
Minimum coins = 6
Coins used: 25 x 2 10 x 1 1 x 3
parent 数组记录了每一步所使用的硬币面额。还原时从目标金额出发,每次减去对应硬币面额,直到金额归零。63 = 25 + 25 + 10 + 1 + 1 + 1,共 6 枚硬币。
动态规划法 — 组合数
组合数(Number of Combinations)问题:给定硬币面额,求凑出目标金额共有多少种不同的组合方式。注意,这里的组合区分顺序,1+2+2 和 2+1+2 视为不同组合;但使用外层循环遍历硬币、内层循环遍历金额的写法,可以保证每种组合只计数一次(即按硬币种类有序处理)。
状态转移方程:
count[i]表示凑出金额i的组合数count[0] = 1(金额为 0 有 1 种方式:不选任何硬币)count[i] += count[i - coin],对每个coin累加
以硬币 {1, 2, 5}、金额 5 为例,共有 4 种方式:
- 1 + 1 + 1 + 1 + 1
- 1 + 1 + 1 + 2
- 1 + 2 + 2
- 5
C++ 实现
#include <iostream>
#include <vector>
long long countCombinations(const std::vector<int>& coins, int amount)
{
// count[i] = number of ways to make amount i
std::vector<long long> count(amount + 1, 0);
count[0] = 1;
// outer loop: coins (ensures each combination counted once)
for (int coin : coins)
{
for (int i = coin; i <= amount; i++)
{
count[i] += count[i - coin];
}
}
return count[amount];
}
int main()
{
std::vector<int> coins = {1, 2, 5};
int amount = 5;
long long ways = countCombinations(coins, amount);
std::cout << "Number of ways to make " << amount
<< " = " << ways << "\n";
return 0;
}
运行该程序将输出
Number of ways to make 5 = 4
C 实现
#include <stdio.h>
long long countCombinations(int coins[], int n, int amount)
{
// count[i] = number of ways to make amount i
long long count[amount + 1];
for (int i = 0; i <= amount; i++)
{
count[i] = 0;
}
count[0] = 1;
// outer loop: coins (ensures each combination counted once)
for (int j = 0; j < n; j++)
{
for (int i = coins[j]; i <= amount; i++)
{
count[i] += count[i - coins[j]];
}
}
return count[amount];
}
int main()
{
int coins[] = {1, 2, 5};
long long ways = countCombinations(coins, 3, 5);
printf("Number of ways to make 5 = %lld\n", ways);
return 0;
}
运行该程序将输出
Number of ways to make 5 = 4
Python 实现
def count_combinations(coins, amount):
# count[i] = number of ways to make amount i
count = [0] * (amount + 1)
count[0] = 1
# outer loop: coins (ensures each combination counted once)
for coin in coins:
for i in range(coin, amount + 1):
count[i] += count[i - coin]
return count[amount]
coins = [1, 2, 5]
amount = 5
print(f"Number of ways to make {amount} = {count_combinations(coins, amount)}")
运行该程序将输出
Number of ways to make 5 = 4
Go 实现
package main
import "fmt"
func countCombinations(coins []int, amount int) int64 {
// count[i] = number of ways to make amount i
count := make([]int64, amount+1)
count[0] = 1
// outer loop: coins (ensures each combination counted once)
for _, coin := range coins {
for i := coin; i <= amount; i++ {
count[i] += count[i-coin]
}
}
return count[amount]
}
func main() {
coins := []int{1, 2, 5}
amount := 5
fmt.Printf("Number of ways to make %d = %d\n", amount,
countCombinations(coins, amount))
}
运行该程序将输出
Number of ways to make 5 = 4
注意外层循环遍历硬币、内层循环遍历金额的顺序至关重要。这种顺序保证了每种硬币按类型依次加入,避免了相同硬币组合因顺序不同被重复计数。使用 long long / int64 是因为组合数可能增长很快。
递归法(带记忆化)
递归法(Recursive with Memoization)采用自顶向下的方式求解。基本思路是将目标金额递归分解为子问题,同时用哈希表或数组缓存已计算的结果,避免重复计算。
递归关系:
solve(amount)=min(solve(amount - coin) + 1),对所有有效的coin- 基准情形:
amount == 0返回 0,amount < 0返回无穷大
C++ 实现
#include <iostream>
#include <vector>
#include <climits>
#include <unordered_map>
class CoinChangeMemo
{
private:
std::unordered_map<int, int> memo;
public:
int solve(const std::vector<int>& coins, int amount)
{
// base case
if (amount == 0) return 0;
if (amount < 0) return INT_MAX;
// check memo
if (memo.find(amount) != memo.end())
{
return memo[amount];
}
int minResult = INT_MAX;
for (int coin : coins)
{
int sub = solve(coins, amount - coin);
if (sub != INT_MAX)
{
minResult = std::min(minResult, sub + 1);
}
}
memo[amount] = minResult;
return minResult;
}
};
int main()
{
CoinChangeMemo solver;
std::vector<int> coins1 = {1, 3, 4};
int result1 = solver.solve(coins1, 6);
std::cout << "Minimum coins for 6 = "
<< (result1 == INT_MAX ? -1 : result1) << "\n";
CoinChangeMemo solver2;
std::vector<int> coins2 = {1, 5, 10, 25};
int result2 = solver2.solve(coins2, 30);
std::cout << "Minimum coins for 30 = "
<< (result2 == INT_MAX ? -1 : result2) << "\n";
return 0;
}
运行该程序将输出
Minimum coins for 6 = 2
Minimum coins for 30 = 2
Python 实现
def min_coins_memo(coins, amount, memo=None):
if memo is None:
memo = {}
# base case
if amount == 0:
return 0
if amount < 0:
return float('inf')
# check memo
if amount in memo:
return memo[amount]
min_result = float('inf')
for coin in coins:
sub = min_coins_memo(coins, amount - coin, memo)
if sub != float('inf'):
min_result = min(min_result, sub + 1)
memo[amount] = min_result
return min_result
coins1 = [1, 3, 4]
r1 = min_coins_memo(coins1, 6)
print(f"Minimum coins for 6 = {-1 if r1 == float('inf') else r1}")
coins2 = [1, 5, 10, 25]
r2 = min_coins_memo(coins2, 30)
print(f"Minimum coins for 30 = {-1 if r2 == float('inf') else r2}")
运行该程序将输出
Minimum coins for 6 = 2
Minimum coins for 30 = 2
Go 实现
package main
import (
"fmt"
"math"
)
func minCoinsMemo(coins []int, amount int, memo map[int]int) int {
// base case
if amount == 0 {
return 0
}
if amount < 0 {
return math.MaxInt32
}
// check memo
if val, ok := memo[amount]; ok {
return val
}
minResult := math.MaxInt32
for _, coin := range coins {
sub := minCoinsMemo(coins, amount-coin, memo)
if sub != math.MaxInt32 {
if sub+1 < minResult {
minResult = sub + 1
}
}
}
memo[amount] = minResult
return minResult
}
func main() {
memo1 := make(map[int]int)
coins1 := []int{1, 3, 4}
r1 := minCoinsMemo(coins1, 6, memo1)
if r1 == math.MaxInt32 {
r1 = -1
}
fmt.Printf("Minimum coins for 6 = %d\n", r1)
memo2 := make(map[int]int)
coins2 := []int{1, 5, 10, 25}
r2 := minCoinsMemo(coins2, 30, memo2)
if r2 == math.MaxInt32 {
r2 = -1
}
fmt.Printf("Minimum coins for 30 = %d\n", r2)
}
运行该程序将输出
Minimum coins for 6 = 2
Minimum coins for 30 = 2
递归法带记忆化与自底向上的动态规划在本质上是相同的——都是通过避免重复计算来优化性能。区别在于递归法从目标金额出发,按需计算子问题;而动态规划从小到大依次填表。在实际应用中,动态规划通常更高效,因为不存在函数调用开销。
完整实现
下面将所有方法整合到一个完整程序中,方便对比不同算法的结果。
C++ 实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
#include <unordered_map>
#include <map>
// --- Greedy ---
int greedyChange(std::vector<int> coins, int amount)
{
std::sort(coins.begin(), coins.end(), std::greater<int>());
int count = 0;
int remaining = amount;
for (int coin : coins)
{
while (remaining >= coin)
{
remaining -= coin;
count++;
}
}
return remaining == 0 ? count : -1;
}
// --- DP: minimum coins ---
int dpMinCoins(const std::vector<int>& coins, int amount)
{
std::vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
for (int coin : coins)
{
if (coin <= i && dp[i - coin] != INT_MAX)
{
dp[i] = std::min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
// --- DP: coin combination reconstruction ---
std::vector<int> dpWithChange(const std::vector<int>& coins, int amount)
{
std::vector<int> dp(amount + 1, INT_MAX);
std::vector<int> parent(amount + 1, -1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
for (int coin : coins)
{
if (coin <= i && dp[i - coin] != INT_MAX)
{
if (dp[i - coin] + 1 < dp[i])
{
dp[i] = dp[i - coin] + 1;
parent[i] = coin;
}
}
}
}
std::vector<int> result;
if (dp[amount] == INT_MAX) return result;
int curr = amount;
while (curr > 0)
{
result.push_back(parent[curr]);
curr -= parent[curr];
}
return result;
}
// --- DP: combination count ---
long long dpCountCombinations(const std::vector<int>& coins, int amount)
{
std::vector<long long> count(amount + 1, 0);
count[0] = 1;
for (int coin : coins)
{
for (int i = coin; i <= amount; i++)
{
count[i] += count[i - coin];
}
}
return count[amount];
}
// --- Recursive with memoization ---
int memoSolve(const std::vector<int>& coins, int amount, std::unordered_map<int, int>& memo)
{
if (amount == 0) return 0;
if (amount < 0) return INT_MAX;
if (memo.count(amount)) return memo[amount];
int best = INT_MAX;
for (int coin : coins)
{
int sub = memoSolve(coins, amount - coin, memo);
if (sub != INT_MAX) best = std::min(best, sub + 1);
}
memo[amount] = best;
return best;
}
// --- Main ---
int main()
{
std::vector<int> coins = {1, 5, 10, 25};
int amount = 63;
std::cout << "=== Making Change: amount = " << amount << " ===\n";
std::cout << "Coins: {1, 5, 10, 25}\n\n";
// greedy
int greedy = greedyChange(coins, amount);
std::cout << "Greedy: " << greedy << " coins\n";
// DP min coins
int dpMin = dpMinCoins(coins, amount);
std::cout << "DP (minimum coins): " << dpMin << " coins\n";
// DP with change
std::vector<int> used = dpWithChange(coins, amount);
std::map<int, int, std::greater<int>> coinCount;
for (int c : used) coinCount[c]++;
std::cout << "DP (coin breakdown): ";
for (auto& [coin, cnt] : coinCount) std::cout << coin << "x" << cnt << " ";
std::cout << "\n";
// DP combination count
long long ways = dpCountCombinations(coins, amount);
std::cout << "DP (combinations): " << ways << " ways\n";
// Recursive memoization
std::unordered_map<int, int> memo;
int memoResult = memoSolve(coins, amount, memo);
std::cout << "Recursive+Memo: " << (memoResult == INT_MAX ? -1 : memoResult) << " coins\n";
return 0;
}
运行该程序将输出
=== Making Change: amount = 63 ===
Coins: {1, 5, 10, 25}
Greedy: 6 coins
DP (minimum coins): 6 coins
DP (coin breakdown): 25x2 10x1 1x3
DP (combinations): 37 ways
Recursive+Memo: 6 coins
C 实现
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
// --- comparison for descending sort ---
int cmpDesc(const void* a, const void* b) { return *(int*)b - *(int*)a; }
// --- Greedy ---
int greedyChange(int* coins, int n, int amount)
{
qsort(coins, n, sizeof(int), cmpDesc);
int count = 0, remaining = amount;
for (int i = 0; i < n; i++)
{
while (remaining >= coins[i]) { remaining -= coins[i]; count++; }
}
return remaining == 0 ? count : -1;
}
// --- DP: minimum coins ---
int dpMinCoins(int coins[], int n, int amount)
{
int dp[amount + 1];
for (int i = 0; i <= amount; i++) dp[i] = INT_MAX;
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
for (int j = 0; j < n; j++)
{
if (coins[j] <= i && dp[i - coins[j]] != INT_MAX)
{
int val = dp[i - coins[j]] + 1;
if (val < dp[i]) dp[i] = val;
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
// --- DP: combination count ---
long long dpCountCombinations(int coins[], int n, int amount)
{
long long count[amount + 1];
memset(count, 0, sizeof(count));
count[0] = 1;
for (int j = 0; j < n; j++)
{
for (int i = coins[j]; i <= amount; i++)
{
count[i] += count[i - coins[j]];
}
}
return count[amount];
}
// --- Recursive with memoization ---
int memoSolve(int coins[], int n, int amount, int* memo)
{
if (amount == 0) return 0;
if (amount < 0) return INT_MAX;
if (memo[amount] != -1) return memo[amount];
int best = INT_MAX;
for (int j = 0; j < n; j++)
{
int sub = memoSolve(coins, n, amount - coins[j], memo);
if (sub != INT_MAX && sub + 1 < best) best = sub + 1;
}
memo[amount] = best;
return best;
}
int main()
{
int coins[] = {1, 5, 10, 25};
int n = 4, amount = 63;
printf("=== Making Change: amount = %d ===\n", amount);
printf("Coins: {1, 5, 10, 25}\n\n");
// greedy (make a copy because qsort modifies the array)
int coinsCopy[4];
memcpy(coinsCopy, coins, sizeof(coins));
printf("Greedy: %d coins\n", greedyChange(coinsCopy, n, amount));
// DP min coins
printf("DP (minimum coins): %d coins\n", dpMinCoins(coins, n, amount));
// DP combination count
printf("DP (combinations): %lld ways\n", dpCountCombinations(coins, n, amount));
// Recursive memoization
int memo[amount + 1];
memset(memo, -1, sizeof(memo));
int memoResult = memoSolve(coins, n, amount, memo);
printf("Recursive+Memo: %d coins\n", memoResult == INT_MAX ? -1 : memoResult);
return 0;
}
运行该程序将输出
=== Making Change: amount = 63 ===
Coins: {1, 5, 10, 25}
Greedy: 6 coins
DP (minimum coins): 6 coins
DP (combinations): 37 ways
Recursive+Memo: 6 coins
Python 实现
# --- Greedy ---
def greedy_change(coins, amount):
sorted_coins = sorted(coins, reverse=True)
count, remaining = 0, amount
for coin in sorted_coins:
while remaining >= coin:
remaining -= coin
count += 1
return count if remaining == 0 else -1
# --- DP: minimum coins ---
def dp_min_coins(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i:
dp[i] = min(dp[i], dp[i - coin] + 1)
return -1 if dp[amount] == float('inf') else dp[amount]
# --- DP: coin combination reconstruction ---
def dp_with_change(coins, amount):
dp = [float('inf')] * (amount + 1)
parent = [-1] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i and dp[i - coin] + 1 < dp[i]:
dp[i] = dp[i - coin] + 1
parent[i] = coin
if dp[amount] == float('inf'):
return [], -1
used, curr = [], amount
while curr > 0:
used.append(parent[curr])
curr -= parent[curr]
return used, dp[amount]
# --- DP: combination count ---
def dp_count_combinations(coins, amount):
count = [0] * (amount + 1)
count[0] = 1
for coin in coins:
for i in range(coin, amount + 1):
count[i] += count[i - coin]
return count[amount]
# --- Recursive with memoization ---
def memo_solve(coins, amount, memo=None):
if memo is None:
memo = {}
if amount == 0:
return 0
if amount < 0:
return float('inf')
if amount in memo:
return memo[amount]
best = float('inf')
for coin in coins:
sub = memo_solve(coins, amount - coin, memo)
if sub != float('inf'):
best = min(best, sub + 1)
memo[amount] = best
return best
coins = [1, 5, 10, 25]
amount = 63
print(f"=== Making Change: amount = {amount} ===")
print("Coins: {1, 5, 10, 25}\n")
print(f"Greedy: {greedy_change(coins, amount)} coins")
print(f"DP (minimum coins): {dp_min_coins(coins, amount)} coins")
used, total = dp_with_change(coins, amount)
coin_count = {}
for c in used:
coin_count[c] = coin_count.get(c, 0) + 1
breakdown = " ".join(f"{c}x{cnt}" for c, cnt in sorted(coin_count.items(), reverse=True))
print(f"DP (coin breakdown): {breakdown}")
print(f"DP (combinations): {dp_count_combinations(coins, amount)} ways")
print(f"Recursive+Memo: {memo_solve(coins, amount)} coins")
运行该程序将输出
=== Making Change: amount = 63 ===
Coins: {1, 5, 10, 25}
Greedy: 6 coins
DP (minimum coins): 6 coins
DP (coin breakdown): 25x2 10x1 1x3
DP (combinations): 37 ways
Recursive+Memo: 6 coins
Go 实现
package main
import (
"fmt"
"math"
"sort"
)
// --- Greedy ---
func greedyChange(coins []int, amount int) int {
sorted := make([]int, len(coins))
copy(sorted, coins)
sort.Sort(sort.Reverse(sort.IntSlice(sorted)))
count, remaining := 0, amount
for _, coin := range sorted {
for remaining >= coin {
remaining -= coin
count++
}
}
if remaining != 0 {
return -1
}
return count
}
// --- DP: minimum coins ---
func dpMinCoins(coins []int, amount int) int {
dp := make([]int, amount+1)
for i := range dp {
dp[i] = math.MaxInt32
}
dp[0] = 0
for i := 1; i <= amount; i++ {
for _, coin := range coins {
if coin <= i && dp[i-coin] != math.MaxInt32 {
if dp[i-coin]+1 < dp[i] {
dp[i] = dp[i-coin] + 1
}
}
}
}
if dp[amount] == math.MaxInt32 {
return -1
}
return dp[amount]
}
// --- DP: coin combination reconstruction ---
func dpWithChange(coins []int, amount int) ([]int, int) {
dp := make([]int, amount+1)
parent := make([]int, amount+1)
for i := range dp {
dp[i] = math.MaxInt32
parent[i] = -1
}
dp[0] = 0
for i := 1; i <= amount; i++ {
for _, coin := range coins {
if coin <= i && dp[i-coin] != math.MaxInt32 {
if dp[i-coin]+1 < dp[i] {
dp[i] = dp[i-coin] + 1
parent[i] = coin
}
}
}
}
if dp[amount] == math.MaxInt32 {
return nil, -1
}
var used []int
curr := amount
for curr > 0 {
used = append(used, parent[curr])
curr -= parent[curr]
}
return used, dp[amount]
}
// --- DP: combination count ---
func dpCountCombinations(coins []int, amount int) int64 {
count := make([]int64, amount+1)
count[0] = 1
for _, coin := range coins {
for i := coin; i <= amount; i++ {
count[i] += count[i-coin]
}
}
return count[amount]
}
// --- Recursive with memoization ---
func memoSolve(coins []int, amount int, memo map[int]int) int {
if amount == 0 {
return 0
}
if amount < 0 {
return math.MaxInt32
}
if val, ok := memo[amount]; ok {
return val
}
best := math.MaxInt32
for _, coin := range coins {
sub := memoSolve(coins, amount-coin, memo)
if sub != math.MaxInt32 && sub+1 < best {
best = sub + 1
}
}
memo[amount] = best
return best
}
func main() {
coins := []int{1, 5, 10, 25}
amount := 63
fmt.Printf("=== Making Change: amount = %d ===\n", amount)
fmt.Println("Coins: {1, 5, 10, 25}\n")
fmt.Printf("Greedy: %d coins\n", greedyChange(coins, amount))
fmt.Printf("DP (minimum coins): %d coins\n", dpMinCoins(coins, amount))
used, _ := dpWithChange(coins, amount)
coinCount := make(map[int]int)
for _, c := range used {
coinCount[c]++
}
var keys []int
for k := range coinCount {
keys = append(keys, k)
}
sort.Sort(sort.Reverse(sort.IntSlice(keys)))
breakdown := ""
for _, k := range keys {
breakdown += fmt.Sprintf("%dx%d ", k, coinCount[k])
}
fmt.Printf("DP (coin breakdown): %s\n", breakdown)
fmt.Printf("DP (combinations): %d ways\n", dpCountCombinations(coins, amount))
memo := make(map[int]int)
memoResult := memoSolve(coins, amount, memo)
if memoResult == math.MaxInt32 {
memoResult = -1
}
fmt.Printf("Recursive+Memo: %d coins\n", memoResult)
}
运行该程序将输出
=== Making Change: amount = 63 ===
Coins: {1, 5, 10, 25}
Greedy: 6 coins
DP (minimum coins): 6 coins
DP (coin breakdown): 25x2 10x1 1x3
DP (combinations): 37 ways
Recursive+Memo: 6 coins
所有方法在标准美分系统 {1, 5, 10, 25} 上结果一致:金额 63 需要 6 枚硬币(25 + 25 + 10 + 1 + 1 + 1)。贪心法在此币种下恰好能给出最优解,但如前所述,这并不总是成立。
找零问题的性质
贪心法 vs 动态规划法
| 特性 | 贪心法(Greedy) | 动态规划法(DP) |
|---|---|---|
| 正确性 | 仅对规范币种有效 | 对任意币种均有效 |
| 时间复杂度 | O(n log n + amount) | O(n x amount) |
| 空间复杂度 | O(1) 或 O(n)(排序) | O(amount) |
| 实现难度 | 简单 | 中等 |
| 适用场景 | 美分系统等规范币种 | 通用场景 |
| 组合计数 | 不支持 | 支持 |
其中 n 为硬币种类数,amount 为目标金额。
贪心法何时有效
贪心法在以下币种系统中保证最优解:
- 美分系统
- 欧元硬币
- 人民币硬币
这类币种系统称为规范硬币系统(Canonical Coin Systems)。其特征是:每种面额都是比它小的面额的倍数关系或满足特定数学性质,使得贪心选择始终安全。判断一个硬币系统是否为规范系统是一个已研究的问题,没有简单的通用判定方法。
应用场景
| 场景 | 说明 |
|---|---|
| 自动售货机 | 根据投入金额和商品价格计算找零方案 |
| 货币系统设计 | 设计新的硬币面额使找零效率最高 |
| 资源分配 | 将资源按最小单元分配,类似装箱问题 |
| 时间分配 | 将时间段按固定粒度划分(如课程排期) |
| 密码学 | 背包密码系统(Knapsack Cryptosystem)的理论基础 |
复杂度总结
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 贪心法 | O(n log n + amount) | O(1) |
| DP 最少硬币 | O(n x amount) | O(amount) |
| DP 方案还原 | O(n x amount) | O(amount) |
| DP 组合计数 | O(n x amount) | O(amount) |
| 递归 + 记忆化 | O(n x amount) | O(amount) |

浙公网安备 33010602011771号