0-1揹包問題的遞歸實現與非遞歸實現

題目有N件物品和一個容量爲V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使價值總和最大。基本思路這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量爲v的揹包可以獲得的最大價值。則其狀態轉移方程便是:
 

這個方程非常重要,基本上所有跟揹包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:“將前i件物品放入容量爲v的揹包中”這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化爲一個只牽扯前i-1件物品的問題。如果不放第i件物品,那麼問題就轉化爲“前i-1件物品放入容量爲v的揹包中”,價值爲f[i-1][v];如果放第i件物品,那麼問題就轉化爲“前i-1件物品放入剩下的容量爲v-c[i]的揹包中”,此時能獲得的最大價值就是f[i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i]。優化空間複雜度以上方法的時間和空間複雜度均爲(V N),其中時間複雜度應該已經不能再優化了,但空間複雜度卻可以優化到(N)1。這個方程非常重要,基本上所有跟揹包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:“將前i件物品放入容量爲v的揹包中”這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化爲一個只牽扯前i-1件物品的問題。如果不放第i件物品,那麼問題就轉化爲“前i-1件物品放入容量爲v的揹包中”,價值爲f[i-1][v];如果放第i件物品,那麼問題就轉化爲“前i-1件物品放入剩下的容量爲v-c[i]的揹包中”,此時能獲得的最大價值就是f[i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i]。優化空間複雜度以上方法的時間和空間複雜度均爲(V N),其中時間複雜度應該已經不能再優化了,但空間複雜度卻可以優化到(N)。


優化空間複雜度以上方法的時間和空間複雜度均爲(V N),其中時間複雜度應該已經不能再優化了,但空間複雜度卻可以優化到(N)1。先考慮上面講的基本思路如何實現,肯定是有一個主循環i=1..N,每次算出來二維數組f[i][0..V]的所有值。那麼,如果只用一個數組f[0..V],能不能保證第i次循環結束後f[v]中表示的就是我們定義的狀態f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]兩個子問題遞推而來,能否保證在推f[i][v]時(也即在第i次主循環中推f[v]時)能夠得到f[i-1][v]和f[i-1][v-c[i]]的值呢?事實上,這要求在每次主循環中我們以v=V..0的順序推f[v],這樣才能保證推f[v]時f[v-c[i]]保存的是狀態f[i-1][v-c[i]]的值。
僞代碼如下:


其中的f[v] = maxff[v]; f[v..c[i]]g一句恰就相當於我們的轉移方程f[i][v] = maxff[i..1][v]; f[i..1][v .. c[i]]g,因爲現在的f[v-c[i]]就相當於原來的f[i .. 1][v .. c[i]]。如果將v的循環順序從上面的逆
序改成順序的話,那麼則成了f[i][v]由f[i][v-c[i]]推知,與本題意不符,但它卻是另一個重要的揹包問題P02最簡捷的解決方案,故學習只用一維數組解01揹包問題是十分必要的。

注意這個過程裏的處理與前面給出的僞代碼有所不同。前面的示例程序寫成v=V..0是爲了在程序中體現每個狀態都按照方程求解了,避免不必要的思維複雜度。而這裏既然已經抽象成看作黑箱的過程了,就可以加入優化。費用爲cost的物品不會影響狀態f[0..cost-1],這是顯然的。



遞歸實現:

#include<iostream>
using namespace std;

const int W = 150;
const int number = 5;
const int VALUE[] = {60, 20, 10, 60, 100};
const int WEIGHT[] = {20, 30, 50, 60, 80};
 

//function Make( i {處理到第i件物品} , j{剩餘的空間爲j}) :integer;
int Make(int i, int j)
{  
	int r1 = 0;
	int r2 = 0;
	int r = 0;
	
	if (i == -1)
	{
		return 0;
	}

	if(j >= WEIGHT[i])   //揹包剩餘空間可以放下物品 i  
	{
		r1 = Make(i-1,j - WEIGHT[i]) + VALUE[i]; //第i件物品放入所能得到的價值
		r2 = Make(i-1,j); //第i件物品不放所能得到的價值  
		r = (r1>r2)?r1:r2;
	}   

	return r;
}


void main()
{
	int maxValue = Make(number-1, W);
	cout<<"maxValue: "<<maxValue<<endl;
}


非遞歸實現:
#include<iostream>
using namespace std;

const int W = 150;
const int number = 5;
const int VALUE[] = {60, 20, 10, 60, 100};
const int WEIGHT[] = {20, 30, 50, 60, 80};
int f[151];

void ZeroOnePack(int w, int v) 
{
	for(int x = W; x >= w; x--)
		f[x]=(f[x] > (f[x-w]+v))?f[x]:(f[x-w]+v);
 
}


void main()
{
	for (int i=0; i < 151; i++)
	{
		f[i] = 0;
	}

	for (int j=0; j < number; j++)
	{
		ZeroOnePack(WEIGHT[j], VALUE[j]);
	}
 
	cout<<"maxValue: "<<f[W]<<endl;
	 
	
}


對於非遞歸的實現思路,我想下面這個例子和相應的圖片是最好的說明了:

因爲揹包最大容量M未知。所以,我們的程序要從1到M一個一個的試。比如,開始任選N件物品的一個。看對應M的揹包,能不能放進去,如果能放進去,並且還有多的空間,則,多出來的空間裏能放N-1物品中的最大價值。怎麼能保證總選擇是最大價值呢?看下錶。
測試數據:
10,3
3,4
4,5
5,6



這張圖表剛好說明了調用ZeroOnePackage函數的整個過程,和ZeroOnePackage函數裏的執行for循環的執行過程:

c[i][j]數組保存了1,2,3號物品依次選擇後的最大價值.

這個最大價值是怎麼得來的呢?從揹包容量爲0開始,1號物品先試,0,1,2,的容量都不能放.所以置0,揹包容量爲3則裏面放4.這樣,這一排揹包容量爲4,5,6,....10的時候,最佳方案都是放4.假如1號物品放入揹包.則再看2號物品.當揹包容量爲3的時候,最佳方案還是上一排的最價方案c爲4.而揹包容量爲5的時候,則最佳方案爲自己的重量5.揹包容量爲7的時候,很顯然是5加上一個值了。加誰??很顯然是7-4=3的時候.上一排 c3的最佳方案是4.所以。總的最佳方案是5+4爲9.這樣.一排一排推下去。最右下放的數據就是最大的價值了。(注意第3排的揹包容量爲7的時候,最佳方案不是本身的6.而是上一排的9.說明這時候3號物品沒有被選.選的是1,2號物品.所以得9.)



發佈了14 篇原創文章 · 獲贊 19 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章