湊硬幣問題
題目詳情為:有面值為1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元?
最近在學習一些重要算法,作為五大算法之一的動態規划法,自然要認真學習,這是一道典型的動態規划問題,這里使用動態規划法的思想來解題;
我們用d(i)=j來表示湊夠i元最少需要j個硬幣,通過題目,很容易得到:當i=0時,d(0)=0, 表示湊夠0元最小需要0個硬幣; 當i=1時,只有面值為1元的硬幣可用, 因此我們拿起一個面值為1的硬幣,接下來只需要湊夠0元即可,而這個是已經知道答案的, 即d(0)=0,則有d(1) = d(1 - 1) + 1 = 1,湊夠1元最少需要1個硬幣,當i = 2時,d(2) = d(2 - 1) + 1= d(1) +1=2, 當i = 3時,d(3) = min{d(3 - 1) + 1 , d(3 - 3) + 1} = min(3, 1) = 1;動態規划算法通常基於一個遞推公式及一個或多個初始狀態。在這里d(i) 就是狀態,通過分析推導的過程,可以得到,針對面值為1,3,5的硬幣,可以得到遞推公式(狀態轉移方程)為:
d(i) = min{ d(i - Vj) + 1} ,i >= Vj。
在動態規划中,得到了該問題的狀態及其狀態轉移方程,問題已經解決了一大半了,然后,在分析的過程中,並不能一眼就看出遞推公式,它需要更多的練習和更多的實踐積累的,並不是一朝一夕能做到的,況且動態規划的關鍵就是找到狀態和狀態轉移方程,那么容易找到,就不是動態規划了,就不是難點了。根據這個公式,我們可以比較輕易的寫出實現的代碼:

/* @動態規划練習題 如果我們有面值為1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元? */ #include <stdio.h> #include <stdlib.h> #include <string.h> int DP_leastcoin(const int coin[], int money) { int *d = (int *)malloc(sizeof(int) * (money + 1)); memset(d, 0, sizeof(int) * money); int iterx = 0, itery = 0; int MIN = 0; int result = 0; d[0] = 0; for(iterx = 1; iterx <= money; iterx++) { for(itery = 0; itery < 3 && iterx >= coin[itery]; itery++) { if(itery == 0) { MIN = d[iterx - coin[itery]] + 1; } else if(MIN > (d[iterx - coin[itery]] + 1)) { MIN = (d[iterx - coin[itery]] + 1); } } d[iterx] = MIN; } printf("要湊的錢 MIN\n"); for(iterx = 0; iterx <= money; iterx++) { printf("序號%-3d : %d\n", iterx, d[iterx]); } result = d[money]; free(d); return result; } int main(void) { const int coin[3] = {1, 3, 5}; printf("\nThe result is %d \n", DP_leastcoin(coin, 112)); return 0; }
在研究湊硬幣問題的時候,我把硬幣的面值換為2,3,5,然后依舊使用這個狀態轉移方程,得到的結果是錯的,由此也可以知道,狀態轉移方程是針對某一問題分析得到的,盡管只是修改了硬幣的面值,該方程就不再成立了,首先我們要找到問題所在,是什么問題導致了該方程不再適用。
我們手動分析面值為2,3,5的情況:
d(0) = 0
d(1) = 0
d(2) = 1
d(3) = 1
d(4) = 2
d(5) = 1
d(6) = 2
d(7) = 2
d(8) = 2
d(9) = 3
d(10) = 2
d(11) = 3
我們先來看看面值為2,3,5的結果圖片,看看是在哪里開始出錯的:
找出兩者不一樣的值,發現是從序號4開始的,d(4) , d(6) , d(9) , d(11) 這幾個不同(當然后續還有其他不同的值),把這些狀態代入上面的狀態轉移方程,看看那哪里不對:
d(4) = min{ d(4 - 3) + 1, d(4 - 2) + 1} = min{ d(1)+1, d(2)+1 }=min(0+1, 1+1) = 1;
問題來了,本來d(4)應該是等於2,由兩個面值額外2的硬幣湊成,這里怎么會有1呢?1的由來,是d(1)+1 = 1;d(6)也是有問題的,看下面
d(6) = min{d(6 - 5)+1, d(6 - 3)+1, d(6 - 2)+1} = min(d(1)+1, d(3)+1, d(4)+1}=min(1, 2, 2);
問題還是在d(1)上面,至於后面的d(9) 和d(11)是因為使用了錯誤的d(6)和d(4)才錯的,那這個方程的罪魁禍首就是d(1)咯?
假設我們把d(4) 和 d(6) 都糾正過來,即d(4)= 2, d(6) = 2,那么結果又如何,你可以自己從新列一遍,從d(6)開始,后面都是正確的。這里把我糾正的圖片發一下,面值為2,3,5的,我給的需要湊得錢值是112,設一個更大的值,容易排查錯誤情況:
上面沒有把數據全部列出來。我檢查了一下,對於面值為2,3,5的情況,沒有發現錯誤的。
很奇怪,這樣一改程序就正確了,我猜想,把面值1,3,5的狀態轉移方程,拿到面值為2,3,5問題里面就出錯,而修改一下(2,3,5)問題的前面某些值d(4)和d(6),狀態方程依舊適用,主要原因還是在面值為1的硬幣上,由於存在面值為1的情況,假設要湊的錢數為N,那么只要N>0,肯定可以湊出來,把硬幣面值換為(2,3,5),那要湊出1塊錢是不可能的,所以d(1)+1就有了問題,因為你無法湊到1塊錢,是不能使用d(1)的,把存在d(1)的情況去掉,那結果就是正確的,現在知道為什么是4和6了吧,因為你的面值為2,3,5,一個數減掉2,3,5得到1的數就是3,4,6,所以,d(3), d(4) ,d(6)就是錯的,那前面怎么沒有指出d(3),因為恰好d(1)的結果不影響d(3),盡管如此,還是要把d(1)去掉,方法如下:
d(4) = min{d(4 - 2) + 1} = min(2) = 2,本來是d(4) = min{d(4 - 2) + 1, d(4 - 3) + 1} = min(2 , 1) = 1
程序修改十分簡單,就在嵌套的for循環里面加上一條語句 “if(iterx - coin[itery] == 1) continue;//當硬幣面值沒有1時” 即可,如下:

/* @動態規划練習題 如果我們有面值為1元、3元和5元的硬幣若干枚,如何用最少的硬幣湊夠11元? */ #include <stdio.h> #include <stdlib.h> #include <string.h> int DP_leastcoin(const int coin[], int money) { int *d = (int *)malloc(sizeof(int) * (money + 1)); memset(d, 0, sizeof(int) * money); int iterx = 0, itery = 0; int MIN = 0; int result = 0; d[0] = 0; for(iterx = 1; iterx <= money; iterx++) { for(itery = 0; itery < 3 && iterx >= coin[itery]; itery++) { if(iterx - coin[itery] == 1) continue;//當硬幣面值沒有1時 if(itery == 0) { MIN = d[iterx - coin[itery]] + 1; } else if(MIN > (d[iterx - coin[itery]] + 1)) { MIN = (d[iterx - coin[itery]] + 1); } } d[iterx] = MIN; } printf("要湊的錢 MIN\n"); for(iterx = 0; iterx <= money; iterx++) { printf("序號%-3d : %d\n", iterx, d[iterx]); } result = d[money]; free(d); return result; } int main(void) { const int coin[3] = {2, 3, 5}; printf("\nThe result is %d \n", DP_leastcoin(coin, 112)); return 0; }
啰啰嗦嗦的寫了很多,我的表達能力有限,望見諒!此外,希望本博客對大家學習算法能有一點幫助!
ps:把硬幣面值換為其他值,可能要重新分析,這個題目也可以用貪心算法實現,但是貌似貪心算法有可能求出來的是局部最優解,我對貪心算法不大會,等后續學習到了,再來討論!