最少錢幣數(湊硬幣)詳解-2-動態規划算法(初窺)-CCF-CSP練習題(100)


目錄

題目:

分析:

C++動態轉移方程代碼:

總結:


這篇使用動態規划算法來解決這個問題,借這篇博客初窺動態規划算法。最少錢幣數問題也可以看作多重背包問題。

那么什么是動態規划算法?

動態規划(dynamic programming,DP)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。20世紀50年代初美國數學家R.E.Bellman等人在研究多階段決策過程(multistep decision process)的優化問題時,提出了著名的最優化原理(principle of optimality),把多階段過程轉化為一系列單階段問題,利用各階段之間的關系,逐個求解,創立了解決這類過程優化問題的新方法——動態規划。1957年出版了他的名著《Dynamic Programming》,這是該領域的第一本著作。(思想也有點像分布式處理)

                                                                                                                                  ———From Baidupedia

那么我們如何去描述它?

動態規划算法通常基於一個遞推公式及一個或多個初始狀態。 當前子問題的解將由上一次子問題的解推出。使用動態規划來解題只需要多項式時間復雜度, 因此它比回溯法、暴力法等要快許多。

                                                                                                                         ——From 從動態規划新手到專家

概念講完,開始做題。

題目:

最少錢幣數
問題描述 這是一個古老而又經典的問題。用給定的幾種錢幣湊成某個錢數,一般而言有多種方式。例如:給定了 6 種錢幣面值為 2、5、10、20、50、100,用來湊 15 元,可以用 5 個 2 元、1個 5 元,或者 3 個 5 元,或者 1 個 5 元、1個 10 元,等等。顯然,最少需要 2 個錢幣才能湊成 15 元。
        你的任務就是,給定若干個互不相同的錢幣面值,編程計算,最少需要多少個錢幣才能湊成某個給出的錢數。
輸入形式 輸入可以有多個測試用例。每個測試用例的第一行是待湊的錢數值 M(1 <= M<= 2000,整數),接着的一行中,第一個整數 K(1 <= K <= 10)表示幣種個數,隨后是 K個互不相同的錢幣面值 Ki(1 <= Ki <= 1000)。輸入 M=0 時結束。
輸出形式 每個測試用例輸出一行,即湊成錢數值 M 最少需要的錢幣個數。如果湊錢失敗,輸出“Impossible”。你可以假設,每種待湊錢幣的數量是無限多的。
樣例輸入 15
6 2 5 10 20 50 100
1
1 2
0
樣例輸出 2
Impossible

分析:

上一篇最少錢幣數-1-貪心算法(錯,或者叫有問題)-CCF-CSP練習題中使用的貪心算法解決的湊硬幣問題,有些情況下是可以得出正解的,比如后一個錢幣面值沒有達到前一個錢幣面值的2倍時;但對於某些情況來說得出的解是錯誤的。比如有3種面值分別為3元,5元,7元的紙幣,(1)那么至少用幾張紙幣能湊夠10元?我的直覺告訴我先選面值最大的,7元一張,然后再選面值5元的時候發現超額了(7+5>10),因此我們選3元一張,最少用2張紙幣就能湊夠10元,這個時候可以得出正解。這個方法容易想出來,抽象一點可以叫貪心算法(每次都選當前看來最好的選擇,不從整體最優考慮),(2)那么至少用幾張紙幣能湊夠8元呢?如果還按照貪心算法來解的話會得到Impossible。因為先選一張7元,然后再選5元(7+5>8)不行,換選3元(7+3>8)還不行。但是仔細看會發現5元+3元不是8元嗎,怎么會無解。所以貪心算法解這個問題是不行的。考CSP時只能用來騙點分。

到這里我們發現用貪心算法會出現兩個問題 1)本來有解用貪心法算出來卻無解,例如上例(2)至少用幾張紙幣能湊夠8元?;2)算出來的解不是最優解,例如有 1元,7元,9元,10元四種面值的紙幣,要湊18元 ,貪心算法會給出答案需要3張(10元1張,7元1張,1元1張),但是我們可以明顯看出 2張(兩張9元)也可以。

那么問題出在了哪里?在這里用貪心算法是有條件的——后一個的權值(這里就是紙幣面值)是前一個的2倍或以上才可以使用,這里10不到9的兩倍。貪心算法不是對所有問題都能得到整體最優解。關鍵是貪心策略的選擇,選擇的貪心策略必須具備無后效性,即某個狀態以前的過程不會影響以后的狀態,只與當前狀態有關。在這里我們先選擇了10元,在第二步的時候9元就選擇不了了(10+9>18了),所以會錯過9+9這個最優解。所以說貪心算法在對問題求解時,總是做出在當前看來是最好的選擇,不從整體最優考慮。

這里說了這么多,都是在討論上一篇,本篇主要討論正確解法——動態規划算法解題。可能你已經不耐煩了,那么先上一個此問題遞推公式(也可以叫做動態轉移方程):(注:v[i]表示可以使用的紙幣的面額組成的數組,dp[m]表示要湊m元至少需要多少張紙幣。)

dp[m] = min(dp[m-v[i] ]+1,dp[m])

那么這個方程怎么得來的呢?我們先了解一下DP(Dynamic Programing)的基本原理:首先,找到某個狀態的最優解,然后在它的幫助下,找到下一個狀態的最優解。不明白這個概念沒關系,我們以上面的例子為例來分析一下——如果我們有4種面值分別為1元,3元,5元,7元的紙幣,那么至少需要幾張紙幣就能湊出8元?

在分析這個問題之前先來思考一個問題,至少用多少張紙幣能湊夠m(表示money)元(m<8)呢?為什么要這么問呢? 動態規划的思想:(1)當我們遇到一個大問題時,總是習慣把問題的規模變小,這樣便於分析討論。 (2)這個規模變小后的問題和原來的問題是同質的,除了規模變小,其它的都是一樣的, 本質上它還是同一個問題(規模變小后的問題其實是原問題的子問題)。

讓我們從規模最小的m開始。當m=0時,即我們需要多少個幣來湊夠0元呢? 由於1,3,5,7都大於0,即沒有比0小的幣值,因此湊夠0元我們最少需要0個幣。 (em......Interesting,這個分析很傻是不是?別着急,這個思路有利於我們理清動態規划究竟在做些什么。) 為了方便我們用dp[m]=c來表示湊夠m元最少需要c個硬幣。於是我們就得到了dp[0]=0, 表示湊夠0元最小需要0個硬幣。當m=1時,只有面值為1元的硬幣可用, 因此我們拿起一個面值為1的硬幣,接下來只需要湊夠0元即可,而這個是已經知道的dp[0]=0。所以,dp[1]=dp[1-1]+1=dp[0]+1=0+1=1。當m=2時, 仍然只有面值為1的硬幣可用,於是我拿起一個面值為1的硬幣, 接下來我只需要再湊夠2-1=1元即可(記得要用最小的硬幣數量),而這個答案也已經知道了。 所以dp[2]=dp[2-1]+1=dp[1]+1=1+1=2。分析到這里,聰明的你可能已經看出端倪,沒看出來沒關系,接下來讓我們看看m=3時的情況。當m=3時我們能用的硬幣就有兩種了:1元的和3元的( 5元的仍然沒用,因為你需要湊的數目是3元,5元面值太大了)。 既然能用的硬幣有兩種,我就有兩種方案。如果我拿了一個1元的硬幣,我的目標就變為了: 湊夠3-1=2元需要的最少硬幣數量。即dp[3]=dp[3-1]+1=dp[2]+1=2+1=3。 這個方案說的是,我拿3個1元的硬幣;第二種方案是我拿起一個3元的硬幣, 我的目標就變成:湊夠3-3=0元需要的最少張紙幣。即dp[3]=dp[3-3]+1=dp[0]+1=0+1=1. 這個方案說的是,我拿1個3元的硬幣。好了,這兩種方案哪種更優呢? 記得我們的問題是要用最少的硬幣數量來湊夠3元。所以, 選擇dp[3]=1,怎么來的呢?具體是這樣得到的:dp[3]=min(dp[3-1]+1, dp[3-3]+1)。

有了上面的分析,這回應該能看出個門道了吧,你可能早已按奈不住了,現在我們就可以從以上分析抽象出我們想要的東西了——遞推公式。從以上的文字中, 我們要抽出動態規划里非常重要的兩個概念:狀態狀態轉移方程

上文中dp[m]表示湊夠m元需要的最少硬幣數量,我們將它定義為該問題的"狀態", 這個狀態是怎么找出來的呢?是根據子問題定義狀態。你找到子問題,狀態也就浮出水面了。 最終我們要求解的問題,可以用這個狀態來表示:dp[8],即湊夠8元最少需要多少個硬幣。 那狀態轉移方程是什么呢?既然我們用dp[m]表示狀態,那么狀態轉移方程自然包含dp[m], 上文中包含狀態 dp[m] 的方程是:dp[3]=min(dp[3-1]+1, dp[3-3]+1)。沒錯, 它就是狀態轉移方程,描述狀態之間是如何轉移的。當然,我們要對它抽象一下,dp[m] = min(dp[ m-v[i] ]+1,dp[m]),其中 m-v[i] >=0,v[i] 表示第i個硬幣的面值,方程的含義是拿出一個面值為 v[i] 的硬幣后,湊夠 m-v[i] 元至少需要的硬幣數目(dp[m-v[i] ]+1和湊夠m元至少需要的硬幣數目(dp[m])相比較,取較小的存入dp[m]。

這里可能就會有人問了,為什么還要和dp[m]比較后再存入dp[m],正如上面的例子,因為我們在湊夠m元時,可能有多種可行的方案,我們要比較出哪一種方案所需硬幣數目最小。例如在4種硬幣1、3、5、7元湊8元的時候會有三種方案,1)8個1元;2)3+5元;3)1+7元。我們得從中找到我們所要的答案。(如果用貪心算法的話可能會錯過最優解)

有了動態轉移方程,問題基本就算解決了。當然,Talk is cheap,show me the code!

C++動態轉移方程代碼:

#include <iostream>
using namespace std;

int main()
{
    int coins[10] = {0};   //硬幣面值數組,由於題目給出不超過10種,所以我申請了10。
    int money = 0;         //待湊錢的數值*/
    int kind = 0;          //錢幣種類數目*/

    while(1)
    {
        cin >> money;
        if(0 == money)break; //結束標志
        int dp[money+1];     //動態規划數組
        dp[0] = 0;           //初始化第一個元素為0,因為要湊0元需要0個錢幣
        cin >> kind;         //硬幣面值種類數
        for(int k=0; k<kind; k++)
        {
            cin >> coins[k]; //讀入硬幣面值,存入數組coins[]
        }

        for(int i = 1; i <= money; i++) dp[i] = 99999; //初始化數組dp[],設置dp[i]等於無窮大

        for(int i = 1; i <= money; i++)  //從湊1元開始,一直算到money元為止。
        {
            for(int j = 0; j < kind; j++)
            {
                if(i >= coins[j])
                {
                    dp[i] = min(dp[i- coins[j] ] + 1, dp[i]);
                }
                /*****也可以寫成******
                if(i >= coins[j] && dp[i - coins[j]] + 1 < dp[i])
                {
                    dp[i] = dp[i- coins[j] ] + 1;
                }
                 自己干了,不用麻煩min()函數
                 */
            }
        }
        if( dp[money] == 99999 )
        {
            cout << "Impossible"<< endl;
        }
        else
        {
            cout << dp[money] << endl;
        }

    }
    return 0;
}

 

圖1-1 湊0-8元所需最少錢幣

如圖1-1所示,有4種面值的錢幣1元、3元、5元、7元時,從湊0元到湊8元至少所需錢幣數。

總結:

使用動態規划算法解決此題時,能全面的考慮到所有情況,從而找到最優解。但是相對於貪心算法來說時間和空間復雜度都會增加。這兩個算法有什么區別呢?如圖1-2所示。

圖1-2 貪心算法和動態規划區別

動態規划算法不是一個具體的算法,動態規划算法要求我們具體問題具體分析。把一個大的問題變為一個和它同質的(除了規模變小,其他都一樣)小規模問題。然后推導出遞推公式(狀態轉移方程)。這是關鍵。那么這個推導遞推公式的能力怎么獲得呢?

動態規划程序設計是對解最優化問題的一種途徑、一種方法,而不是一種特殊算法。不像搜索或數值計算那樣,具有一個標准的數學表達式和明確清晰的解題方法。動態規划程序設計往往是針對一種最優化問題,由於各種問題的性質不同,確定最優解的條件也互不相同,因而動態規划的設計方法對不同的問題,有各具特色的解題方法,而不存在一種萬能的動態規划算法,可以解決各類最優化問題。因此讀者在學習時,除了要對基本概念和方法正確理解外,必須具體問題具體分析處理,以豐富的想象力去建立模型,用創造性的技巧去求解。我們也可以通過對若干有代表性的問題的動態規划算法進行分析、討論,逐漸學會並掌握這一設計方法。                                                                                   ——From Baidupedia

最后一點總結,寫完東西一定要保存,剛剛去吃飯忘記保存,結果重新寫了倆多小時。

參考博文:從動態規划新手到專家

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM