[LeetCode解題] -- 零錢兌換

龍骨發表於2020-12-06

動態規劃是 求解最優化問題的一種常用策略

零錢兌換   322. 零錢兌換

[ 題目描述 ]

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

你可以認為每種硬幣的數量是無限的。

 

示例 1:

輸入:coins = [1, 2, 5], amount = 11
輸出:3 
解釋:11 = 5 + 5 + 1
示例 2:

輸入:coins = [2], amount = 3
輸出:-1
示例 3:

輸入:coins = [1], amount = 0
輸出:0
示例 4:

輸入:coins = [1], amount = 1
輸出:1
示例 5:

輸入:coins = [1], amount = 2
輸出:2

一、貪心求解

貪心只看眼前最優,但不一定得到全域性最優解。如:41 = 25 10 5 1 最優解 20 20 1

如果沒有如下限制

1. 如果面值從1開始,此時一定有解(最壞情況 零錢面值都是1)

則此時如下條件一定可以不滿足: 如果沒有任何一種硬幣組合能組成總金額,返回 -1。( 即如果amount數額湊不齊,返回 -1 ) 

2. coins 陣列逆序排序

[ 解題思路 ]

如下解題思路為解決 陣列逆序排序問題,可忽略

陣列逆序排序,需要 int 是包裝型別 Integer

1. Collections.reverseOrder()

// 可以用此方法進行逆序排序,但是int陣列必須是Integer包裝類 型別
Arrays.sort(coin, Collections.reverseOrder());

2. 傳入比較器

// 簡寫如下
Arrays.sort(coins, (Integer f1, Integer f2) -> f2 - f1); // 傳入比較器,逆序排序  {25, 5, 10, 1}

即
Arrays.sort(coins,(Integer f1,Integer f2)->{
		return f1-f2;
});

當然可以先將其轉為包裝類

Integer[] coin = new Integer[coins.length];
for (int i = 0; i <coins.length ; i++) {
	coin[i]=coins[i];
}

[ 程式碼實現 ]

    static int  coinChange1(int[] coins, int amount) {
        int res = 0, i = 0;         // res面值可用數量 i 第i個
        while (i < coins.length) {
            if (amount < coins[i]) {	//逆序,先取出最大的face[0]=25  剩下的錢 < 當前值。面值不可用
                i++;        // 面值不可用數量
                continue;
            }
            amount -= coins[i]; // 面值是可用的
            res++;
        }
        return res;
    }


[ 效能分析 ]

 

二、遞迴求解

如果沒有如下限制

1. 如果沒有任何一種硬幣組合能組成總金額,返回 -1。( 即如果amount數額湊不齊,返回 -1 ) 

[ 解題思路 ]

參考 斐波那契

//	 此處類別 fib  1. fib有2個選項 零錢兌換有4個選項  2.都有重複計算--記憶化搜尋 優化
	static int fib(int n){
		if(n<=2)
			return 1;
		return fib(1)+fib(2);
	}

[ 程式碼實現 ]

解法1 暴力遞迴

    /**
     * 暴力遞迴(自頂向下的呼叫,出現了重疊子問題)
     *
     * 貪心策略得到的並非最優解 只看眼前
     *
     * dp(20) 湊到20分需要的最少硬幣個數
     *
     * dp(41) 湊到41分需要的最少硬幣個數
     *
     * dp(n)  湊到n分需要的最少硬幣個數
     *
     * 第一次選擇了25分的硬幣,現在還差 n-25分
     * 	dp[n-25]湊到n-25需要的最少硬幣個數
     * 如果第一次選擇的了25分的硬幣,那麼 dp(n)=dp(n-25)+1 // +1,已經選擇了1枚硬幣,25分的硬幣
     */
    static int coinChange2_(int n) {
        if (n < 1) return Integer.MAX_VALUE;
        if (n == 25 || n == 20 || n == 5 || n == 1) return 1;
        int min1 = Math.min(coinChange2_(n - 25), coinChange2_(n - 20));
        int min2 = Math.min(coinChange2_(n - 5), coinChange2_(n - 1));
        return Math.min(min1, min2) + 1;
    }

解法2 記憶化搜尋優化

    /**
     * 記憶化搜尋(自頂向下的呼叫)
     *  dp[1]=dp[5]=dp[20]=dp[25]=1 湊夠1、5、20、25分需要的最少硬幣個數都是 1 枚
     */
    static int coinChange2(int n) {
        if (n < 1) return -1;
        int[] dp = new int[n + 1];		// 需要計算到湊過n分,下標要取到n,故長度為n+1
        int[] faces = {1, 5, 20, 25};
        for (int face : faces) {	// dp[1]=dp[5]=dp[20]=dp[25]=1; 直接賦值有bug。如果n=20,dp[25]越界
            if (n < face) break;
            dp[face] = 1;
        }
        return coinChange2(n, dp);
    }

    static int coinChange2(int n, int[] dp) {
        if (n < 1) return Integer.MAX_VALUE;
        if (dp[n] == 0) {	// 陣列預設是0  dp[n] == 0 表示沒有計算過
            int min1 = Math.min(coinChange2(n - 25, dp), coinChange2(n - 20, dp));
            int min2 = Math.min(coinChange2(n - 5, dp), coinChange2(n - 1, dp));
            dp[n] = Math.min(min1, min2) + 1;
        }
        return dp[n];	// 已經算過,直接返回。記憶化搜尋
    }

    //	 此處類別 fib  1. fib有2個選項 零錢兌換有4個選項  2.都有重複計算--記憶化搜尋 優化
    static int fib(int n){
        if(n<=2)
            return 1;
        return fib(1)+fib(2);
    }


[ 效能分析 ]

 

三 動態規劃求解

[ 解題思路 ]
[ 程式碼實現 ]

解法1

如果沒有如下條件限制

1. 如果沒有任何一種硬幣組合能組成總金額,返回 -1。( 即如果amount數額湊不齊,返回 -1 ) 

    /*
        dp[n] 湊夠n分需要的最少硬幣個數
        dp[i]=min{dp[i-1],dp[i-5],dp[i-20],dp[i-25]}+1
             解讀:dp[i] 湊夠i分需要的最少硬幣個數 = 湊夠 {,,,}最小個數,再+1個硬幣
        coins={5,20,25}
        amount=5 dp=[0, 0, 0, 0, 0, 1]
        amount=6 dp=[0, -2147483648, -2147483648, -2147483648, -2147483648, 1, -2147483647]
     */
    static int coinChange3_1(int[] coins,int amount) {
        if (amount < 1 || coins == null || coins.length == 0)
            return -1;
        int[] dp = new int[amount + 1];
        for (int i = 1; i <= amount; i++) {
            int min = Integer.MAX_VALUE;
            for (int coin : coins) {
                if (i < coin)
                    continue;
                min = Math.min(dp[i-coin],min);
            }
            dp[i]=min+1;
        }
        System.out.println(Arrays.toString(dp));
        return dp[amount];
    }

解法2

    //  dp[n] 湊夠n分需要的最少硬幣個數
	/*
		// 如果amount數額湊不齊。即:如果沒有任何一種硬幣組合能組成總金額,返回 -1。
		coins={5,20,25}
		amount=5  dp=[0, -1, -1, -1, -1, 1]
		amount=6  dp=[0, -1, -1, -1, -1, 1, -1]
		amount=20 dp=[0, -1, -1, -1, -1, 1, -1, -1, -1, -1, 2, -1, -1, -1, -1, 3, -1, -1, -1, -1, 1]
	 */
    static int coinChange3_2(int[] coins,int amount) {
        if (amount < 1 || coins == null || coins.length == 0)
            return -1;
        int[] dp = new int[amount + 1];
        for (int i = 1; i <= amount; i++) {
            int min = Integer.MAX_VALUE;
            for (int coin : coins) {
                if (i < coin)	// 示例 i=1 faces={5,20,25}沒有1,此時dp[1]=-1 dp[1~4]=-1
                    continue;
                int v = dp[i - coin];
                // 如果面值不是從1開始,那麼湊夠dp[i]需要的最少硬幣個數v就是-1.即:如果{5,20,25},dp[1~4]=-1
                // 如果湊夠dp[i]需要的最少硬幣個數v >=min ,那麼沒有必要更新min.即:如果{20,5,25},amount=20
                // 		v=dp[20-20]=0 (湊夠0分需要0枚);或者那麼v=dp[20-15]=3 (湊夠15分需要3枚)
                // 		則 v=3 答案捨棄
                if (v < 0 || v >= min)  // v=-1 即沒法湊
                    continue;
                min = v;
            }
            if (min == Integer.MAX_VALUE) {	// 只要湊不齊,dp[i=-1]
                dp[i] = -1;
            } else {
                dp[i] = min + 1;
            }
        }
        System.out.println(Arrays.toString(dp));
        return dp[amount];
    }

3 完整程式碼

    static int coinChange(int[] coins,int amount) {
        if (amount < 1 || coins == null || coins.length == 0)
            return -1;
        int[] dp = new int[amount + 1];
        for (int i = 1; i <= amount; i++) {
            int min = Integer.MAX_VALUE;
            for (int coin : coins) {
                if (i < coin)	
                    continue;
                int v = dp[i - coin];
                if (v < 0 || v >= min)  //v < 0 即沒辦法湊;v >= min已經有更優解
                    continue;
                min = v;
            }
            if (min == Integer.MAX_VALUE) {	// 湊不齊,dp[i=-1]
                dp[i] = -1;
            } else {
                dp[i] = min + 1;
            }
        }
        System.out.println(Arrays.toString(dp));
        return dp[amount];
    }


[ 效能分析 ]

 

 

 

 

 

相關文章