多重背包问题全做法 (P1776 宝物筛选 题解)

目录

思路分析

        1、简单方法(超时)

                ①转换成01背包

                        普通代码

                         空间优化代码

                ②直接求解

                        普通代码

                        空间优化代码

        2、优化 (AC)

                2、二进制拆分优化

                        代码

                3、单调队列优化

                        代码


题目描述

终于,破解了千年的难题。小 FF 找到了王室的宝物室,里面堆满了无数价值连城的宝物。

这下小 FF 可发财了,嘎嘎。但是这里的宝物实在是太多了,小 FF 的采集车似乎装不下那么多宝物。看来小 FF 只能含泪舍弃其中的一部分宝物了。

小 FF 对洞穴里的宝物进行了整理,他发现每样宝物都有一件或者多件。他粗略估算了下每样宝物的价值,之后开始了宝物筛选工作:小 FF 有一个最大载重为 W 的采集车,洞穴里总共有 n 种宝物,每种宝物的价值为 vi​,重量为 wi​,每种宝物有 mi​ 件。小 FF 希望在采集车不超载的前提下,选择一些宝物装进采集车,使得它们的价值和最大。

输入格式

第一行为一个整数 n 和 W,分别表示宝物种数和采集车的最大载重。

接下来 n 行每行三个整数 vi​,wi​,mi​。

输出格式

输出仅一个整数,表示在采集车不超载的情况下收集的宝物的最大价值。

输入输出样例

输入 #1

4 20
3 9 3
5 9 1
9 4 2
8 1 3

输出 #1

47

说明/提示

对于 30% 的数据,n≤∑mi​≤10^4,0≤W≤10^3。

对于 100% 的数据,n≤∑mi​≤10^5,0≤W≤4×10^4,1≤n≤100。

思路分析

        我们可以发现这道题即有点像01背包,又有点像完全背包。之前01背包是一个物品只能取一次,现在可以最多取 m[i] 次。所以,我们很可能用动态规划去做。而这种题目叫做多重背包问题。

        1、简单方法(超时)

                下面写出两种思路。

                ①转换成01背包

                        一个简单的思路就是想办法把多重背包问题转化为01背包问题。而这两个问题的区别只在于01背包一个数只能取一次,多重背包能取 m[i] 次。那么我们就可以把一种最多能用 m[i] 次的物品,拆分成 m[i] 种能取一次的物品,这样的话,就又回到01背包问题了。

                        我们对于第 i 种物品,在里面加一层循环,进行 m[i] 次,最里面还是正常循环背包容量。

                        普通代码
#include<bits/stdc++.h>
using namespace std;
int f[100010];
int v[100010],w[100010],m[100010];
int main()
{
	int n,W;
	cin>>n>>W;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i]>>m[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int k=0;k<m[i];k++)
		{
			for(int j=W;j>=w[i];j--)
			{
				f[j]=max(f[j],f[j-w[i]]+v[i]);
			}
		}
	}
	cout<<f[W];
	return 0;
}

                        我们还可以把数组去掉, v , w , m 都在循环里面定义。

                         空间优化代码
#include<bits/stdc++.h>
using namespace std;
int f[100010];
int main()
{
	int n,W;
	cin>>n>>W;
	for(int i=1;i<=n;i++)
	{
		int v,w,m;
		cin>>v>>w>>m;
		for(int k=0;k<m;k++)
		{
			for(int j=W;j>=w;j--)
			{
				f[j]=max(f[j],f[j-w]+v);
			}
		}
	}
	cout<<f[W];
	return 0;
}

                        我们分析一下时间复杂度,因为每种物品被分成了 m[i] 种物品,所以总物品数量是 \sum_{n}^{i=1}m_{i}  ,总时间复杂度是 O(W\sum_{n}^{i=1}m_{i}) 。这样的时间复杂度按照本题的数据范围会超时。

                ②直接求解

                        我们知道,01背包一个物品只能要或不要,计算 f[i][j] 时是:

 f[i][j] = f[i-1][j-w[i]] + v[i]

                        如果一个物品能取 2 次呢? 计算 f[i][j] 时就是:

 f[i][j] = f[i-1][j-2*w[i]]+2*v[i]

                        同理,我们从取 3 次、取 4 次…一直到取 m[i] 次,这些值,最终再取一个最大的就是 f[i][j] 了。还有一个条件,就是背包容量 j 够取当前的物品 m[i] 次。如果不够的话,背包容量除以物品的重量的商,就是最大能取当前物品的次数。

                        普通代码

 

#include<bits/stdc++.h>
using namespace std;
int f[100010];
int v[100010],w[100010],m[100010];
int main()
{
	int n,W;
	cin>>n>>W;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>w[i]>>m[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=W;j>=w[i];j--)
		{
			for(int k=1;k<=m[i]&&k*w[i]<=j;k++)
			{
				f[j]=max(f[j],f[j-k*w[i]]+k*v[i]);
			}
		}
	}
	cout<<f[W];
	return 0;
}

                        同理,这个代码也有空间优化的版本。 

                        空间优化代码
#include<bits/stdc++.h>
using namespace std;
int f[100010];
int main()
{
	int n,W;
	cin>>n>>W;
	for(int i=1;i<=n;i++)
	{
		int v,w,m;
		cin>>v>>w>>m;
		for(int j=W;j>=w;j--)
		{
			for(int k=1;k<=m&&k*w<=j;k++)
			{
				f[j]=max(f[j],f[j-k*w]+k*v);
			}
		}
	}
	cout<<f[W];
	return 0;
}

                        以后的代码就只用空间优化的代码了。 

        2、优化 (AC)

                2、二进制拆分优化

                        但是,上面我们讨论的两种思路都会超时,可以看到,这两个代码这是循环的顺序交换了,时间复杂度并没有太大的区别。

                        我们回来看看把多重背包转化成01背包的解法。代码中有三层循环,我们现在想想怎么优化。我们在代码中枚举了 m[i] 个数进行 dp ,如果我们不需要枚举 m[i] 次,是不是复杂度就降低了一些?有没有方法,使我们不用枚举 m[i] 个数,就能表示 m[i] 个数呢?

                        当然有!

                        其实,我们可以用二进制数去表示十进制数。

                        假设我们有三位二进制数,那么我们能表示的数的范围就是: 000 ~ 111 。上面的三位二进制数组成了它。也就是:

                        001——第一位是二进制数。

                        010——第二位是二进制数。

                        100——第三位是二进制数。

                        我们可以看出, 000 ~ 111 中的任何数,其实就是第几位要么是 0 ,要么是 1 。如果是 0 ,要想变成另外一个数,只需要在这一位加上 1 。也就是:

                        101 = 100 + 001;

                        110 = 100 + 010;

                        111 = 100 + 010 + 001;

                        那么我们能看出什么呢?我们可以用 001 、 010 、 100 来表示 000 ~ 111 中所有的数。也就是我们用 1 、 2 、 4 就能表示出 0 ~ 7 以内的任何一个数。

                        那么同理,我们用 2^{0} 、 2^{1} 、 … 2^{k} 就可以表示出 02^{(k+1)}-1 之内的所有数。

                        如果要表示的数超过了 2^{(k+1)}-1 呢?

                        比如我要表示 300 ,而我只有 2^{0} 、 2^{1} 、 … 2^{7} ,这些数能表示 2^{8}-1 ,也就是  255 ,那 256 ~ 300 怎么表示呢?

                        我们只用加一个 45 就可以了。

                        为什么?因为我们只用前面的数的话,只能表示 0 ~ 255 ,我们如果每次都加上一个 45 ,能表示 45 ~ 300 。虽然稍微有点重叠,但是也就多了几次运算,我们的答案还是对的。

                        所以我们发现,我们可以用 log_{2}m_{i} 个数就能组合出 0 到 m[i] 的情况。也就是说,我们可以把时间复杂度从 O(W\sum_{n}^{i=1}m_{i}) 优化到了 O(W\sum_{n}^{i=1}log_{2}m_{i}) 。

                        也就是从图1:

转化成了图2:

 

                        代码
#include<bits/stdc++.h>
using namespace std;
int f[100010];
int main()
{
	int n,W;
	cin>>n>>W;
	for(int i=1;i<=n;i++)
	{
		int v,w,m;
		cin>>v>>w>>m;
		for(int k=1;k<=m;k*=2)
		{
			for(int j=W;j>=k*w;j--)
			{
				f[j]=max(f[j],f[j-k*w]+k*v);
			}
			m-=k;
		}
		for(int j=W;j>=m*w;j--)
		{
			f[j]=max(f[j],f[j-m*w]+m*v);
		}
	}
	cout<<f[W];
	return 0;
}

                3、单调队列优化

                        最优的解法是利用单调队列去优化,可以把时间复杂度优化到 O(nW) 。

                        我们看看状态转移方程:

 f[j] = max { f[j-k*w] + k*v }

                        它的外层循环是 j ,内层循环是 k ,我们观察 j-k*w 的变化情况。先对比 j 和 j+1 , k 从 1 递增, 它们的 j-k*w[i] 如下:

                j :        j-3*w[i]        j-2*w[i]        j-w[i]

              j+1:        j+1-3*w[i]        j+1-2*w[i]        j+1-w[i]

                        可以看出, k 的滑动窗口并没有重叠。但是:

                j :        j-3*w[i]        j-2*w[i]        j-w[i]

              j+w[i]:        j+w[i]-4*w[i]        j+w[i]-3*w[i]        j+w[i]-2*w[i]

                        我们发现有重叠。那么我们可以推理出,当 j = j 、 j + w[i] 、 j + 2*w[i] … 时有重叠;进一步推理出,当 j 除以 w[i] 的余数相等时,会发生重叠。那么我们把 j 循环改为按 j 除以 w[i] 的余数相等的值进行循环,就能利用单调队列优化了。

                        我们现在把原状态转移方程变成应用单调队列的方程:

        让 j = x + y*w[i] ,其中 x = j % w[i] , x 为 j 除以 w[i] 得到的余数。 y = j / w[i] , y 为 j 整除 w[i] 的结果。把 j 带入原方程,得:

 f[x + y * w[i]] = max ( f[x + ( y - k ) * w[i]] + k * w[i] )

                        代码
#include<bits/stdc++.h>
using namespace std;
int f[100010];
int q[100010];
int num[100010];
int main()
{
	int n,W;
	cin>>n>>W;
	for(int i=1;i<=n;i++)
	{
		int v,w,m;
		cin>>v>>w>>m;
		if(m>W/w)
		{
			m=W/w;
		}
		for(int b=0;b<w;b++)
		{
			int head=1,tail=1;
			for(int y=0;y<=(W-b)/w;y++)
			{
				int tmp=f[b+y*w]-y*v;
				while(head<tail&&q[tail-1]<=tmp)
				{
					tail--;
				}
				q[tail]=tmp;
				num[tail++]=y;
				while(head<tail&&y-num[head]>m)
				{
					head++;
				}
				f[b+y*w]=max(f[b+y*w],q[head]+y*v);
			}
		}
	}
	cout<<f[W];
	return 0;
}
  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值