站点图标 AI技术聚合

【算法笔记】三种背包问题——背包 DP

前言

背包(Knapsack)问题是经典的动态规划问题,也很有实际价值。

01背包

洛谷 P2871 [USACO07DEC] Charm Bracelet S
AtCoder Educational DP Contest D – Knapsack 1
【算法笔记】三种背包问题——背包 DP个物品和一个总容量为的背包。第件物品的重量是,价值是。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

这是最原始的01背包问题(即每个物品只能选次)。下面我们来看如何求解。
表示只考虑前个物品的情况下,容量为的背包所能装的最大总价值。则最终答案为,状态转移方程为:

依次递增,逐步增加问题规模即可求解。时间、空间复杂度均为
在本题中,的空间复杂度容易MLE,因此考虑使用数组重复利用或者滚动表的优化。

压缩掉的第一维,变成:

此时空间复杂度为
一定要牢记这个公式,注意使用时需倒序枚举,防止串连转移。参考代码:

#include <cstdio>
#define setmax(x, y) if(x < y) x = y
using namespace std;

int f[12881];

int main()
{
	int n, m;
	scanf("%d%d", &n, &m);
	while(n--)
	{
		int w, v;
		scanf("%d%d", &w, &v);
		for(int i=m; i>=w; i--)
			setmax(f[i], f[i - w] + v);
	}
	printf("%d\n", f[m]);
	return 0;
}

01背包还有一种简单变形,即求最小剩余空间,此时用总空间最大可装空间即可。

#include <cstdio>
#define setmax(x, y) if(x < y) x = y
using namespace std;

int f[20005];

int main()
{
	int n, v;
	scanf("%d%d", &v, &n);
	while(n--)
	{
		int w;
		scanf("%d", &w);
		for(int i=v; i>=w; i--)
			setmax(f[i], f[i - w] + w);
	}
	printf("%d\n", v - f[v]);
	return 0;
}

扩展:对付更大的

AtCoder Educational DP Contest E – Knapsack 2
本题和普通的01背包完全相同,只是数据范围改为

注意数据范围,意味着只要开这么大的数组都会MLE,因此我们考虑修改dp状态。前看原来的dp状态,本质上就是“确定重量,求最大价值”,现在我们反过来,即“确定价值,求最小重量”。令表示只用前个物品,达到总价值为所需的最小空间。由于,所以,极限情况下dp数组只需要开即可,相对而言会好很多。下面考虑dp状态转移方程:

其中,这种情况不存在,所以初始值为。最终答案,即为最大的,使得,更新状态时可同时记录这个答案。

这种算法的时间和空间都可以优化:

  • 时间:对于每个,循环迭代时只需到即可,因为当前的总价值不可能超过这个值;
  • 空间:用与前面完全相同的方法,压缩掉第一维空间,变成(注意要倒序枚举

运用了两种优化的代码如下:

#include <cstdio>
#include <cstring>
using namespace std;

long long f[100005], t, tot, ans;

int main()
{
	int n, sz;
	scanf("%d%d", &n, &sz);
	memset(f, 0x3f, sizeof f);
	f[0] = 0;
	while(n--)
	{
		int w, v;
		scanf("%d%d", &w, &v);
		tot += v;
		for(int i=tot; i>=v; i--)
			if((t = f[i - v] + w) < f[i] && t <= sz)
			{
				f[i] = t;
				if(i > ans) ans = i;
			}
	}
	printf("%lld\n", ans);
	return 0;
}

完全背包

洛谷 P1616 疯狂的采药
个物品和一个总容量为的背包。第件物品的重量是,价值是每个物品可以使用无限次。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

这种背包与01背包唯一的不同之处在于,每个物品使用次数不限,所以参考01背包的dp状态:
表示只考虑前个物品的情况下,容量为的背包所能装的最大总价值,则有:

可以发现,实际上只需要用即可,因为此时的会被更新,又会被更新,以此类推,这样算与前面的公式等效。

对比一下01背包和完全背包的状态转移方程:

实际上,区别就在于一个是,一个是。所以仍可以使用数组压缩,只需要改变一下循环顺序,是不是很神奇?
long long别忘了~

#include <cstdio>
#define setmax(x, y) if(x < y) x = y
using namespace std;

long long f[10000005];

int main()
{
	int sz, n;
	scanf("%d%d", &sz, &n);
	while(n--)
	{
		int w, v;
		scanf("%d%d", &w, &v);
		for(int i=w; i<=sz; i++)
			setmax(f[i], f[i - w] + v);
	}
	printf("%lld\n", f[sz]);
	return 0;
}

多重背包

洛谷 P1776 宝物筛选
个物品和一个总容量为的背包。第件物品的重量是,价值是最多能选择。我们要选择一些物品,使这些物品的重量总和不超过背包容量,且价值总和最大。

很容易想到,可以转换成每件物品都被拆分成个只能选一次的物品,即的01背包。很明显,这样做的时间复杂度是,会TLE。因此,我们可以优化拆分的方法,将转化为的形式,举几个栗子:

  • ,其中
  • ,其中
  • ,其中

这种方法的正确性这里就不详细说明了,主要依赖于二进制的拼凑。容易发现,数字按这种拆分的方法会被拆分为个数字的和,因此总时间复杂度为,可以通过此题。

还有一种单调队列/单调栈优化,同样针对多重背包问题,时间复杂度为有时不一定优于二进制优化,这里就不多说了。下面给出二进制优化的参考程序。

#include <cstdio>
#define maxw 40004
#define setmax(x, y) if(x < y) x = y
using namespace std;

int n, w, f[maxw];

inline void add(int a, int b) // a: value, b: weight
{
	for(int i=w; i>=b; i--)
		setmax(f[i], f[i - b] + a);
}

int main()
{
	scanf("%d%d", &n, &w);
	while(n--)
	{
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		for(int i=0; (1<<i)<=c; i++)
			add(a << i, b << i), c -= 1 << i;
		if(c) add(a * c, b * c);
	}
	printf("%d\n", f[w]);
	return 0;
}

混合背包

没错,就是前三种放在一起的背包。不用的物品有不同的种类。比如洛谷 P1833 樱花,就是混合背包。不过,也不要慌,直接用个分支判断,比如:

for (循环物品种类) {
  if (是 0 - 1 背包)
    套用 0 - 1 背包代码;
  else if (是完全背包)
    套用完全背包代码;
  else if (是多重背包)
    套用多重背包代码;
}

实际上也不一定要这样,可以全部统一成混合背包:

  • 对于01背包,可选数目
  • 对于完全背包,可选数目

这里就不给出详细代码,有兴趣的读者可以自己尝试一下。

总结

让我们来总结一下三种基本背包DP的异同:

项目 01背包 完全背包 多重背包
适用场景 每件物品只能选择一次 每件物品可以无限选择 每件物品可以选择的次数有限
状态转移方程1 基本同01背包
时间复杂度2
空间复杂度3
编码难度

创作不易,如果觉得好就请给个三连,谢谢支持!


  1. 压缩掉第一维,01背包为倒序枚举,完全背包为正序 ↩︎

  2. 完全背包为优化后的复杂度,多重背包为二进制优化的复杂度。 ↩︎

  3. 指压缩第一维后的dp数组大小。 ↩︎

版权声明:本文为博主作者:GoodCoder666原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/write_1m_lines/article/details/126385779

退出移动版