詳解動態規劃01揹包問題--JavaScript實現

YinTokey發表於2018-05-19

對其他動態規劃問題感興趣的,也可以檢視

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

詳解動態規劃最長公共子序列--JavaScript實現

一開始在接觸動態規劃的時候,可能會雲裡霧裡,似乎能理解思路,但是又無法準確地表述或者把程式碼寫出來。本篇將一步一步通過作圖的方式幫助初次接觸動態規劃的同學來理解問題。這一篇將以經典的 01揹包 問題為例子來講解,最後通過純 JavaScript 來實現,在 Sublime 上執行演示。當然如果不會 JavaScript 也一點關係都沒有,因為最重要的是理解整個推導過程。在語言實現的時候,也沒有涉及什麼語言特性,基本上懂個C語言就能看懂了。

問題

給定一個固定大小的揹包,揹包的容量為 capacity,有一組物品,存在對應的價值和重量,要求找出一個最佳的解決方案,使得裝入揹包的物品總重量不超過揹包容量 capacity,而且總價值最大。本題中給出了3個物品,其價值和重量分別是 (3,2),(4,3),(5,4)。括號左邊為價值,右邊為重量,揹包容量 capacity 為5。那麼求出其搭配組合,使得揹包內總價最大,且最大價值為多少?

分析

在開始計算之前,需要先對動態規劃中的01揹包問題有基本的理解:

  • 物品無法拆成分數形式,如果能拆分,那就屬於貪婪演算法問題,在後面的文章我們也會介紹貪婪演算法。
  • 不一定恰好裝滿揹包。
  • 裝滿時總價值不一定最大。
  • 每樣物品各一件
  • 常規情況下,在表格中,價格和重量一般都是從上到下遞增的。我們填表分析的時候,其實是事先預設了這種遞增的關係。

清楚了上面的原則之後,就可以開始進行分析了。 當一個新物品出現的時候,需要去決策如果選擇了它,是否會讓總價值最大化。我們根據問題,建立如下表格用於分析:

詳解動態規劃01揹包問題--JavaScript實現

我們對這個表格做一下說明,左上角 valw 分別是物品的價值和重量。即上面所描述的3個物品的價值與重量對應關係。

從第三列到最後一列,使用了變數 j,它表示揹包總容量,最大值為5,也就是前面問題所說的 capacity 的值。

第二行到最後一行,使用 i 表示,下標從0開始,一共有3個物品,所以 i 的最大值為 2。即我們使用i表示物品,在下面介紹中將i=0稱為物品0i=1稱為物品1,以此類推。

除了 j = 0 的情況以外,我們將從左到右,從上到下一步一步去填寫這個表格,來找到最大的價值。

表格中未填寫的空格,表示揹包內物品總價值。我們後面將使用 T[i][j] 二維陣列來表示它。

1. 總容量為0的情況

如果揹包總容量為0,那麼很顯然地,任何物品都無法裝進揹包,那麼揹包內總價值必然是0。所以第一步先填滿 j=0 的情況。

詳解動態規劃01揹包問題--JavaScript實現

2. 第0行,i = 0 的空格分析

正如上面所說,我們接下來將從上到下,從左往右地填寫這個表格。所以現在把注意力定位到 i =0, j = 1 的空格上。

在分析過程中,有一個重要原則:分析第i行時,它的物品組合僅能是小於等於i的情況。

怎麼理解這個原則:比如分析i=0這一行,那麼揹包裡只能裝入物品0,不能裝入其他物品。分析i=1這一行,物品組合可以是物品0物品1

i=0 j=1 : 揹包總容量為1,但是物品0 的重量為 2,無法裝下去,所以這一格應該填 0

i=0 j=2 : 揹包總容量為2,剛好可以裝下物品0 ,由於物品0 的價值為3,因此這一格填 3

i=0 j=3 : 揹包總容量為3,由於根據上面說明的物品組合原則,第0行,僅能放物品0,不需要考慮物品1 和 物品2,所以這一格填 3

i=0 j=4 : 同理,填 3

i=0 j=5 : 同理,填 3

這樣我們可以完成第0行的填寫,如下圖:

詳解動態規劃01揹包問題--JavaScript實現

3. 第1行,i = 1 的空格分析

在這一行,可以由物品0 和物品1 進行自由組合,來裝入揹包。 i=1 j=1 : 揹包總容量為1,但是物品0 的重量為 2,物品1重量為3,揹包無法裝下任何物品,所以填 0

i=1 j=2 : 揹包總容量為2,只能裝下物品0,所以填 3

i=1 j=3 : 揹包總容量為3,這時候可以裝下一個物品1,或者一個物品0,僅僅從人工填表的方式,很容易理解要選擇物品1,但是我們該如何以一個確切的邏輯來表達,讓計算機明白呢?基於上面說說明的價值和重量在表格中從上到下遞增原則,可以確認物品1的價值是大於物品0的,所以預設情況下優先考慮物品1,當選擇了物品1之後,把揹包剩餘的容量和物品1之前的物品重量對比(也就是和物品0的重量對比,如果剩餘重量能裝下前面的物品,那麼就繼續裝)。所以這裡選擇物品1,填 4

i=1 j=4 : 選擇了物品1之後,物品1 的重量為3,揹包容量為4, 減去物品1的重量後, 剩餘容量為1,無法裝下物品0,所以這裡填 4

i=1 j=5 選擇了物品1之後,剩餘的容量為2,剛好可以裝下物品0,所以一格揹包裝了物品1,和物品 0,總價值為7,把 7 填入表格。

這樣我們就完成了第二行的填寫,如下圖:

詳解動態規劃01揹包問題--JavaScript實現

3. 第2行,i = 2 的空格分析

i=2 j=1 : 填 0

i=2 j=2 : 填寫這一行時,3種物品都有機會被裝入揹包。總容量為2時,只能裝物品0,所以填 3

i=2 j=3 : 物品2的重量為4,大於容量j,所以這裡可以參考 T[i-1][j]的值,也就是 i=1 j=3那一格的值,填 4

i=2 j=4 : 可以裝下物品2,價值為5。也可以裝下物品1。這一空格需要謹慎一點。我們將使用更嚴謹的方式來分析。在i=1 j=5中出現了物品組合一起裝入揹包的情況,這一空將延續這種分析方式。我們選擇了物品2,剩餘的容量表示式應為 j-w[i]4 - 4 = 0,剩餘的容量用於上一行的搜尋,由於上一行我們是填寫完的,所以可以很輕易地得到這個值。表示式可以寫成 val[i] + T[i][j-w[i]] ,可以根據這個表示式得出一個值。但是這並不是最終結果,還需要和上一行同一列數值對比,即 T[i-1][j],對比,取最大值。最後這裡填 5

i=2 j=5 : 根據上面計算原理,這裡如果選擇了物品2,那麼最大價值只能5,參照上一行,同一列,價值為7,取最大值。所以放棄物品2,選擇將物品0和物品1裝入揹包,填寫7。

完成後的表格如下:

詳解動態規劃01揹包問題--JavaScript實現

虛擬碼表達

理解了上面整個填表過程,我們要把邏輯抽取出來,在具體程式碼實現之前,先用虛擬碼表達出來。

if(j < w[i]){ //容量小於重量,hold不住
	T[i][j] = T[i-1][j]; //所以值等於上一行,同一列。如果i=0,沒有上一行,則T[i][j] 取0
}else{
	T[i][j] = max(val[i] + T[i-1][j-w[i]] , T[i-1][j]);  //參照上面 i=2 j=4 和 i=2 j=5 時的填表分析
}
複製程式碼

以上這簡短的虛擬碼就是解決問題的核心思路,可以應用於任何你熟悉的程式語言上。

說到這裡,這篇文章應該是要基本告一段落了。

JavaScript 實現

如果你已經理解了上面的填表分析和虛擬碼表達,那麼就可以嘗試著自己去用程式碼實現了。最後放出在使用 JavaScript的一種實現方式供大家參考,不再針對針對程式碼做太多說明,重要區域會有註釋。如果 Sublime 支援純 JavaScript,可以直接複製黏貼,command+b 執行看結果。

function knapSack(w,val,capacity,n){
	var T = []

	for(let i = 0;i < n;i++){
		T[i] = [];
		for(let j=0;j <= capacity;j++){
			if(j === 0){ //容量為0
				T[i][j] = 0;
				continue;
			}	
			if(j < w[i]){ //容量小於物品重量,本行hold不住
				if(i === 0){
					T[i][j] = 0; // i = 0時,不存在i-1,所以T[i][j]取0

				}else{
					T[i][j] = T[i-1][j]; //容量小於物品重量,參照上一行

				}
				continue;
			}
			if(i === 0){
				T[i][j] = val[i]; //第0行,不存在 i-1, 最多隻能放這一行的那一個物品
			}else{
				T[i][j] = Math.max(val[i] + T[i-1][j-w[i]],T[i-1][j]);

			}
		}

	}

	findValue(w,val,capacity,n,T);


	return T;
}
![](https://user-gold-cdn.xitu.io/2018/5/19/16377bff6e2d5c63?w=448&h=468&f=png&s=180322)

//找到需要的物品
function findValue(w,val,capacity,n,T){

	var i = n-1, j = capacity;
	while ( i > 0 && j > 0 ){

		if(T[i][j] != T[i-1][j]){
			console.log('選擇物品'+i+',重量:'+ w[i] +',價值:' + values[i]);
			j = j- w[i];
			i--;
		}else{
			i--;  //如果相等,那麼就到 i-1 行
		}
	}
	if(i == 0 ){
		if(T[i][j] != 0){ //那麼第一行的物品也可以取
			console.log('選擇物品'+i+',重量:'+ w[i] +',價值:' + values[i]);

		}
	}
}

// w = [2,3,4].  val = [3,4,5] , n = 3 , capacity = 5
//function knapSack([2,3,4],[3,4,5],5,3);
// 
var values = [3,4,5],
	weights = [2,3,4],
	capacity = 5,
	n = values.length;

console.log(knapSack(weights,values,capacity,n));
複製程式碼

輸出結果

詳解動態規劃01揹包問題--JavaScript實現

相關文章