動態規劃--揹包問題、比值問題轉揹包問題

動態規劃--dynamic programming

    可以用動態規劃解決的問題都可以通過直接遞歸來解決,但這樣的時間複雜度通常都會高達指數關係。所以,通過以空間換時間的代價,用dp通常可以達到o(N*M)的時間複雜度,至於空間複雜度,就要看問題的要求了:

1)如果要求得到整個dp過程中的細節,如揹包問題中的放了哪些物品、LCS問題中的LCS的值,那麼就要o(N*M)的空間複雜度;

2)如果只需要知道最後的結果,如揹包問題中所放物品的最大價值、LCS問題中的LCS的長度,那麼就只需要o(n)的空間複雜度。

當然,如果只需要直接輸出存入了的物品,那麼o(n)的空間複雜度也是可以的。

下面先討論0-1揹包問題(不要求正好放滿,要求輸出放了哪些物品)的實現:

//動態規劃求解
int ZeroOnePack(int total_weight, int w[], int v[], int flag[], int n) {
	//動態創建二維數組
	int **c=new int*[n+1];
	for(int i=0;i<n+1;i++){
		c[i]=new int[total_weight+1];
	}
	for(int i = 0; i < n+1; i++)  
		c[i][0]=0;        //第0列都初始化爲0  
	for(int j = 0; j < total_weight+1; j++)  
		c[0][j]=0;        //第0行都初始化爲0  
	//c[i][j]表示前i個物體放入容量爲j的揹包獲得的最大價值:i=1,2,...,n;j=1,2,...,total_weight。
	//其中第i件物品的重量和價值分別爲w[i-1],v[i-1]。
	// 狀態轉移方程:c[i][j] = max{c[i-1][j], c[i-1][j-w[i-1]]+v[1]}
	//狀態轉移方程的解釋:
	//第i件物品要麼放,要麼不放:
	//如果第i件物品不放的話,就相當於求前i-1件物體放入容量爲j的揹包獲得的最大價值
	//如果第i件物品放進去的話,就相當於求前i-1件物體放入容量爲j-w[i-1]的揹包獲得的最大價值

	for (int i = 1; i < n+1; i++) {
		for (int j = 1; j < total_weight+1; j++) {
			if (w[i-1] > j) {
				// 說明第i件物品大於揹包的重量,放不進去
				c[i][j] = c[i-1][j];
			} else {
				//說明第i件物品的重量小於揹包的重量,所以可以選擇第i件物品放還是不放
				if (c[i-1][j] > v[i-1]+c[i-1][j-w[i-1]]) {//如果不放大於放
					c[i][j] = c[i-1][j];
				}
				else {
					c[i][j] =  v[i-1] + c[i-1][j-w[i-1]];
				}
			}
		}
	}

	//下面求解哪個物品應該放進揹包
	int i = n, j = total_weight;
	while (c[i][j] != 0) {
		if (c[i-1][j-w[i-1]]+v[i-1] == c[i][j]) {
			// 如果第i個物體在揹包,那麼顯然去掉這個物品之後,前面i-1個物體在重量爲j-w[i-1]的揹包下價值是最大的
			flag[i-1] = 1;
			j -= w[i-1];//揹包容量減w[i-1]
		}--i;
	}
	int val=c[n][total_weight];
	//釋放二維數組  
	for(int i=0;i<n+1;++i){
		delete[] c[i];
	}
	delete[] c;
	return val;
}

int _tmain(int argc, _TCHAR* argv[])
{
	int total_weight = 8;
	int w[4] = {4, 3, 4, 5};
	int v[4] = {1, 4, 5, 6};
	int flag[4]; //flag[i]=0表示物品i不放入揹包,否則放入
	int total_value = ZeroOnePack(total_weight, w, v,flag, 4);
	cout << "需要放入的物品如下" << endl;
	for (int i = 0; i < 4; i++) {
	if (flag[i] == 1)
		cout << i << "重量爲" << w[i] << ", 價值爲" << v[i] << endl;
	}
	cout << "總的價值爲: " << total_value << endl;
	system("pause");
	return 0;
}




接下來,分析只要求輸出能放下的物品的最高價值的0-1揹包問題,因爲c[i][j]的結果只依賴於c[i-1][j]和才c[i-1][j-w[i-1]],所以,只要知道前一行的數據,就能求出接下來一行的數據。至於當前行的數據的保存問題,如果從行尾開始計算的話,直接用當前行c[i][j]代替前一行的數據c[i-1][j]就能解決,所以,存儲已經計算過的前i個物品放入容量爲1,2,...,total_weight的揹包的最大值這些數據只需要taotal_weight+1(加上c[0]=0)的空間。

0-1揹包問題(不要求正好放滿,只要求輸出能放下的物品的最高價值)的實現:

int zero_one_pack(int total_weight, int w[], int v[], int n) {
	int *c=new int[total_weight+1];
	for(int i = 0; i < total_weight+1; i++)  
			c[i]=0;       
  
	for (int i = 0; i < n; i++) {
		for (int j = total_weight; j >= 1; j--) {
			if (w[i] > j) {
			// 說明第i件物品大於揹包的重量,放不進去
			continue;
		} else {
			//說明第i件物品的重量小於揹包的重量,所以可以選擇第i件物品放還是不放
			if (c[j] < v[i]+c[j-w[i]]) {//如果不放大於放
				c[j] =  v[i] + c[j-w[i]];
			}
		}
		}
	}
	int val=c[total_weight];
	delete[] c;
	return val;
}

int _tmain(int argc, _TCHAR* argv[])
{	
	int total_weight = 10;//揹包容量
	int w[4] = {4, 3, 4, 5};//物品重量
	int v[4] = {1, 4, 5, 6};//物品價值
	int total_value = zero_one_pack(total_weight, w, v, 4);
	cout << "總的價值爲: " << total_value << endl;
	system("pause");
	return 0;
}

對於0-1揹包問題,如果要求正好裝滿的話,就要對上面的代碼做些改動,只要在初始化數組c的時候稍加改動就行。初始化的時候,將c[0]/c[i][0]初始化爲0,其他都初始化爲負無窮大。這樣,對於c[j]<v[i]+c[j-w[i]]這個式子,如果前i-1個物品能正好放入容量爲j-w[i]的揹包中(即c[j-w[i]]>0,如果放不進,c[j-w[i]]必然是負無窮大,因爲負無窮大加有限值依然是負無窮大),那麼前i個物品必然能正好放入容量爲j的揹包中。這時c[j]<v[i]+c[j-w[i]]這個式子肯定成立,c[j]將被賦值爲v[i]+c[j-w[i]]。當然在程序中不能真的將變量賦值爲負無窮大,不過可以用很小的值代替,如32系統上用const int MIN=0x80000000代替。這樣,如果c[weight]>0,表明揹包能正好被裝滿,且裝入物品的價值爲c[weight]。

對於0-1揹包問題,如果要求正好裝滿的代碼如下:

const int MIN=0x80000000;

//動態規劃求解
int ZeroOnePack(int total_weight, int w[], int v[], int flag[], int n) {
	//動態創建二維數組
	int **c=new int*[n+1];
	for(int i=0;i<n+1;i++){
		c[i]=new int[total_weight+1];
	}
	for(int i = 0; i < n+1; i++)  
		for(int j = 0; j < total_weight+1; j++)  
			if(j==0)
				c[i][j]=0;
			else
				c[i][j]=MIN;        //第0行都初始化爲0  
	//c[i][j]表示前i個物體放入容量爲j的揹包獲得的最大價值:i=1,2,...,n;j=1,2,...,total_weight。
	//其中第i件物品的重量和價值分別爲w[i-1],v[i-1]。
	// 狀態轉移方程:c[i][j] = max{c[i-1][j], c[i-1][j-w[i-1]]+v[1]}
	//狀態轉移方程的解釋:
	//第i件物品要麼放,要麼不放:
	//如果第i件物品不放的話,就相當於求前i-1件物體放入容量爲j的揹包獲得的最大價值
	//如果第i件物品放進去的話,就相當於求前i-1件物體放入容量爲j-w[i-1]的揹包獲得的最大價值

	for (int i = 1; i < n+1; i++) {
		for (int j = 1; j < total_weight+1; j++) {
			if (w[i-1] > j) {
				// 說明第i件物品大於揹包的重量,放不進去
				c[i][j] = c[i-1][j];
			} else {
				//說明第i件物品的重量小於揹包的重量,所以可以選擇第i件物品放還是不放
				if (c[i-1][j] > v[i-1]+c[i-1][j-w[i-1]]) {//如果不放大於放
					c[i][j] = c[i-1][j];
				}
				else {
					c[i][j] =  v[i-1] + c[i-1][j-w[i-1]];
				}
			}
		}
	}

	//下面求解哪個物品應該放進揹包
	int i = n, j = total_weight;
	while (c[i][j] > 0) {
		if (c[i-1][j-w[i-1]]+v[i-1] == c[i][j]) {
			// 如果第i個物體在揹包,那麼顯然去掉這個物品之後,前面i-1個物體在重量爲j-w[i-1]的揹包下價值是最大的
			flag[i-1] = 1;
			j -= w[i-1];//揹包容量減w[i-1]
		}--i;
	}
	int val=c[n][total_weight];//如果val>0則能正好裝滿,否則無解
	//釋放二維數組  
	for(int i=0;i<n+1;++i){
		delete[] c[i];
	}
	delete[] c;
	return val;
}

題意:有1,5,10,25,50這五種硬幣。給一個價值,求有多少種組合可以得到該價值。

完全揹包:因爲每個物品都能放多次,所以第二層循環從小到大的的順序。

首先想到了用dp[j][0]代表容量爲j的揹包能裝下的最大價值,dp[j][1]代表容量爲j的揹包裝下最大價值時的組合方式數量

一定要注意:揹包問題的外層循環是物品種類0,1,2...,n-1,內層循環是揹包容量v[i],...,weight。在確定轉移方程的時候最好腦海裏能回憶書上那個表格~

const int M = 2000;
int _tmain(int argc, _TCHAR* argv[])
{
	unsigned int dp[M][2];
	int v[5] = {1, 5, 10, 25, 50};
	int weight;
	int n=5;
	while(cin)
	{
		cin>>weight;
		memset(dp, 0, sizeof(dp));
		dp[0][1] = 1;
		for(int i = 0; i < n; i++)
		{
			for(int j = v[i]; j <= n; j++)
			{
				//dp[j][0]:前i-1個物品放入容量爲j的揹包的最大價值
				//dp[j-v[i]][0]:前i個物品放入容量爲j-v[i]的揹包的最大價值,因爲是從行頭到行尾進行內層循環的,所以對於第i個物品,dp[j-v[i]][0]已經計算過了
				//情況一:前i-1個物品放入容量爲j中的價值不能達到前i個物品放入容量爲j中的價值
				//那麼,對於將前i個物品放入容量爲j中的放入情況就不包括只放前i-1個物品的情況,必須放第i個
				//所以,相當於前i個物品放入容量爲j-v[i]後,再放一個物品i。所以組合方式數量相等
				if(dp[j-v[i]][0]+v[i] > dp[j][0])
				{
					dp[j][0] = dp[j-v[i]][0]+v[i];
					dp[j][1] = dp[j-v[i]][1];
				}
				//情況二:前i-1個物品放入容量爲j中的價值能達到前i個物品放入容量爲j中的價值
				//那麼,前i個物品放入容量爲j中的方式=前i-1個物品放入容量爲j中的方式(不放第i個)+放第i個時的方式
				//其中,放第i個時的方式數量等於前i個物品放入容量爲j-v[i]的方式數量(其實這點是幣值問題中最巧妙的)
				else if(dp[j-v[i]][0]+v[i] == dp[j][0])
				{
					dp[j][1] += dp[j-v[i]][1];
				}
			}
		}
		cout<<dp[weight][1]<<endl;
	}
	system("pause");
	return 0;
}

對於一定要恰好裝滿的情況,比如沒有面值爲2的硬幣,這時要怎麼改還沒想清楚。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章