多重背包
定义:有一个固定容量的背包,并且有若干种物品,每种物品有
n
i
n_i
ni个,每种物品所占的空间是
v
i
v_i
vi,价值为
w
i
w_i
wi,问如何取可以让背包内的物品的总价值最大。
朴素的多重背包dp
f
[
i
]
[
j
]
f[i][j]
f[i][j]的定义:表示从前
i
i
i种物品中选,背包容量为
j
j
j的情况下,所有选法的最大值。
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
]
,
f
[
i
−
1
]
[
j
−
k
×
v
[
i
]
]
+
k
×
w
[
i
]
)
;
f[i][j]=max(f[i][j],f[i-1][j-k\times v[i]]+k\times w[i]);
f[i][j]=max(f[i][j],f[i−1][j−k×v[i]]+k×w[i]);这个状态转移方程的含义就是选
k
k
k个第
i
i
i种物品的情况下,从前
i
−
1
i-1
i−1个物品中选可以得到的最大价值
朴素的多重背包dp就是进行三重循环,第一重循环是枚举放入的第
i
i
i个物品,第二重循环是枚举背包的容量,第三重循环就是枚举第
i
i
i个物品放入的个数。
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
但是这样的时间复杂度
为
O
(
n
×
m
×
c
)
为O(n \times m \times c)
为O(n×m×c)过高了,所以就要想办法进行优化。
二进制优化
二进制优化就是将所有种类的物品按照二进制进行分组,因为所有的十进制数都可以由若干个二进制的数组成,这样就可以用01背包的方法进行求解了
时间复杂度降低为
O
(
n
×
w
×
l
o
g
c
)
O(n\times w\times logc)
O(n×w×logc),有一定程度的优化
//二进制优化 (每若干个分组)//可以等价为01背包 所以就直接写一维
int cnt=0;//记录编号
for(int i=1;i<=n;i++)
{
int a,b,s;
cin>>a>>b>>s;// a体积 b价值 s数量
int k=1;//二进制分组
while(k<=s)
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0)//说明最后还剩下一些不足2的k+1次方的数 直接单独存起来
{
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
n=cnt;//代表每一个物品都按照2进制分类完后等价的物品总数
//后面直接按照01背包的解法就可以
for(int i=1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout<<f[m];
优先队列优化
前置芝士:用优先队列求滑动窗口的最大值。(见我的另一篇文章 优先队列.)
- f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v ] + w , f [ i − 1 ] [ j − 2 v ] + 2 w , . . . . + f [ i − 1 ] [ j − s v ] + s w ) f[i][j]=max(f[i-1][j],f[i-1][j-v]+w, f[i-1][j-2v]+2w,....+f[i-1][j-sv]+sw) f[i][j]=max(f[i−1][j],f[i−1][j−v]+w,f[i−1][j−2v]+2w,....+f[i−1][j−sv]+sw)
- f [ i ] [ j − v ] = m a x ( f [ i − 1 ] [ j − v ] , f [ i − 1 ] [ j − 2 v ] + w , f [ i − 1 ] [ j − 3 v ] + 2 w , . . . . + f [ i − 1 ] [ j − ( s + 1 ) v ] + s w ) f[i][j-v]=max(f[i-1][j-v],f[i-1][j-2v]+w, f[i-1][j-3v]+2w,....+f[i-1][j-(s+1)v]+sw) f[i][j−v]=max(f[i−1][j−v],f[i−1][j−2v]+w,f[i−1][j−3v]+2w,....+f[i−1][j−(s+1)v]+sw)
-
f
[
i
]
[
j
−
2
v
]
=
m
a
x
(
f
[
i
−
1
]
[
j
−
2
v
]
,
f
[
i
−
1
]
[
j
−
3
v
]
+
w
,
f
[
i
−
1
]
[
j
−
4
v
]
+
2
w
,
.
.
.
.
+
f
[
i
−
1
]
[
j
−
(
s
+
2
)
v
]
+
s
w
)
f[i][j-2v]=max(f[i-1][j-2v],f[i-1][j-3v]+w, f[i-1][j-4v]+2w,....+f[i-1][j-(s+2)v]+sw)
f[i][j−2v]=max(f[i−1][j−2v],f[i−1][j−3v]+w,f[i−1][j−4v]+2w,....+f[i−1][j−(s+2)v]+sw)
.
.
.
我们不难发现其实每一个状态的max序列都是固定长度的,并且关于v的余数都是相同的,所以我们可以将这个求解过程等价为一个长度为 s s s的滑动窗口,并且求出每一次滑动窗口右移动一位后的窗口内的最大值,这样也就可以使用优先队列来求滑动窗口内的最大值。
但是并不能直接套用,因为max序列内前一个元素比后一个元素少了一个 w w w,所以我们还要处理这个偏移量。
我们可以先将所有元素在不考虑偏移量的情况下,写在一个数轴上
如下由于在数轴上的元素前后关系与max序列中是相反的,所以前一个元素会比后一个元素多一个 w w w
我们可以将这个关系写为绝对关系,也就是在滑动窗口中的后一个元素会比前一个元素少一个
w
w
w,而与队头相比会少的
w
w
w的数量就是与队头相比可以放入的当前种物品的数量差。这样就避免了每次都更新一次滑动窗口中的相对关系。(换个方法比较雀食可以方便很多,雀食蟀)
而有n种物品就需要n个数轴进行计算。
我们可以不用二维数组进行计算,因为只用到上一种物品和当前种物品之间的关系,所以我们就可以用滚动数组进行求解。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=20010;
int n,m;
int f[N],g[N],q[N];//f表示当前的物品,g表示上一个物品,q表示队列
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
int v,w,s;
cin>>v>>w>>s;
memcpy(g,f,sizeof f);
for(int j=0;j<v;j++)//遍历余数
{
int hh=0,tt=-1;
for(int k=j;k<=m;k+=v)//遍历给第i个物品预留的空间
{
if(hh<=tt&&q[hh]<k-s*v) hh++;//如果滑出窗口了 s是该种物品可以选的个数,
if(hh<=tt) f[k]=max(f[k],g[q[hh]]+(k-q[hh])/v*w);//准备要加入队列的元素 判断加入当前物品,并且总体积为k时的最大值
//f[k]就代表f[i-1][j],而后面这段就是求的后面的那段最大值。g[q[hh]]就是取前i-1个物品体积为q[hh]时的最大值
//所以一个状态转移语句就是判断不取第i个物品与取第i个物品两种情况哪个大
//g[q[hh]]+(k-q[hh])/v*w)加上偏移量
while(hh<=tt&&g[q[tt]]-(q[tt]-j)/v*w<=g[k]-(k-j)/v*w) tt--;//如果队尾去除所有偏移量后小于要加入的元素去除所有偏移量就将队尾删除,
//因为这样新加入的一定是更优的,形成单调队列
q[++tt]=k;
}
}
}
cout<<f[m]<<endl;
return 0;
}