0 1背包问题

简述

  0 1 背包可以说是讲述动态规划的最适合入门的问题了,所谓动态规划,其实可以简单理解为一种特殊的分而治之的策略,和我们常见的如二分查找等分而治之策略的不同在于,可以采用分而治之策略的问题,它划分出来的若干子问题是没有交集的,即子问题的求解完全不受其他因素的干扰,就如下图所示:在这里插入图片描述
  我们只需要将目光聚集解决如何解决每一个小问题,再将所有待解决的小问题合并,复杂的大问题也就迎刃而解。
  但对于一般的动态规划问题,它分出来的子问题却往往互相交错,你中有我我中有你,使得问题变得十分复杂,在求解一个子问题的时候还需要考虑其他子问题对该问题的影响,这使得我们陷入了一种复杂的递归调用中。

问题

  01背包问题的描述大概就是我有一个负重固定的背包,还有一地物品,物品的重量和价值随机,但每件物品都只有一个,其实有相同的也无所谓,现在需要尽量装走这些物品,使得在背包负重足够的情况下,被背包装入的物品总价值最大
在这里插入图片描述
  上图中的W代表每个物品的重量,V代表每个物品对应的价值,我们可以很快地想到一个方法,每次拿上未拿的物品中 v/w 最大的那个物品,直到背包装不下为止,这种想法是非常自然的,但往往却不是正确的,以上述图为例, v/m 最大的是第五个物品,其次是第四个,然后是第六个, 此时这三个物品重量为 3+4+9 = 16,已经放不下任何一个其他物品了,这三个物品的总价值为 4+7+8 = 19;然而经过观察,我们发现明明取 1 , 2, 4 , 5 这四个物品,总重量为19 , 总价值为20,比贪心策略求出的值还要大!!

  我们当然可以遍历所有的情况,然后找出那个 v 最大的情况,但这种遍历是非常耗时的,我们有更好的解法,只需将思路稍微转变一下即可。
  我们将目光聚焦在每次做抉择的时刻,假设我已经挑选过前i-1个物品,这些物品我有的拿了有的没拿,在背包负重不足的条件下(前i-1个物品并未全部放入或者全部放入后放不下第i个物品),这是我这个状态下背包存放物品的最优解,那么在遇见第i个物品后,我便要思考拿不拿这个物品了, 无非有两种情况,将背包里的一些物品丢弃,且并不需要考虑丢了谁,只关注背包负重,直到可以装下第i个物品 ,然后将这个物品放入背包; 或者干脆不用管这个第i个物品, 因为就算我为它腾出了空间,带上它以后价值居然还没有我原来背包大,我们当然选择使得我背包价值尽量大的那种选择,选择完毕后,目前这个背包的状态,就是探寻了前i个物品后我得到的最优的背包!
  这很好解释,简单来说,我之前的状态是最优的,那么我仅需要考虑这次的选择最优,那么我总体的状态也就是最优的。
  有了上述一层关系,就可以很简单地得到如下推导:
      假设 i 为 前 i 个物品, j 为当前背包负重,那么f(i,j)表示:只考虑前i个物品的情况下,负重 j情况下 所能得到的最优解,递推式如下:

                          f(i,j) = max{ f(i-1 , j) , f( (i-1)  ,  j-i.w ) + i.v}

   一般的,我们称上述推导式为状态转移方程

  假设总共有 M 件物品 , 背包的总负重为 G , 不妨再通过上面的描述再解释一遍 ,考虑前 M 件物品 , 负重G情况下所能得到的最优解,道法自然 , 这不正就是我们所要求的最后的问题的解吗?
  关于上述的状态转换方程的解释,我提到了一个条件,在背包负重不足的情况下,那么如果背包的负重大到可以将所有物品都存放进去,这个方程还成立吗?
  换一种角度就能够想到,我们实际的操作其实就是填表,背包负重越大,表越长,填到最后如果负重足够,所有的物品也都是可以顺利被包含在内的。

小问题

  还有一个小问题值得我们思考,假设我们通过递归的思想,不断地递归递归,直到原始状态,先不考虑原始状态的情况,将重点放在 状态转移方程上,这两个比对的上一层状态,会不会是之前已经出现过的呢? 如果已经出现过了,我们自然没有必要耗费资源再计算一遍,这也是文章开头我提到的交叉子问题的情况,很显然,确实会发生这种问题,而且还十分普遍。
  如何解决这种不断重复的问题,最简单明了的方法就是记下来,将我们之前计算过的所有 f() 都记录在对应位置,这样当我们要求解一个子问题,我们先查询之前是否有计算过,如果有,直接返回,如果没有,则计算该值,并记录下来留作下次查询。当然,还有一种方法是迭代的自底而上全都求解一遍,下面的代码就采取的是迭代方式而不是递归。

代码
const int N = 2002; //根据题目要求顶下数组大小
int f[N][N] ; 

int getAnswer(int n , int bag , int* w , int* v){ 
// n代表物品个数 , bag指的是背包负重 , w存放物品质量,v存放物品价值
	memset(f , 0 , sizeof(0)); //数组初始化
	for(int i = 1 ; i <= n ; i++){
		for(int j = w[i] ; j <= bag ; j++){
		 //细节:在这个背包负重不超过它的重量时,它的值一直都是0,表示没有放进去
			f[i][j] = max( f[i-1][j] , f[i-1][j-w[i]] + v[i]);
		}
	}
	return f[n][bag];
}

  上述的代码实际上就是展示一种填表的操作,将所有存放所有最优状态的表填好,答案自然就在这张表的最末尾,由此可以得到该算法的复杂度 O( n * bag) ,所以背包问题通常它的物品数或者背包负重都能太大。

优化:滚动数组

  我们再来看一下它的状态转移方程:

                          f(i,j) = max{ f(i-1 , j) , f( (i-1)  ,  j-i.w ) + i.v}

  结合上面的代码就不难发现,第 i 行的状态的求解 , 仅与第 i-1行有关 , 我们需要的答案在那张表的最末端 , 也就是说,这张表的其他地方对于我来说是无所谓的,那么是否可以仅开一个一维的数组,存放当前行的最优状态,等到下一行时,再通过原有的值刷新一遍,得到新的一行的状态值呢?
  答案是当然的,方式也很简单,仅需要逆向遍历
  为何要逆向遍历呢? 再仔细观察一下状态转移方程中第二个子状态:f(i -1 , j-i.w) + i.v,如果做顺序遍历,那么它找到的背包实际上是已经更新过以后的背包了, 原来的数据被抹去无法找回,但如果是逆序,则这个子状态需要的上一层的数据,都是未曾动过的。
  此时的状态转换方程为:f(j) = max{ f(j) , f(j - i.w) + i.v }
  优化后的代码如下:

const int N = 2002; //根据题目要求顶下数组大小
//int f[N][N] ; 
int f[N];

int getAnswer(int n , int bag , int* w , int* v){ //采用滚动数组
	memset(f,0,sizeof(f)); //数组初始化
	for(int i = 1 ; i <= n ; i++){
		for(int j = bag ; j >= 	w[i] ; j--){
			//逆序遍历刷新
			f[j] = max(f[j] , f[j-w[i]] + v[i]);
		}
	}
	return f[bag];
}

  完整代码如下

#include <bits/stdc++.h>
using namespace std;

const int N = 2002; //根据题目要求顶下数组大小
//int f[N][N] ; 
int f[N];

int getAnswer(int n , int bag , int* w , int* v){ //采用滚动数组
	memset(f,0,sizeof(f)); //数组初始化
	for(int i = 1 ; i <= n ; i++){
		for(int j = bag ; j >= 	w[i] ; j--){
			//逆序遍历刷新
			f[j] = max(f[j] , f[j-w[i]] + v[i]);
		}
	}
	return f[bag];
}



// int getAnswer(int n , int bag , int* w , int* v){  //二维数组遍历
// 	memset(f , 0 , sizeof(0)); //数组初始化
// 	for(int i = 1 ; i <= n ; i++){
// 		for(int j = w[i] ; j <= bag ; j++){
// 			f[i][j] = max( f[i-1][j] , f[i-1][j-w[i]] + v[i]);
// 		}
// 	}
// 	return f[n][bag];
// }



int main(int argc, char const *argv[])
{
	int n ; // 物件数量
	int bag; //背包大小
	cin>>n>>bag;
	int w[n+2];  int v[n+2]; // 保存物品的重量和价值
	for(int i = 1 ; i <= n ; i++){
		int tw , tv; //临时变量,记录重量和价值
		cin>>tw>>tv;
		w[i] = tw ; 
		v[i] = tv ; 
	}
	cout<<getAnswer(n,bag,w,v);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值