1. 程式人生 > >動態規劃-硬幣問題分析

動態規劃-硬幣問題分析

什麼是動態規劃

上次對動態規劃已經有了個大概的分析。引用維基百科的話就是:
dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems

翻譯過來就是動態規劃就是通過拆分問題,來解決複雜的問題的一個方式。所以我們也知道,動態規劃的核心就是如何取拆分這個問題那如何拆分問題,這個也就成為我們要去研究的重點。借鑑於大部分權威人士比較認同的觀點是:拆分問題,靠的是狀態的定義和狀態轉移方程的定義。
這裡有一遍知乎是對上面的定義的見解:

https://www.zhihu.com/question/23995189/answer/35324479

什麼是狀態的定義

我們對狀態的定義是這樣的:解答動態規劃問題,需要開一個數組,陣列中的每一個元素f[i]或f[i][j]代表了什麼。而狀態的定義需要兩個步驟:
1.最後一步;
2.子問題。

題目描述

You are given coins of different denominations and a total amount of money amount. Write a function to compute the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.

Example Given coins = [1, 2, 5], amount = 11 return 3 (11 = 5 + 5 + 1)

Given coins = [2], amount = 3 return -1.

Notice You may assume that you have an infinite number of each kind of coin.

問題簡單解釋:現有不同幣值的硬幣,湊足一個給定的金額,不能多不能少,得出最少的硬幣個數;如果無解,返回-1。

解題思路

那我們重新返回之前說的狀態的定義的兩個步驟。
最後一步:

我們就從例子裡面說的,有1,2,5三個幣值的硬幣,湊足11塊。我們假設最優策略是k個硬幣湊足了11塊,最後的一塊硬幣幣值是a(K)元,則前面的硬幣拼出的幣值為11-a(K)元,這就是先著手最後一步的狀態定義。

然後從子問題講:
由於我們已經從最後一步定義,將原問題轉化成另外一個問題是用最少的硬幣湊足11-a(K)元,此時將原規模變小,而此時的問題就是原問題的子問題。可以將湊足多少元(即規模)定義為X,則可以用狀態f(X) = 用最少的硬幣湊足X元。我們假設最後一枚硬幣是1,則f(11) = f(11 -1)+1枚;最後一枚是2,則f(11) = f(11-2)+1枚;同理,最後一枚為5,則f(11)=f(11-5)+1枚,即其實最少的硬幣為f(11)=min{f(11-1)+1,f(11-2)+1,f(11-5)+1}。 我們可以用程式進行表示:

// 遞迴解法 
// 沒有考慮邊界問題
虛擬碼
int f(x){
if(x==0)return 0;
int res = Integer.MAX_VALUE;
if(x>=1){
res = Math.min(f(x-1)+1,res);
}
if(x>=2){
res = Math.min(f(x-2)+1,res);
}
if(x>=5){
res = Math.min(f(x-5)+1,res);
}
return res;
}

但遞迴法解法做了很多重複計算,比如f(9)->f(11-1)->f(10-9)或者f(11-2)計算可得。這時候我們可以從動態規劃第二部分---狀態的轉移。

其實很簡單將f(x) 變成一個數組f[x],將計算結果儲存起來。此時函式f[x]=min{f[x-1]+1,f[x-2]+1,f[x-5]+1}。
由於是個陣列,所以我們應該考慮下邊界和初始值的問題。假設拼不出來的值如f[-1]或者f[-3]這種,我們定義為正無窮Max_Value,初始值f[0]=0代表要拼出0元即為0個硬幣。此時我們應當從小到大順序算出每個元素的最小硬幣值,即:
f[1]=min{f[1-1]+1,f[1-2]+1,f[1-5]+1}=f[0]+1 = 1

f[2] = min{f[2-1]+1,f[2-2]+1,f[2-5]+1} = f[0]+1 = 1,

我們不難發現,當f[2]以1塊為最後的硬幣時,f[1]其實已經儲存該值的最小硬幣數,不會重複計算,且演算法時間度是X*3

可以看下程式:

public int coinChange(int[] a, int M) {
        // write your code here
        int length = a.length;
        int []f = new int[M+1];

        // 初始化
        f[0]=0;
        for(int i=1;i<=M;i++){
            // 先將f[i]初始無窮大,進行對比
            f[i]= Integer.MAX_VALUE;
            for (int j=0;j<length;j++){
                // 湊足的金額一定得大於硬幣的值且減了硬幣不應該等於無窮大
                if(i>=a[j]&&f[i-a[j]]!=Integer.MAX_VALUE&&f[i-a[j]]+1<f[i]){
                    f[i]= f[i-a[j]]+1;
                }
            }
        }
        if(f[M]==Integer.MAX_VALUE){
            return -1;
        }else {
            return f[M];
        }
    }

作者簡介

考拉後端開發小哥Rowland,熱愛