背包题型总结

概述

大致分为以下几类:

  1. 01背包
  2. 完全背包
  3. 多重背包
  4. 混合背包
  5. 二维背包
  6. 分组背包
  7. 依赖背包(树形背包)

以及一个变式:跳楼梯模型,本质是转移顺序的改变。

背包题基本上没有套路,只能靠多练题,总结思路,最重要的还是把问题抽象成背包模型的能力。

01 背包

特点:无序加入,每个物品加一次。

完全背包

特点:无序加入,每个物品无限加。

变式:跳楼梯模型:问跳完一段楼梯有多少种不同的方案数

这两者的区别就在于:

  1. 跳楼梯模型只有求方案数时才可以使用,而完全背包既可以求方案数,也可以求最优解。(完全背包除了求方案数以外,求最优解其实可以使用跳楼梯模型,但是这种写法不普遍也不推荐。)
  2. 跳楼梯模型强调有序 ,即要考虑谁先放谁后放;而完全背包强调无序,两个物品先放后放不会影响最终答案。
  3. 转移顺序不同。

第三点的区别最主要体现在代码上:

完全背包是外层先循环物品种类,然后循环背包容量。这样就可以通过人为地定一个放的顺序来达到无序的效果。
跳楼梯模型是外层先循环背包容量,然后循环物体种类。这样就可以使后放的情况包含先放的情况。
具体区别可以看这两个题,很明显:

跳楼梯模型:纸币问题2
完全背包:纸币问题3

多重背包

特点:无序加入,一个物体有多个。

重点在于其优化方式以及看出题目中各类物品对应题目中的什么东西。

朴素做法时间为 \(O(nm \sum c)\)

优化方式

二进制优化

约定:\(n\) 代表物体种类,\(m\) 代表背包容量,\(c\) 代表物体数量。

正确的原因(本质):任何数都可以用二进制表示出来,因此我们挑选出二的整数次幂数量的物体,依靠他们组合便能组合出所有数量物体。

实现:把一个物体按上述方式拆分成多个物体,进行 01 背包。

细节:一个物体,从 \(1\) 倍枚举到 最后一个 \(i\) 使物体最大数量 $\ge 2^i $ 的倍即可。(不能超过物体最大数量。)

时间复杂度 \(O(nm \sum \log c)\)

单调队列优化

约定:\(c\) 代表物体数量,\(w\) 代表物体价值,\(v\) 代表物体体积,\(f\) 代表 dp 数组,\(m\) 代表背包容量。

正确的原因(本质):观察到

\[f[i][j]=max(f[i-1][j-c[i]]+w[i],f[i-1][j-2*c[i]]+2*w[i],f[i-1][j-3*c[i]]+3*w[i],...) \]

我们发现,第二维的数在模 \(c[i]\) 的意义下始终相等。也就是一个模 \(c[i]\) 剩余系。
并且 \([0,m]\) 中的所有数都可以分在几个模 \(c[i]\) 的剩余系中。

那么我们对于每一个剩余系,做一个单调队列,来维护 \(f[i-1][j-k*c[i]]+k*w[i]\) 的最大值,不在最大数量之内的就弹出,每次通过队头转移之后插入这个数。注意插入单调队列时要加上本次放入物品的价值,又因为单调队列每次往后移动一位他都会统一加上一个值,所以它依然是单调的。

需要注意的是:这里我们要做一个 dp 数组的备份,因为我们是顺序转移的,无法保证这次的转移一定是转上一次的,因此我们要先把上一次的 dp 数组存下来。

具体实现:第一维枚举每个数,第二维枚举模 \(c[i]\) 的余数,第三维枚举这个模 \(c[i]\) 剩余系中的各个数,扫一遍,进行状态转移。注意每次要先弹出不合法的,然后转移,最后再把这一位压入单调队列。

时间复杂度分析:第一维枚举 \(n\) 个数,第二维枚举 \(c\) 个数,第三位枚举 \(m/c\) 个数,相乘得到 \(n*c*m/c=n*m\),因此时间复杂度为 \(O(nm)\)

板子题代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll f[40005],n,v,w,m,s,q[100005],h,t,g[40005];
int main()
{
	cin>>n>>s;
	while(n--)
	{
		cin>>v>>w>>m;//v:价格 , w:重量 , m:数量 
		for(int i=0;i<=s;i++)
		{
			g[i]=f[i];
		}
		for(int i=0;i<w;i++)
		{
			h=0,t=-1;
			for(int j=i;j<=s;j+=w)
			{
				while((t-h+1)&&q[h]<j-m*w)h++;
				if((t-h+1))f[j]=max(g[j],g[q[h]]+(j-q[h])/w*v);
				while((t-h+1)&&(g[j]>=g[q[t]]+(j-q[t])/w*v))t--;//要加上这次放入的价值再判断弹出队尾
				q[++t]=j;
			}
		}
	}
	cout<<f[s];
	return 0;
}

依赖背包(树形背包)

特殊情况的做法:规定树只有两层

穷举:金明的预算方案

对于只有两层,且儿子节点的数量极少的情况,我们可以将一个树内的选法穷举出来,做分组背包。
时间是 \(O(2^knV)\) ,本题 \(k=2\) 可以通过。

一层分组

对于主件和附件依赖的情况,我们对依赖某个特定主件的所有附件做一个 01 背包,算出该子树内 最大体积为 \(V\) 时最大的价值是多少,接下来把每种体积所得价值各自加上主件和为一个新的物品,这些物品再和为分组背包中的一个组即可。

时间 \(O(nV^2)\),无法通过 金明的预算方案 一题。

树形背包通解

有依赖的背包问题

观察上面 “一层分组” 的做法,很容易想到,我们对树中的每个子树都做一次这样的操作就好了。

时间为 \(O(nV^2)\)

posted @ 2024-07-09 16:57  KS_Fszha  阅读(10)  评论(0编辑  收藏  举报