动态规划--揹包问题、比值问题转揹包问题

动态规划--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的硬币,这时要怎么改还没想清楚。


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