詳解動態規劃最少硬幣找零問題–JavaScript實現

YinTokey發表於2019-03-02

硬幣找零問題是動態規劃的一個經典問題,其中最少硬幣找零是一個變種,本篇將參照上一篇01揹包問題的解題思路,來詳細講解一下最少硬幣找零問題。如果你需要檢視上一篇,可以點選下面連結:
詳解動態規劃01揹包問題–JavaScript實現

也可以檢視下一篇
詳解動態規劃最長公共子序列–JavaScript實現

下面讓我們開始吧。

問題

給定4種面額的硬幣1分,2分,5分,6分,如果要找11分的零錢,怎麼做才能使得找的硬幣數量總和最少。

分析

最少硬幣找零問題,是為了求硬幣的組合,所以一個大前提是硬幣無限量供應。我們建立如下表格來分析問題:

詳解動態規劃最少硬幣找零問題–JavaScript實現

其中每列用j表示零錢總額,每行i表示硬幣面額。T[i][j]表示硬幣個數,它是我們即將填入表格的數字。

在填寫表格之前,我們需要先明確幾個規則:

  • 當填寫第i行時,使用的硬幣面額僅能是i以及小於i的面額。舉個例子,比如我填寫第0行,i=0,那麼這一樣只能使用面額為1分的硬幣。當我填寫第2行,i=2,那麼可以使用1分,2分,5分三種面額的硬幣。
  • 當填寫第j列時,表示當前需要使用硬幣湊出的總額。比如j=6,表示需要使用硬幣組合出總額為6分的情況。

1. i = 0

當我們只能使用面額為1分的硬幣時,根據上面的規則,那麼很顯然,總額為幾分,就需要幾個硬幣。即T[i][j] = j

詳解動態規劃最少硬幣找零問題–JavaScript實現

2. i = 1

當我們有1分和2分兩種面額時,那麼組合方式就相對多了點。

i=1 j = 1:總額為1時,只能使用1分的面額。即填1。
i=1 j = 2:總額為2時,可以使用2個1分的,也可以使用1個2分的。因為我們要求最少硬幣,所以使用1個2分的。表格所表達的意思是硬幣的數量,所以這裡也填1。
i=1 j = 3:總額為3時,可以使用3個1分的,也可以使用1個1分加1個2分。因此這裡應該填2。
i=1 j = 4:總額為4時,可以使用4個1分的,可以使用2個1分加1個2分,也可以使用2個2分。其中硬幣最少的情況應該是2個2分。因此這裡填2。
i=1 j = 5:總額為5時,組合就更多了,但是聰明的你應該能想到使用2個2分加1個1分,可以實現最少硬幣的需求。因此這裡填3。

我們來看填寫完上面5格後的情況:

詳解動態規劃最少硬幣找零問題–JavaScript實現

建議你自己再紙上照著我這圖畫一個表格。接下來,別急著填表。我們要根據已有的資料,總結出T[i][j]的規律,然後通過填寫剩餘表格來驗證。

我們將硬幣面額使用陣列coins[i]來表示,根據表格有 1分=coins[0], 2分=coins[1]。
當j<coins[i]時,T[i][j]的值,應該等於它的同列,上一行,即使T[i][j] == T[i-1][j]。 比如我們從表中所看到的,T[1][1]==T[0][1]
當j>=coins[i]時,根據已有的 i=1行可以推出一個規律,令a = 1+T[i][j-coins[i]]T[i][j]= min(T[i-1][j],a),即二者比較取最小值。可能一開始你看到這個關於a的公式,有點太突然,難以接受。稍微解釋一下,當第i行,優先選擇這一樣的硬幣,因為這一行的硬幣面額最大,最有可能使得總硬幣數量最少。因此j-coins[i],就很好理解了,就是選擇了這一行的硬幣後,還剩下多少總額。舉個例子,當i=1,j=3時,j-coins[1]=1。那麼選擇2分後,還剩餘總額為1,這時候我們再定位到i=1,j=1,即T[1][1],它的值為1,再加上一個常數1,即得最終結果2。

再舉例,i=1 j=5。由於是從左到右填表的,所以i=1,j<5的表格都填完了。j-coins[i]=3,定位到T[1][3]=2,加上常數1,即得最後結果T[1][5]=3

其實公式本身很短,也很好記。如果實在無法理解,建議先不用糾結。先最小化瀏覽器,不要看本篇剩餘的內容。帶著這個解題公式,自己在紙上,把這個表格填寫完整,在填表分析的過程中就能慢慢理解了。

3. 剩餘內容

按照上一步所提供的公式,其實所有的T[i][j]都可以填完了。如下表格。

詳解動態規劃最少硬幣找零問題–JavaScript實現

建議先自己再紙上填表,填完了,再和我的圖對比一下,看是否答案存在出入。

4.虛擬碼

以上的填表邏輯,使用虛擬碼表示如下


if(i == 0){
	T[i][j] = j/coins[i]; //硬幣找零一定要有個 最小面額1,否則會無解
}else{
	if(j >= coins[i]){
		T[i][j] = min(T[i-1][j],1+T[i][j-coins[i]])
	
	}else{
		T[i][j] = T[i-1][j];
	}
}
複製程式碼

5. 尋找組合

至此,填完表格我們已經接近完成了。接下來要尋找從表格中尋找硬幣組合。?

與填表順序相反,尋找組合從有下角開始。

首先需要明確的是如果T[i][j] == T[i-1][j],那麼就向上搜尋。根據圖來分析:

詳解動態規劃最少硬幣找零問題–JavaScript實現

1. 定位到T[3][11] ,由於不存在T[i][j] == T[i-1][j],所以不用向上搜尋,確定選中一個6分硬幣。尋找組合的思路和填寫T[i][j]的思路幾乎是反過來的

2. 選擇一個6分硬幣後,剩餘的總額為11-6=5。因此定位到T[3][5]中。由於T[3][5]==T[2][5],因此看圖中的藍色箭頭,向上搜尋,直到T[i][j] != T[i-1]

3. 定位到T[2][5]中,此時coins[i]為5分。選中5分硬幣只有,剩餘的總額為5-5=0。

4. 當j=0時,搜尋結束。由上面步驟確定選中的硬幣組合為:1個5分,1個6分。

程式碼

以上就是整個最少硬幣找零問題的分析思路。最終程式碼使用 JavaScript 實現,如果你的 Sublime 支援純 JavaScript,你可以直接複製黏貼程式碼,command + b 直接執行檢視結果,然後修改輸入變數,檢視更多情況下的輸出結果。

//動態規劃 -- 硬幣找零問題
function minCoins(coins,total,n){
	var T = [];

	for(let i = 0;i<n;i++){
		T[i] = []
		for (let j=0;j<= total;j++){
			if(j == 0){
				T[i][j] = 0;
				continue;
			}

			if(i == 0){
				T[i][j] = j/coins[i]; //硬幣找零一定要有個 最小面額1,否則會無解
			}else{
				if(j >= coins[i]){
					T[i][j] = Math.min(T[i-1][j],1+T[i][j-coins[i]])
			
				}else{
					T[i][j] = T[i-1][j];
				}
			}

		}

	}
	findValue(coins,total,n,T);

	return T;

}

function findValue(coins,total,n,T){
	var i = n-1, j = total;
	while(i>0 && j >0){
		if(T[i][j]!=T[i-1][j]){
			//鎖定位置,確定i,j值,開始找構成結果的硬幣組合。 其實根據這種計算方法,只需要考慮最右邊那一列,從下往上推。
			//console.log(T[i][j]);
			break
		}else{
			i--;
		}
	}

	var s = []; //儲存組合結果
	
	while(i >= 0 && j > 0 ){
		
		s.push(coins[i]);
		j=j-coins[i];
		if(j <= 0){
			break; //計算結束,退出迴圈
		}
		//如果 i == 0,那麼就在第 0 行一直迴圈計算,直到 j=0即可
		if(i>0){
			//console.log(i);
			while(T[i][j] == T[i-1][j]){
				i--;
				if(i== 0){
					break;
				}
			}
		}
	}
	
	console.log(s);
	//可以把陣列s return 回去



}


var coins = [1,2,5,6];
var total = 11
var n = coins.length

console.log(minCoins(coins,total,n));
複製程式碼
詳解動態規劃最少硬幣找零問題–JavaScript實現

相關文章