动态规划之背包问题

0/1背包问题

1.二维数组解法

题目描述:有一个容量为m的背包,还有n个物品,他们的重量分别为w1、w2、w3.....wn,他们的价值分别为v1、v2、v3......vn。每个物品只能使用一次,求可以放进背包物品的最大价值。

输入样例:

10 4

2 1

3 3

4 5

7 9

输出样例:

12

解:

符号描述:i表示第i个物品,背包容量为j,dp[i][j]表示从下标为0到i,背包容量为j时任意选取物品所得价值的最大值。所以全局的最优解就是dp[m][n]

背包问题和函数的递归很像,只不过函数递归时从结果去接近边界,而背包问题是从边界出发,从小问题逐步去接近最终所要求的最优解。

先创建一个二维数组

可以看到当背包容量为零,或者可选物品为0时,他的局部最优解都是0

然后从每一行的左到右开始遍历(具体是为什么可以自己试一试)

- 当背包容量为1时由于第一个物品的重量为2无法放进去,所以dp[1][1]=0;

- 当背包容量为2时可以放进第一个物品,dp[2][1]=1;

- 当背包容量大于2时,后续的最大价值都是1;

接着看第二行,这个第二行的含义就是当背包物品容量从1到j变化时,任意选物品1-2的最优解

先放的i=2的物品,然后看剩余重量能容纳的上一行的局部最优解。最后还要判断是否这个最优解比上一行同一列的最优解更大,如果更大就更新状态,否则就继承状态。

- j=1;dp[2][1]=0; 继承上一行的状态

- j=2;dp[2][2]=0; 0<1,继承上一行的状态

- j=3;dp[2][3]=3+dp[2][0]=3 ,3>1,更新状态使dp[2][3]=3;

- j=4;dp[2][4]=3+dp[2][1]=3,同样状态更新

- j=5;dp[2][5]=3+dp[1][2]=4,4>1 状态更新。

后面也是同理。

再看第三行

j从零到三无法当下第三个物品,所以此时的最优解依然是前两个物品最优选择的最优解,依旧继承上一行的状态。

然后从第4列开始,物品3就可以被放下

- j=4,dp[3][4]=5+dp[2][0]=5,5>3,状态更新

- j=5,dp[3][5]=5+dp[2][1]=5,5>4,状态更新

- j=6,dp[3][6]=5+dp[2][2]=6,6>4,状态更新

我想你聪明如你已经看到规律了,接着写出第4行

所以得出来全局的最优解就是12

下面来看代码:

#include<iostream>
#include<algorithm>

using namespace std;

//学校的IDE有点老,好像不支持algorithm里的max
int max(int x,int y)
{
	return x>y?x:y;
}

int m,n;
int dp[30][30]={0};//初始化全部设置为0
int w[30];//重量
int v[30];//价值
//0/1背包问题
int main()
{
	cin>>m>>n;
	int i=0,j=0;
	//输入w,v
	for(i=1;i<=n;i++)
	{
		cin>>w[i]>>v[i];
	}

	//主要的循环体
	for(i=1;i<=n;i++)//物品编号遍历
	{
		for(j=1;j<=m;j++)//背包重量遍历
		{
			if(j<w[i])//这个物品无法放进去,继承上一行的状态
			{
				dp[i][j]=dp[i-1][j];
			}
			else//判断当前最优解与上一行的最优解谁更大
			{
				dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
			}
		}
	}
	cout<<dp[n][m]<<endl;
	return 0;
}

2.一维数组滚动解法

我们注意到二维数组的解法的时间复杂度是m*n,空间复杂度是m*n

而暴力求解的时间复杂度是2^n,空间复杂度也是m*n

二维数组法确实优化的时间复杂度,但是空间复杂度却和暴力一样,因此便有了一维数组滚动解法来进一步优化。

我们在上面的分析中,一步步的更新局部最优解,最终得到所求的最优解。但是有时候并没有更新元素而是继承上一行的最优解,那么是不是就可以只用一个一维数组来存储第i行的最优解,然后需要更新的时候更新一下就可以了。

这时候我们就可以把原有的代码稍作修改:

#include<iostream>
#include<algorithm>

using namespace std;

//学校的IDE有点老,好像不支持algorithm里的max
int max(int x,int y)
{
	return x>y?x:y;
}

int m,n;
int dp[30]={0};//初始化全部设置为0
int w[30];//重量
int v[30];//价值
//0/1背包问题
int main()
{
	cin>>m>>n;
	int i=0,j=0;
	//输入w,v
	for(i=1;i<=n;i++)
	{
		cin>>w[i]>>v[i];
	}

	//主要的循环体
	for(i=1;i<=n;i++)
	{
	for(j=m;j>=w[i];j--)//从最右边遍历,后面的多重背包是从做左到右遍历注意区分。
	{
		dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	
	}
	}
	cout<<dp[m]<<endl;
	return 0;
}

电脑验证

二维数组:

完全背包问题

题目描述:有一个容量为m的背包,还有n个物品,他们的重量分别为w1、w2、w3.....wn,他们的价值分别为v1、v2、v3......vn。每个物品有无限个,求可以放进背包物品的最大价值。

输入样例:10 4 2 1 3 3 4 6 8 10

输出样例:13

完全背包区别于0/1背包就是每个物品的选择没有次数限制。

它们的解题思路的区别在于主要的循环体那,完全背包需要先继承上一层的状态,然后考虑能不能放下,如果不能那这个位置的最优解就是上层位置的最优解,否则就把这个物品放进来,再加上背包容量为j-w[i]的同层位置的最优解(同层是因为物品个数没有限制),这样就可以完成叠加。

二维数组法

来看代码:

#include<iostream>
#include<algorithm>

using namespace std;

int m,n;
int dp[30][30]={0};
int w[30];
int v[30];
//完全背包问题 
int main()
{
	int i,j;
	//输入m,n
	cin>>m>>n;
	// 输入w,v
	for(i=1;i<=n;i++)
	{
		cin>>w[i]>>v[i];
	 } 
	 //主要循环体 
	 for(i=1;i<=n;i++)
	 {
	 	for(j=1;j<=m;j++)
	 	{
	 		//完全背包要先继承上一层状态
			 dp[i][j]=dp[i-1][j];
			 if(j>=w[i])
			 {
			 	dp[i][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]);
			  } 
			 }
		 }
		 cout<<dp[n][m]<<endl;
	return 0;
}

一维数组滚动解法

这个解法同样也是为了降低空间复杂度

所以以同样的方法优化一下代码:

#include<iostream>
#include<algorithm>

using namespace std;


int m,n;
int dp[30]={0};
int w[30];
int v[30];
//完全背包问题 
int main()
{
	int i,j;
	//输入m,n
	cin>>m>>n;
	// 输入w,v
	for(i=1;i<=n;i++)
	{
		cin>>w[i]>>v[i];
	 } 
	 //主要循环体 
	 for(i=1;i<=n;i++)
	 {
	 	for(j=w[i];j<=m;j++)
	 	{
	 		dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
		 }
		 cout<<dp[m]<<endl;
	return 0;
}

电脑验证

二维数组法:

一维数组滚动法:

多重背包问题

多重背包问题与前面两个问题的区别也是在物品的数量上,这次它换成了有限个。

题目描述:有一个容量为m的背包,还有n个物品,他们的重量分别为w1、w2、w3.....wn,他们的价值分别为v1、v2、v3......vn,它们的数量分别有c1、c2、c3......cn个,求可以放进背包物品的最大价值。

输入样例:

10 3

2 1 6

6 10 3

3 6 3

输出样例:

转换成传统的0/1背包问题

这个方法比较容易想到,就不过多赘述了

看代码:

#include<iostream>
#include<algorithm>

using namespace std;

int m,n;
int dp[30]={0};
int w[30];
int v[30];
int c[30];
int main()
{
	int i,j,k;
	//输入m,n
	cin>>m>>n;
	//输入w,v,c(数量) 
	for(i=1;i<=n;i++)
	{
		cin>>w[i]>>v[i]>>c[i];
	 } 
	 for(i=1;i<=n;i++)
	 {
	 	for(k=1;k<=c[i];k++)//多次模拟0/1背包 
	 	{
	 		for(j=m;j>=w[i];j--)//一维滚动法 
	 		{
				dp[j]=max(dp[j],dp[j-w[i]]+v[i]);	 			
			}
	 	}
	 }
	 	for(i=1;i<=m;i++)//这里直接电脑验证了 
	 	{
	 		cout<<dp[i]<<" ";
		 }
		 cout<<endl;
		 cout<<dp[m];
	return 0;
 } 
 //10 3 2 1 6 6 10 3 3 6 3

二进制优化法

这个方法的处理方式也是0\1背包的处理方式,只不过每个物品的数量并不是用十进制的数去一一遍历。

我们知道任意数都可以用二进制来表示,那么我们把十进制代表的数量用二进制来表示:

假设某个物品的属性是1(重量) 1(价值) 600(数量),由于2^0+2^1+...2^8=511<600

所以我们就可以根据这个把这个600的数量分为

然后根据选用不选直接构成0-600间的所有数字

看代码:

#include<iostream>
#include<algorithm>

using namespace std;


int dp[50];
int w[50];
int v[50];
int c[50];
//二进制优化 (也是用一维数组滚动解法解)
int main()
{
	int m, n;
	cin >> m >> n;
	int i, j, w1, v1, c;
	int k = 1, pos = 0;//k表示2的阶乘 pos表示重新划分的组数 
	for (i = 1; i <= n; i++)
	{
		k = 1;
		cin >> w1 >> v1 >> c;
		while (k <= c)
		{
			w[++pos] = w1 * k;
			v[pos] = v1 * k;
			c -= k;
			k *= 2;
		}
		if (c)//说明c并没有被分完
		{
			w[++pos] = w1 * c;
			v[pos] = v1 * c;
		}
	}
	for (i = 1; i <= pos; i++)//pos替换原来的n 
	{
		for (j = m; j >= w[i]; j--)
		{
			dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}
	}
	cout << dp[m] << endl;
	return 0;
}
//10 3 2 1 6 6 10 3 3 6 3

特别感谢某站T_zhao 老师的讲解,讲的很明白。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值