#2020寒假集训#背包入门(Knapsack)代码笔记

前景引入

有 N 件物品和一个容量为 V 的背包
放入第 i 件物品耗费的费用是 1 ,得到的价值是 Wi 。每个物品最多可放入一次,求最大的价值总和

  • 无需背包思维,只要排序取前V个即可

01背包(每种物品仅有一件,可以选择放或不放)

有 N 件物品和一个容量为 V 的背包
放入第 i 件物品耗费的费用是 Ci ,得到的价值是 Wi 。一件物品最多放入一次,求最大的价值总和

不可取:性价比思维

n=3 V=3
W1=2 C1=1
W2=3 C2=1
W3=3 C3=2
W1+W2=5 < W2+W3=6

  • 这种情况下,性价比3>2>1.5
  • 当放入前两者后,虽然背包未放满,但也放不下第三者,只能舍弃
  • 如此得到的价值之和,倒不如放第二者和第三者更大些
  • 所以性价比思维的局限在于价值大+耗费多+背包未满仍放不下的情况
正解:动态规划
  • dp[i][j] 表示前 i 个物品放入容量为 j 的最大价值(放入i后的容量为j)
  • 不放第i个物品:dp[i-1][j]
  • 放入第i个物品:dp[i-1][j-cost[i]]+value[i](处理好第i-1个物品后的最大价值+第i个的价值)
  • 状态转移方程:dp[i][j] = max(dp[i-1][j],dp[i-1][j-cost[i]]+value[i])
  • 时空复杂度都是O(nV)
空间优化
  • dp[j] 表示放入容量为j的最大价值
  • 不放第i个物品:dp[j]
  • 放入第i个物品:dp[j-cost[i]]+value[i]
  • 状态转移方程:dp[j] = max(dp[j] , dp[j-cost[i]] + value[i])
  • 时间复杂度O(nV)空间复杂度O(V)
小贴士
  • 空间优化后,遍历到i,就表示处理第i个物品,从而优化二维数组变为一维数组
  • 内层循环逆序,以防大体积用到的小体积dp优化过并选择放入的情况,从而一个物品被多次放入
  • 例如:第1和2个物品各体积均已放好,处理第3个物品时,若从小体积开始
  • 假设第3个物品体积为5,容器体积为5时已能放下第3个物品
  • 遍历到容器体积为10时,调用了容器体积为5的结果,第3个物品再次被放入
  • 但01背包中每个物品只有一个,最多只能被放入一次
代码模板
int knapsack_01()//01背包 
{
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;++i)
	{//遍历到i,就表示遍历到第i个物品,优化二维数组变为一位数组 
		for(int j=v;j>=cost[i];--j)//从大到小遍历!!!
		/*
			以防同一个物品从小到大的话
			先把小体积优化好,再优化大体积
			那么大体积用的小体积dp可能是放这个物品优化过的
			但01背包,每个物品只有一个,不可重复放 
		*/ 
		{
			if(cost[i]<=j) dp[j]=max(dp[j],dp[j-cost[i]]+value[i]);
		}
	}
	return dp[v];
/*
	二维数组code写法 
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;++i)
	{
		for(int j=0;j<=v;++j)
		{
			if(j<c[i]) dp[i][j]=dp[i-1][j];
			else dp[i][j]=max(dp[i-1][j],dp[i-1][j-cost[i]]+value[i]);
		}
	}
	return dp[n][v];
*/
}

多重背包(每件物品可放置指定的多次,非无限次)

基础思维:转化为简单01背包
  • 要求:一件价值Wi,大小Ci的可放置Mi次的物品
  • 转化为:Mi件价值Wi,大小Ci的只可取一次的物品
  • 时间复杂度:O(VΣMi)
高级思维:转化为01背包+二进制优化
  • 把Mi个拆成 20-21-22-23…2(k-1)-(Mi-2k-1)个物品
  • Mi=29按照1 2 4 8 14拆分
  • 时间复杂度:O(VΣlogMi)
  • 比如5张5块钱的纸币,可以拆分为
  • 20个5元(1个5元)+21个5元(1个10元)+(5-22)个5元(1个15元)
  • 如果是求最少用几张纸币凑出指定金额,即应当记录20 21 (5-22)(实际耗费的纸币量)
代码模板
int knapsack_multiple()//多重背包 
{
	int n1=0,m[maxn];//实际m数组应当在main函数内输入完毕 
	for(int i=1;i<=n;++i)
	{
		for(int j=1;m[i];j*=2)//二进制优化拆数1 2 4 8 16 32等等 
		{
			j=min(j,m[i]);
			/*
				比如29拆成1 2 4 8 14,此步骤处理类似剩余14,不足16的情况
				即当前的j与剩余的总数量取最小值
				全部取完的话m[i]==0为假,循环就退出了 
				m[i]是输入的i物品有几个 
			*/ 
			m[i]-=j;
			n1++;
			cost1[n1]=cost1[i]*j;
			value1[n1]=value1[i]*j;
		}
	}
	for(int i=1;i<=n1;++i)
	{
		for(int j=v;j>=cost1[i];--j)
		{
			dp[j]=max(dp[j],dp[j-cost1[i]]+value1[i]);
		}
	}
	return dp[v];
}

完全背包(每件物品可放置无限次)

有 N 件物品和一个容量为 V 的背包。
放入第 i 件物品耗费的费用是 Ci ,得到的价值是 Wi

基础思维:转化为多重背包
  • 转化为: Mi=V/Ci 的多重背包
  • 时间复杂度:O(VΣV/Ci)
巧妙思维:类似于01背包
  • 还记得01背包的时候我们要求内层循环逆序
  • 那时的目的就是防止一件物品被放入多次
  • 但是完全背包就是需要可放置无限次,这个防止的操作就可以省去啦
  • 于是我们的完全背包就是将01背包的内层循环改为正序
代码模板
int knapsack_full()//完全背包 
{
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;++i)
	{
		for(int j=cost[i];j<=v;++j)
		{
			dp[j]=max(dp[j],dp[j-cost[i]]+value[i]);
		}
	}
	return dp[v];
}

分组背包(每组中的物品互相冲突,最多选一件)

有 N 件物品和一个容量为 V 的背包
第 i 件物品的费用是 Ci,价值是 Wi
这些物品被划分为 K 组,每组中的物品互相冲突,最多选一件
求最大的价值总和

关键思维:把每一组看成一个大的01物品(每组只能挑一个组员)
  • 三层循环嵌套
  • 层:组数(序)
  • 层:容量(逆序-类似于01背包)
  • 层:组内成员(序)
代码模板
int knapsack_divide()//分组背包 
{
	memset(dp,0,sizeof(dp));
	//如n门课有m天,花k天数学第i门课的价值是Value[i][k],要求价值最大 
	for(int i=1;i<=n;i++)//第i组,即每门课1组,组内成员是天数
	{
		for(int j=v;j>=1;j--)//背包容量j,即容量是天数 
		{
			for(int k=1;k<=j;k++)//这组中第k个,选几天 
			{
				dp[j]=max(dp[j],dp[j-k]+Value[i][k]);
			}
		}
	}
	return dp[v];
}

混合背包

- 有的物品只能放一次,有的物品能放Mi次,有的物品能放无限次(调用多个函数即可)

补充

memset(dp,0,sizeof(dp));//背包可以不装满
memset(dp,-inf,sizeof(dp));//背包必须被装满(memset后勿忘dp[0]=0;)
  • 当背包可以不装满的时候,如果dp[j-cost[i]]没装满还是可以装的
  • 但当背包必须被装满的时候,如果dp[j-cost[i]]若是没装满则不可能比装满的大
  • 就比如体积为5的袋子,装体积为2的东西,之前没装过体积为3的东西
  • 这个时候dp[5]只是-inf+value[i]
  • 但如果与装过体积为3的东西的dp[5]比较,那么装过的值一定更大,自然取它
  • 勿忘memset后还要初始化dp[0]=0,不然无论如何都没装满哈哈哈
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值