目录
题目描述
终于,破解了千年的难题。小 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] 种物品,所以总物品数量是 ,总时间复杂度是
。这样的时间复杂度按照本题的数据范围会超时。
②直接求解
我们知道,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 以内的任何一个数。
那么同理,我们用 、
、 …
就可以表示出
~
之内的所有数。
如果要表示的数超过了 呢?
比如我要表示 300 ,而我只有 、
、 …
,这些数能表示
,也就是 255 ,那 256 ~ 300 怎么表示呢?
我们只用加一个 45 就可以了。
为什么?因为我们只用前面的数的话,只能表示 0 ~ 255 ,我们如果每次都加上一个 45 ,能表示 45 ~ 300 。虽然稍微有点重叠,但是也就多了几次运算,我们的答案还是对的。
所以我们发现,我们可以用 个数就能组合出 0 到 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、单调队列优化
最优的解法是利用单调队列去优化,可以把时间复杂度优化到 。
我们看看状态转移方程:
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;
}