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

前言

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

01背包

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

knapsack

这是最原始的01背包问题(即每个物品只能选【算法笔记】三种背包问题——背包 DP【算法笔记】三种背包问题——背包 DP次)。下面我们来看如何求解。
【算法笔记】三种背包问题——背包 DP表示只考虑前【算法笔记】三种背包问题——背包 DP个物品的情况下,容量为【算法笔记】三种背包问题——背包 DP的背包所能装的最大总价值。则最终答案为【算法笔记】三种背包问题——背包 DP,状态转移方程为:
【算法笔记】三种背包问题——背包 DP
依次递增【算法笔记】三种背包问题——背包 DP,逐步增加问题规模即可求解。时间、空间复杂度均为【算法笔记】三种背包问题——背包 DP
在本题中,【算法笔记】三种背包问题——背包 DP的空间复杂度容易MLE,因此考虑使用数组重复利用或者滚动表的优化。

压缩掉【算法笔记】三种背包问题——背包 DP的第一维,变成:
【算法笔记】三种背包问题——背包 DP
此时空间复杂度为【算法笔记】三种背包问题——背包 DP
一定要牢记这个公式,注意使用时需倒序枚举【算法笔记】三种背包问题——背包 DP,防止串连转移。参考代码:

#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背包还有一种简单变形,即求最小剩余空间,此时用【算法笔记】三种背包问题——背包 DP总空间【算法笔记】三种背包问题——背包 DP最大可装空间【算法笔记】三种背包问题——背包 DP即可。

#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;
}

扩展:对付更大的【算法笔记】三种背包问题——背包 DP

AtCoder Educational DP Contest E – Knapsack 2
本题和普通的01背包完全相同,只是数据范围改为【算法笔记】三种背包问题——背包 DP

注意数据范围,【算法笔记】三种背包问题——背包 DP意味着只要开这么大的数组都会MLE,因此我们考虑修改dp状态。前看原来的dp状态,本质上就是“确定重量,求最大价值”,现在我们反过来,即“确定价值,求最小重量”。令【算法笔记】三种背包问题——背包 DP表示只用前【算法笔记】三种背包问题——背包 DP个物品,达到总价值为【算法笔记】三种背包问题——背包 DP所需的最小空间。由于【算法笔记】三种背包问题——背包 DP,所以【算法笔记】三种背包问题——背包 DP,极限情况下dp数组只需要开【算法笔记】三种背包问题——背包 DP即可,相对而言会好很多。下面考虑dp状态转移方程:
【算法笔记】三种背包问题——背包 DP
其中,【算法笔记】三种背包问题——背包 DP这种情况不存在,所以初始值为【算法笔记】三种背包问题——背包 DP。最终答案,即为最大的【算法笔记】三种背包问题——背包 DP,使得【算法笔记】三种背包问题——背包 DP,更新状态时可同时记录这个答案。

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

  • 时间:对于每个【算法笔记】三种背包问题——背包 DP,循环迭代【算法笔记】三种背包问题——背包 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 疯狂的采药
【算法笔记】三种背包问题——背包 DP个物品和一个总容量为【算法笔记】三种背包问题——背包 DP的背包。第【算法笔记】三种背包问题——背包 DP件物品的重量是【算法笔记】三种背包问题——背包 DP,价值是【算法笔记】三种背包问题——背包 DP每个物品可以使用无限次。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

这种背包与01背包唯一的不同之处在于,每个物品使用次数不限,所以参考01背包的dp状态:
【算法笔记】三种背包问题——背包 DP表示只考虑前【算法笔记】三种背包问题——背包 DP个物品的情况下,容量为【算法笔记】三种背包问题——背包 DP的背包所能装的最大总价值,则有:
【算法笔记】三种背包问题——背包 DP
可以发现,实际上只需要用【算法笔记】三种背包问题——背包 DP即可,因为此时的【算法笔记】三种背包问题——背包 DP会被【算法笔记】三种背包问题——背包 DP更新,【算法笔记】三种背包问题——背包 DP又会被【算法笔记】三种背包问题——背包 DP更新,以此类推,这样算与前面的公式等效。

对比一下01背包和完全背包的状态转移方程:
【算法笔记】三种背包问题——背包 DP
实际上,区别就在于一个是【算法笔记】三种背包问题——背包 DP,一个是【算法笔记】三种背包问题——背包 DP。所以仍可以使用数组压缩,只需要改变一下循环顺序,是不是很神奇?
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 宝物筛选
【算法笔记】三种背包问题——背包 DP个物品和一个总容量为【算法笔记】三种背包问题——背包 DP的背包。第【算法笔记】三种背包问题——背包 DP件物品的重量是【算法笔记】三种背包问题——背包 DP,价值是【算法笔记】三种背包问题——背包 DP最多能选择【算法笔记】三种背包问题——背包 DP。我们要选择一些物品,使这些物品的重量总和不超过背包容量,且价值总和最大。【算法笔记】三种背包问题——背包 DP

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

  • 【算法笔记】三种背包问题——背包 DP,其中【算法笔记】三种背包问题——背包 DP
  • 【算法笔记】三种背包问题——背包 DP,其中【算法笔记】三种背包问题——背包 DP
  • 【算法笔记】三种背包问题——背包 DP,其中【算法笔记】三种背包问题——背包 DP

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

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

#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
  • 对于完全背包,可选数目【算法笔记】三种背包问题——背包 DP

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

总结

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

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

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


  1. 压缩掉第一维【算法笔记】三种背包问题——背包 DP,01背包为倒序枚举【算法笔记】三种背包问题——背包 DP,完全背包为正序 ↩︎

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

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

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

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

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2024年4月16日
下一篇 2024年4月16日

相关推荐