算法:零錢兌換

問題

給定不同面額的硬幣(coins)和一個總金額(amount) 。寫一個函數來計算可以湊成總金額所需的最少的硬幣個數,如果沒有任何一種硬幣組合能滿足,返回 -1。

示例1

輸入:coins = [1, 2, 5], amount = 11
輸出:3 (5+5+1)

示例2

輸入:coins = [2], amount = 3
輸出:-1 (無法滿足)

解決方案

暴力破解

暴力破解即窮舉,把各種可能組成總金額的情況都匹配一遍,得到所有滿足的組合,然後取硬幣數量最少的那組。

實現思路

剩餘金額減去當前使用的硬幣金額
如果大於 0 ,繼續使用硬幣來組合;
如果等於 0 ,匹配完成,將當前組使用的硬幣數與最小組合硬幣數對比,取較小者;
如果小於 0 ,直接淘汰。

參考代碼
public int CoinChange(int[] coins, int amount)
{
    // 如果輸入的金額小於等於0,返回0
    if (amount <= 0) return 0;

    // 設置初始值爲 amount + 1,實際不存在這種情況的,最壞的情況是 amount 
    var minCount = amount + 1;

    for (int i = 0; i < coins.Length; i++)
    {
        Cal(amount, coins, coins[i], new List<int>(), ref minCount);
    }
    return minCount == amount + 1 ? -1 : minCount;
}

public void Cal(int amount, int[] coins, int coin, List<int> curCoins, ref int minCount)
{
    // 剩餘金額-使用的硬幣金額, 得到新的剩餘金額
    var leftAmount = amount - coin;

    // 如果等於0,說明找到了一組滿足的組合
    if (leftAmount == 0)
    {
        curCoins.Add(coin);

        // 如果當前組使用的硬幣數量小於當前最小組合的硬幣數量,重置最小值
        if (curCoins.Count < minCount)
        {
            minCount = curCoins.Count;
        }
    }
    // 如果剩餘金額大於0,說明還繼續獲取新的硬幣加入集合
    else if (leftAmount > 0)
    {
        // 如果當前組的總硬幣數量已經大於當前最小組合的硬幣數量,就不需要在往下找了
        if (curCoins.Count >= minCount)
        {
            return;
        }

        // 繼續下一次
        for (int i = 0; i < coins.Length; i++)
        {
            var newCoins = new List<int>(curCoins);
            newCoins.Add(coin);
            Cal(leftAmount, coins, coins[i], newCoins, ref minCount);
        }
    }
}
結論

從上圖可以看出,獲得所有可能組合的路線情況非常多,當 amount 值較小時複雜度還不算明顯,隨着 amount 越大,路線的深度(對應代碼遞歸深度)會指數級增加(時間複雜度:2^n),所以當 amount 較大時這種方式必然不可取。

貪心

一般的貪心算法是先使用大幣值,超界了就改用小幣值,幣值遞減。

本題的幣值是 [1,2,5],必然能用 2 肯定不會用 1,所以貪心沒問題。但如果幣值是 [1,5,6],當要組合總金額爲 20 ,按貪心大幣值的方式 6×3+1×2 = 20,需要使用 5 個硬幣,而如果直接使用 5×4 = 20 只需要 4 個硬幣,所以貪心並不合適,這裏就先放棄該方案了。

動態規範
實現思路

定義 dp[i](dp[0] = 0)爲組合成 i 時需要的最少硬幣數,那麼繼續向前推就是 dp[i] = dp(i - coin[j]) 需要最少硬幣數 + 1, + 1 是代表使用 coin[j] 算一次。

假設 i = 1:
當使用1幣值組合,既 dp[1] = dp[0] + 1;

假設 i = 2:
當使用1幣值組合,既 dp[2] = dp[1] + 1;
當使用2幣值組合,既 dp[2] = dp[0] + 1;

假設 i = 3:
當使用1幣值組合,既 dp[3] = dp[2] + 1;
當使用2幣值組合,既 dp[3] = dp[1] + 1;

......

假設 i = 6:
當使用1幣值組合,既 dp[6] = dp[5] + 1;
當使用2幣值組合,既 dp[6] = dp[4] + 1;
當使用5幣值組合,既 dp[6] = dp[1] + 1;

最終 dp[6] 取值爲這 3 種情況的最小值。

動態規劃的思路是將大問題化爲子問題來解決,然後逐漸往大遞推,所以得到最終的動態規劃方程式爲: dp[i] = Math.Min(dp[i], dp[i - coins[j]] + 1),dp[i] 的值可能會隨着 coins[j] 不同而改變,所以需要將 dp[i] 和 dp[i - coins[j]] + 1 中較小值重新賦給 dp[i]。

參考代碼
public int CoinChange(int[] coins, int amount)
{
    var dp = new int[amount + 1];
    // dp[0] 爲 0,其他默認爲 amount + 1(實際是不可能的),爲了方便取對比結果中的最小值
    for (int i = 1; i < dp.Length; i++)
    {
        dp[i] = amount + 1;
    }

    // 計算 1~amount 每項 dp[i] 的值
    for (int i = 1; i <= amount; i++)
    {
        for (int j = 0; j < coins.Length; j++)
        {
            // 如果i能使用存在的面額來組合,得到每種面值組合後的最小值
            if (coins[j] <= i)
            {
                dp[i] = Math.Min(dp[i], dp[i - coins[j]] + 1);
            }
        }
    }

    // 如果 dp[amount] 是 amount + 1 ,代表沒找到組合結果,否則返回組合成 amount 需要的最少硬幣數 dp[amount]
    return dp[amount] > amount ? -1 : dp[amount];
}

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章