动态规划之背包问题

背包问题泛指以下这一种问题:

给定一组有固定价值和固定重量的物品,以及一个已知最大承重量的背包,求在不超过背包最大承重量的前提下,能放进背包里面的物品的最大总价值。


【01背包】

【问题】

0-1背包问题是指每一种物品都只有一件,可以选择放或者不放。现在假设有n件物品,每件物品价值为V,背包承重为m。

【分析】

对于这种问题,可以采用一个二维数组解决,f[i][j],表示此时背包的承重为j,加入了前i件物品的时候背包里面物品的最大总价值。所以f[n][m]就是最终结果。

【状态转移方程】

①加入把f[i][j]放入背包,则f[i][j] = f[i - 1][j - weight[i]] + value[i],这里的f[i-1][j-- weight[i]] + value[i]]的意思是,没放入这个物品前的最大价值加上这个物品的价值,f[i - 1][j - weight[i]]这部分,i - 1代表之前i-1件物品,j - weight[i]这里表示要把这件物品放进背包后剩下的空间位置,所以要选择前i-1件物品,在j-weight[i]这个重量下最大价值。

②不加入f[i][j],这里的最大价值就等于前i-1件物品在这个重量下的最大价值f[i][j] = f[i - 1][j]

③背包放不下当前这一件物品,这种情况下f[i][j] = f[i - 1][j]。

状态转移方程就是:放得下情况下:f[i][j] = max(f[i][j] = f[i - 1][j] , f[i - 1][j - weight[i]] + value[i]) 

【表格分析】

j表示空间

i表示物品

j=0

j=1

j=2

j=3

j=4

i=1

 

 

 

 

 

i=2 w=1

 

 

 

 

i=3

 

 

 

 

 

f2][3]=max(f[2-1][3],f[2-1][3-w]+v[2][3]) ,f[2-1][3-w]等于f[1][2]表示选放入2前,选第1个物品时并能放下w的空间。

//求每一次这个物品在重量j的情况下的最大价值
for (int i = 1; i <= n; i++)
{
	for (int j = 1; j <= m; j++)
	{
		if (weight[i] <= j)                //这个物品的重量起码要小于在这个位置的重量
			f[i][j] = max(f[i - 1][j], f[i - 1][j - weight[i]] + value[i]);
		else
			f[i][j] = f[i - 1][j]
	}
}

【优化空间】

j表示空间

i表示物品

j=0

j=1

j=2

j=3

j=4

i=1

 

 

 

 

i=2

 

 

 

 

i=3

 

 

 

 

 

例如 j=3,f[j]=max( f[j], f[j-w[i]]+v[i] )

                   这个f[j]为i=1时候的f[j]此情况下不放物品二,然后f[j-w[i]]+v[i]是放物品二,用i=2时最大价值去更新i=1。倒序是因为假如不把第二层循环颠倒,当要求f[3]时,f[2]、f[1]已经是同一行(i=2)的状态了,根本没法求,因为要跟上一行作比较。

for (int i = 0; i < n; i++)
{
	for (int j = W; j >= w[i]; j--)     
	{
		f[j] = max(f[j], f[j - w[i]] + v[i]);
	}
}

【01背包装满】

01背包必须装满,初始化除了f[0]其余全为正无穷或者负无穷(取决于题目是求最大值还是最小值),取最大值时,所以非0的初始值是负无穷:

//求01背包所装物品价值之和的最大值时,inf为负无穷大
const int inf=100000000;
for(int i=1;i<=n;i++)
{
	dp[i]=-inf;
}
dp[0]=0;
 for(int i=0;i<n;i++)
 {
	 for(int j=W;j>=weight[i];j--)
	 {                                 //每一种物品只能放一次,则此处要j从大到小,采用逆序
		 dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
	 }
 }

【完全背包】

【问题】

完全背包问题是指每种物品都有无限件。

【分析】

完全背包问题与01背包问题的区别在于完全背包每一件物品的数量都有无限个,而01背包每件物品数量只有1个。

【代码分析】

//不要求装满
for (int i = 1; i <= n; i++) 
{
	for (int j = weight[i]; j <= m; j++)  
	{
		f[j] = max(f[j], f[j - weight[i]] + value[i]);
	}
}
//要求装满
for(int i=1;i<=W;i++)
{
	dp[i]=INF;                   //初始化为正无穷大,求最小值
}
dp[0]=0;                        //初始化第一个
for(int i=1;i<=n;i++)
{
	for(int j=weight[i];j<=W;j++)
	{   
		if(dp[j-w[i]]!=-INF)
		dp[j]=min(dp[j],dp[j-weight[i]]+value[i]);   
	}
}

为什么第二个for里面是顺序呢?

i=1时,计算只放第一件物品的最大价值。

i=2时,计算加上第二件物品的最大价值(在只放第一件物品的前提下一直放到最大价值)

第二层循环要从j=weight[i]开始 。

总的来说,01背包逆序是因为max里面两项都是前一状态的最大值

而这里因为每个物品都是无限的,f[j]表示容量为j的时候背包里得价值,这里得背包不是前一个背包,而是当前得背包。例如:f[2] = max(f[2], f[2 - weight[2]] + value[2]);这里指的是在空间为2时,已经放入了一个2了,是选择继续放2这个物品值最大,还是不放这个物品的值最大。

【多重背包】

【问题】

有N种物品和一个容量为W的背包。第i种物品最多有num[i]件可用,每件重量是weight[i],价值是value[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

【方法一】

仅转化为0-1背包问题即可。比如,有2件价值为5,重量为2的同一物品,我们就可以分为物品a和物品b,a和b的价值都为5,重量都为2,但我们把它们视作不同的物品。

 

1

2

3

4

a

b

a(第一个a有两个)

b

 

int k = n + 1;
for (int i = 1; i <= n; i++)     
{
	while (num[i] != 1) 
	{
		weight[k] = weight[i];
		value[k] = value[i];
		k++;
		num[i]- -;
	}
}    //让每个物品都不一样
for (int i = 1; i <= k; i++)
{
	for (int j = m; j >= 1; j--)
		if (weight[i] <= j) f[j] = max(f[j], f[j - weight[i]] + value[i]);
}

【方法二】

数据量比较大的时候转化为0-1背包+完全背包问题,这里用到二进制转换,比如20个3,我们可以把3分为1,2,4,8,5个3。

官方一点的说:

7的二进制 7 = 111 它可以分解成 001 010 100 这三个数可以

组合成任意小于等于7 的数,而且每种组合都会得到不同的数

15 = 1111 可分解成 0001 0010 0100 1000 四个数字

如果13 = 1101 则分解为 0001 0010 0100 0110 前三个数字可以组合成

7以内任意一个数,加上 0110 = 6 可以组合成任意一个大于6 小于13

的数,虽然有重复但总是能把 13 以内所有的数都考虑到了。

 

int ZeroOnePack(int weight,int value)
{
	for(int j=W;j>=weight;j--)
	{
		dp[j]=max(dp[j],dp[j-weight]+value);
	}
}
int CompletePack(int weight,int value)
{
	for(int j=weight;j<=W;j++)
	{
		dp[j]=max(dp[j],dp[j-weight]+value);
	}
}
int MultiplePack(int weight,int value,int num)
{
	if(W<=num*weight)     //无法将该物品全部装入背包,直接将它当做完全背包
	{
		CompletePack(weight,value);
		return ;
	}  
//可以将该物品全部装入背包,这种情况比较复杂,可以将这些物品拆成多个单件物品,需要二进制优化
	int k=1;
	while(k<num)
	{
		ZeroOnePack(weight*k,value*k);    
		num-=k;
		k<<=1;    //k=k*2   k<<=n等价于 k=k*(2的n次方)k>>=n  等价于 k=k/(2的n次方)
	}
	ZeroOnePack(weight*num,value*num);
	
/*k:num	  num	             k
  1<20	  20-1=19    	     2
  2<19	  19-2=17	     4
  4<17 	  17-4=13	     8
  8<13	  13-8=5	     5  
 所以最终分解为1,2,4,8,5=20*/

    //也可以这样写
	/*for(int k=1;k<num;k<<=1)
	{
		ZeroOnePack(weight*k,value*k);
		num-=k;
	}
	ZeroOnePack(weight*num,value*num);*/



}
main()中
for(int i=1;i<=N;i++)
{
	MultiplePack(weight[i],value[i],num[i]);
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值