零钱兑换问题分析

零钱兑换问题(Coin Change)是一个经典的动态规划问题,目标是找出组成给定金额所需的最少硬币数量。

问题描述

给定不同面额的硬币 coins 和一个总金额 amount,计算可以凑成总金额所需的最少硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

假设: - 硬币数量无限 - 硬币面额都是正整数 - 总金额 amount 是非负整数

方法一:暴力递归(回溯法)

思路分析

通过递归尝试所有可能的硬币组合,找出使用硬币数量最少的方案。

时间复杂度

O(Sⁿ),其中 S 是金额,n 是硬币种类数

代码实现

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>

using namespace std;

// 方法一:暴力递归
class Solution1 {
public:
    int coinChange(vector<int>& coins, int amount) {
        if (amount < 0) return -1;
        if (amount == 0) return 0;

        int minCoins = INT_MAX;

        for (int coin : coins) {
            // 尝试使用当前硬币
            int res = coinChange(coins, amount - coin);
            if (res >= 0) {
                minCoins = min(minCoins, res + 1);
            }
        }

        return minCoins == INT_MAX ? -1 : minCoins;
    }
};

方法二:记忆化递归(自上而下)

思路分析

在暴力递归的基础上,使用记忆化技术存储已经计算过的子问题结果,避免重复计算。

时间复杂度

O(S * n),其中 S 是金额,n 是硬币种类数

代码实现

// 方法二:记忆化递归
class Solution2 {
public:
    int coinChange(vector<int>& coins, int amount) {
        // memo[i] 表示组成金额 i 所需的最少硬币数
        vector<int> memo(amount + 1, -2); // -2 表示未计算
        return dfs(coins, amount, memo);
    }

private:
    int dfs(vector<int>& coins, int amount, vector<int>& memo) {
        if (amount < 0) return -1;
        if (amount == 0) return 0;

        // 如果已经计算过,直接返回结果
        if (memo[amount] != -2) return memo[amount];

        int minCoins = INT_MAX;

        for (int coin : coins) {
            int res = dfs(coins, amount - coin, memo);
            if (res >= 0) {
                minCoins = min(minCoins, res + 1);
            }
        }

        // 记录计算结果
        memo[amount] = (minCoins == INT_MAX) ? -1 : minCoins;
        return memo[amount];
    }
};

方法三:动态规划(自下而上)

思路分析

使用动态规划数组 dp[i] 表示组成金额 i 所需的最少硬币数。通过递推公式逐步计算。

状态转移方程

dp[i] = min(dp[i], dp[i - coin] + 1),对于所有 coin <= i

时间复杂度

O(S * n),其中 S 是金额,n 是硬币种类数

代码实现

// 方法三:动态规划
class Solution3 {
public:
    int coinChange(vector<int>& coins, int amount) {
        // dp[i] 表示组成金额 i 所需的最少硬币数
        vector<int> dp(amount + 1, amount + 1);

        // 初始化:组成金额 0 需要 0 个硬币
        dp[0] = 0;

        // 计算每个金额的最小硬币数
        for (int i = 1; i <= amount; i++) {
            for (int coin : coins) {
                if (coin <= i) {
                    dp[i] = min(dp[i], dp[i - coin] + 1);
                }
            }
        }

        // 如果 dp[amount] 仍然是初始值,说明无法组成该金额
        return dp[amount] > amount ? -1 : dp[amount];
    }
};

方法四:动态规划优化(使用一维数组)

思路分析

对方法三进行空间优化,但本质上与方法三相同。

代码实现

// 方法四:动态规划优化
class Solution4 {
public:
    int coinChange(vector<int>& coins, int amount) {
        // dp[i] 表示组成金额 i 所需的最少硬币数
        vector<int> dp(amount + 1, amount + 1);
        dp[0] = 0;

        // 优化:先对硬币排序,可以减少部分比较
        sort(coins.begin(), coins.end());

        for (int i = 1; i <= amount; i++) {
            for (int coin : coins) {
                if (coin > i) break; // 硬币面额大于当前金额,无需继续
                dp[i] = min(dp[i], dp[i - coin] + 1);
            }
        }

        return dp[amount] > amount ? -1 : dp[amount];
    }
};

方法五:广度优先搜索(BFS)

思路分析

将问题转化为图论问题:每个节点表示一个金额,从 0 开始,每次添加一枚硬币到达新的金额,使用 BFS 找到到达目标金额的最短路径。

时间复杂度

O(S * n),最坏情况下需要探索所有金额

代码实现

// 方法五:广度优先搜索
class Solution5 {
public:
    int coinChange(vector<int>& coins, int amount) {
        if (amount == 0) return 0;

        queue<int> q;
        vector<bool> visited(amount + 1, false);

        q.push(0);
        visited[0] = true;

        int level = 0;

        while (!q.empty()) {
            int size = q.size();
            level++;

            for (int i = 0; i < size; i++) {
                int current = q.front();
                q.pop();

                for (int coin : coins) {
                    int next = current + coin;

                    if (next == amount) {
                        return level;
                    }

                    if (next < amount && !visited[next]) {
                        visited[next] = true;
                        q.push(next);
                    }
                }
            }
        }

        return -1;
    }
};

方法六:贪心算法 + DFS(需要排序)

思路分析

先使用大面额硬币,再使用小面额硬币,通过深度优先搜索寻找最优解。这种方法不一定能得到最优解,但可以快速找到近似解,适用于某些特定场景。

注意

这种方法不保证总是得到最优解,但在硬币面额设计合理时可以工作。

代码实现

// 方法六:贪心 + DFS(不一定总是最优,但效率高)
class Solution6 {
public:
    int coinChange(vector<int>& coins, int amount) {
        // 按面额从大到小排序
        sort(coins.rbegin(), coins.rend());

        int ans = INT_MAX;
        dfs(coins, amount, 0, 0, ans);
        return ans == INT_MAX ? -1 : ans;
    }

private:
    void dfs(vector<int>& coins, int amount, int index, int count, int& ans) {
        if (amount == 0) {
            ans = min(ans, count);
            return;
        }

        if (index == coins.size()) return;

        // 剪枝:如果当前硬币数量已经超过已知最优解,提前返回
        if (count >= ans) return;

        // 尝试使用尽可能多的当前面额硬币
        for (int k = amount / coins[index]; k >= 0 && count + k < ans; k--) {
            dfs(coins, amount - k * coins[index], index + 1, count + k, ans);
        }
    }
};

测试代码

// 测试函数
void testCoinChange() {
    vector<int> coins1 = {1, 2, 5};
    int amount1 = 11;

    vector<int> coins2 = {2};
    int amount2 = 3;

    vector<int> coins3 = {1, 3, 4};
    int amount3 = 6;

    // 测试不同方法
    Solution1 s1;
    Solution2 s2;
    Solution3 s3;
    Solution4 s4;
    Solution5 s5;
    Solution6 s6;

    cout << "测试用例1: coins = [1, 2, 5], amount = 11" << endl;
    cout << "暴力递归: " << s1.coinChange(coins1, amount1) << endl;
    cout << "记忆化递归: " << s2.coinChange(coins1, amount1) << endl;
    cout << "动态规划: " << s3.coinChange(coins1, amount1) << endl;
    cout << "动态规划优化: " << s4.coinChange(coins1, amount1) << endl;
    cout << "广度优先搜索: " << s5.coinChange(coins1, amount1) << endl;
    cout << "贪心+DFS: " << s6.coinChange(coins1, amount1) << endl;
    cout << endl;

    cout << "测试用例2: coins = [2], amount = 3" << endl;
    cout << "暴力递归: " << s1.coinChange(coins2, amount2) << endl;
    cout << "记忆化递归: " << s2.coinChange(coins2, amount2) << endl;
    cout << "动态规划: " << s3.coinChange(coins2, amount2) << endl;
    cout << "动态规划优化: " << s4.coinChange(coins2, amount2) << endl;
    cout << "广度优先搜索: " << s5.coinChange(coins2, amount2) << endl;
    cout << "贪心+DFS: " << s6.coinChange(coins2, amount2) << endl;
    cout << endl;

    cout << "测试用例3: coins = [1, 3, 4], amount = 6" << endl;
    cout << "暴力递归: " << s1.coinChange(coins3, amount3) << endl;
    cout << "记忆化递归: " << s2.coinChange(coins3, amount3) << endl;
    cout << "动态规划: " << s3.coinChange(coins3, amount3) << endl;
    cout << "动态规划优化: " << s4.coinChange(coins3, amount3) << endl;
    cout << "广度优先搜索: " << s5.coinChange(coins3, amount3) << endl;
    cout << "贪心+DFS: " << s6.coinChange(coins3, amount3) << endl;
}

int main() {
    testCoinChange();
    return 0;
}

总结与比较

方法 时间复杂度 空间复杂度 优点 缺点
暴力递归 O(Sⁿ) O(S) 实现简单 效率极低,重复计算多
记忆化递归 O(S * n) O(S) 避免重复计算 递归深度可能较大
动态规划 O(S * n) O(S) 稳定可靠 可能需要计算所有子问题
动态规划优化 O(S * n) O(S) 代码简洁 同动态规划
广度优先搜索 O(S * n) O(S) 直观,易理解 可能需要探索所有状态
贪心+DFS 不确定 O(S) 在某些情况下快 不保证最优解

推荐使用动态规划(方法三或四),因为它: 1. 保证得到最优解 2. 时间复杂度相对较低 3. 实现简单且稳定

对于特别大的金额,可以考虑使用贪心+DFS方法进行优化,但要注意验证是否适用于具体的硬币面额组合。