1. 程式人生 > >動態規劃之換零錢

動態規劃之換零錢

問題描述:如果我們有面值為1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元?
有人會說這太簡單,對是太簡單,但請你用動態規劃解,將問題進行抽象,最後達到什麼程度了,給出任意麵值集合V,湊夠的面值為m,求所需硬幣最少的個數 j.
這是動態規劃的入門題。
首先先介紹一下什麼是動態規劃:
動態規劃演算法通常基於一個遞推公式及一個或多個初始狀態。 當前子問題的解將由上一次子問題的解推出。使用動態規劃來解題只需要多項式時間複雜度, 因此它比回溯法、暴力法等要快許多。
現在對上面的問題進行分析:
首先我們思考一個問題,如何用最少的硬幣湊夠m元為什麼要這麼問呢? 兩個原因:1.當我們遇到一個大問題時,總是習慣把問題的規模變小,這樣便於分析討論。 2.這個規模變小後的問題和原來的問題是同質的,除了規模變小,其它的都是一樣的, 本質上它還是同一個問題(規模變小後的問題其實是原問題的子問題)。
好了,讓我們從最小的m開始吧。當m=0,即我們需要多少個硬幣來湊夠0元。 由於1,3,5都大於0,即沒有比0小的幣值,因此湊夠0元我們最少需要0個硬幣。 這時候我們發現用一個標記來表示這句“湊夠0元我們最少需要0個硬幣。”會比較方便, 如果一直用純文字來表述,不出一會兒你就會覺得很繞了。那麼, 我們用d(m)=j來表示湊夠m元最少需要j個硬幣。於是我們已經得到了d(0)=0, (注意d(0)=0可以做為動態規劃的初始狀態),表示湊夠0元最小需要0個硬幣。當m=1時,只有面值為1元的硬幣可用, 因此我們拿起一個面值為1的硬幣,接下來只需要湊夠0元即可,而這個是已經知道答案的, 即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。當m=2時, 仍然只有面值為1的硬幣可用,於是我拿起一個面值為1的硬幣, 接下來我只需要再湊夠2-1=1元即可(記得要用最小的硬幣數量),而這個答案也已經知道了。 所以d(2)=d(2-1)+1=d(1)+1=1+1=2。一直到這裡,你都可能會覺得,好無聊, 感覺像做小學生的題目似的。因為我們一直都只能操作面值為1的硬幣!耐心點, 讓我們看看m=3時的情況。當m=3時,我們能用的硬幣就有兩種了:1元的和3元的( 5元的仍然沒用,因為你需要湊的數目是3元!5元太多了親)。 既然能用的硬幣有兩種,我就有兩種方案。如果我拿了一個1元的硬幣,我的目標就變為了: 湊夠3-1=2元需要的最少硬幣數量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。 這個方案說的是,我拿3個1元的硬幣;第二種方案是我拿起一個3元的硬幣, 我的目標就變成:湊夠3-3=0元需要的最少硬幣數量。即d(3)=d(3-3)+1=d(0)+1=0+1=1. 這個方案說的是,我拿1個3元的硬幣。好了,這兩種方案哪種更優呢? 記得我們可是要用最少的硬幣數量來湊夠3元的。所以, 選擇d(3)=1,怎麼來的呢?具體是這樣得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。min{ }為取集合中的最小值,上面的min{d(3-1)+1,d(3-3)+1} 取最小值為d(3-3)+1。簡單的過程分析後,讓我們來點抽象的。從以上的文字中, 我們要抽出動態規劃裡非常重要的兩個概念:狀態和狀態轉移方程。
據上文我們可以抽象的d(i) (0<=i<=m)表示湊夠i元需要的最少硬幣數量,我們將它定義為該問題的”狀態”, 這個狀態是怎麼找出來的呢? 它根據子問題定義狀態。你找到子問題,狀態也就浮出水面了。比如d(11),d(0),d(1)等等這些都是狀態 ,這些狀態的值我們都要進行儲存,以便下一步子問題查閱上一步子問題的解,我們最終的狀態是解決問題,所以終態為d(11),即湊夠11元最少需要多少個硬幣。 那狀態轉移方程是什麼呢?既然我們用d(i)表示狀態,那麼狀態轉移方程自然包含d(i), 上文中包含狀態d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。沒錯, 它就是狀態轉移方程,描述狀態之間是如何轉移的。當然,我們要對它抽象一下,
d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示集合V中第j個硬幣的面值,那min{} 是怎麼操作的了以上面的為例,m=11, v={1,3,5}
d(11)=min{d(11-1)+1,d(11-3)+1,d(11-5)+1}=min{d(10)+1,d(8)+1,d(6)+1};找出裡面的最小值,d(10)+1,d(8)+1,d(6)+1,從中可以看出,我們是從底向上解題,也就是說從d(0)開始一直到d(11)。我們將所解的每個狀態的值都儲存到一個集合中。按這個原則,我們可以得到d(11) ,它所依賴的d(10),d(8),d(9)我們都可以從狀態集合中獲取。這樣我就可以只關心當前狀態的求解。而不用關心它之前的狀態,因為它之前的狀態都是已知的。
有了狀態和狀態轉移方程,這個問題基本上也就解決了。
下面我們來看一下程式碼要怎麼實現的:

public class MinCoin{
      public static int dp(int[]V,int m){
          //用於儲存狀態
          int[] minSV=new int[m+1];
          //初始狀態 將d(0)儲存到狀態集合中
          minSV[0]=0;
          //保證minSV[i]即d(i)只被初始化一次
          boolean flog=true;
          //獲取d(1)到的(m)所有的狀態值,將其儲存到狀態集合中
             for(int i=1;i<=m;i++){
                  flog=true
; for(int j=0;j<V.length;j++){ //d(i)=min{d(i-vj)+1} //先假設d(i)為要比較集合min{..}內的第一個。即將d(i)初始化為min{。。}內的第一個。 if(V[j]<=i && flog){ minSV[i]=minSV[i-V[j]]+1; //保證只初始化一次 flog=false
; } //獲取集合內最小的一個賦值給d(i) 即d(i)=min{d(i-vj)+1} //所用選取的面值vj不能大於要湊夠的面值i,且的d(i-vj)是min{..}內最小的 if(V[j]<=i && minSV[i-V[j]]+1<minSV[i]){ minSV[i]=minSV[i-V[j]]+1; } } } //返回終態d(m)的值 return minSV[m]; } public static void main(String[] args) { //要湊夠的面值為11 int m=11; //可用的面值集合為V int[] V=new int[]{1,3,5}; System.out.println("最少需要:"+dp(V,m)+"枚硬幣"); } }