单调队列优化多重背包
Problem 多重背包问题 III
有\(N\)种物品和一个容量是\(V\)的背包。
第\(i\)种物品最多有\(s_i\)件,每件体积是\(w_i\),价值是\(v_i\)。
输出最大价值。
\(0<N≤1000\)
\(0<V≤20000\)
\(0<v_i,w_i,s_i≤20000\)
Solution
容易想到暴力多重背包的做法
#include<bits/stdc++.h>
using namespace std;
const int N=1003,M=20009;
int n,V,w[N],v[N],cnt[N],dp[N][M];
int main(){
scanf("%d%d",&n,&V);
for(int i=1;i<=n;i++)scanf("%d%d%d",&w[i],&v[i],&cnt[i]);
for(int i=1;i<=n;i++)for(int j=0;j<=V;j++)for(int k=0;k*w[i]<=j&&k<=cnt[i];k++)
dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]*k]+v[i]*k);
printf("%d\n",dp[n][V]);
return 0;
}
这个不多解释了
但\(O(nm)\)的空间复杂度很容易 MLE
发现第一维用处不大可以压掉
将原转移式 dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]*k]+v[i]*k);
变成 dp[j]=max(dp[j],dp[j-w[i]*k]+v[i]*k);
但为了使后面一维存储上一行的信息
我们需要让j倒序枚举
即如下代码
#include<bits/stdc++.h>
using namespace std;
const int N=1003,M=20009;
int n,V,w[N],v[N],cnt[N],dp[M];
int main(){
scanf("%d%d",&n,&V);
for(int i=1;i<=n;i++)scanf("%d%d%d",&w[i],&v[i],&cnt[i]);
for(int i=1;i<=n;i++)for(int j=V;j>=0;j--)for(int k=0;k*w[i]<=j&&k<=cnt[i];k++)
dp[j]=max(dp[j],dp[j-w[i]*k]+v[i]*k);
printf("%d\n",dp[V]);
return 0;
}
但这个代码时间复杂度高达 \(O(nV\sum{cnt[i]})\)
是无法通过的
思考如何优化
我们发现问题关键在于如何快速求出\(max(dp[j-w[i]*k]+v[i]*k)\)
发现一个事情:\(dp[i]\) 一定只会由 \(dp[i-k*w[i]]\) 转移而来
这话说得,跟说了话似的
而显然 \(i\ mod\ w[i]=(i-k*w[i])\ mod\ w[i]\)
所以我们枚举每一个余数
然后枚举这个余数下每一个合法的值
for(int i=1;i<=n;i++){
for(int j=0;j<w[i];j++){
for(int k=0;k*w[i]+j<=V;k++){
//QwQ
}
}
}
声明一下:这是一个 \(O(nV)\) 的循环!
因为 \(j+k*w[i]\) 只会取到 \(0 \to V\)中的所有值
下一步我们发现同一余数的 DP 值只会用到前 cnt[i] 个
想求\(max(dp[j-w[i]*k]+v[i]*k)\)
这个式子中减去的 \(k\) 的最大值即为 \(cnt[i]\)
这其实就是一个类似滑动窗口的玩应
可以搞一个单调队列上去
但是有个问题:后半部分咋搞?
其实我们可以往单调队列里插一个 \(dp[k*w[i]+j]-v[i]*k\)
这样元素之间的大小关系没变
用的时候只需要 dp[j+k*w[i]]=dp[j+W[i]*q.front()]+v[i]*(k-q.front())
这也恰恰是只记下标不记录值的优点
如果你手写 \(Deque\ Mini\) 的话基本是\(O(nV)\)的
如果你还是没理解,看一下代码
空间暴力代码
#include<bits/stdc++.h>
using namespace std;
const int N=1003,M=20009;
int n,V,w[N],v[N],cnt[N],dp[N][M];
class Deque{
public:
bool empty(){return tt<ff;}
int front(){return k[ff];}
int back(){return k[tt];}
void pop_front(){++ff;}
void pop_back(){--tt;}
void push_back(int d){k[++tt]=d;}
void clear(){ff=1,tt=0;}
void show(){for(int i=ff;i<=tt;i++)printf("%d ",k[i]);puts("<");}
private:
int k[M],ff=1,tt=0;
}q;
int main(){
scanf("%d%d",&n,&V);
for(int i=1;i<=n;i++)scanf("%d%d%d",&w[i],&v[i],&cnt[i]);
for(int i=1;i<=n;i++){
for(int j=0;j<w[i];j++){
q.clear();
for(int k=0;k*w[i]+j<=V;k++){
while(!q.empty()&&k-q.front()>cnt[i])q.pop_front();
while(!q.empty()&&dp[i-1][q.back()*w[i]+j]-v[i]*q.back()<=dp[i-1][k*w[i]+j]-v[i]*k)q.pop_back();
q.push_back(k);
dp[i][j+k*w[i]]=dp[i-1][j+q.front()*w[i]]+v[i]*(k-q.front());
}
}
for(int j=1;j<=V;j++)dp[i][j]=max(dp[i][j],dp[i][j-1]);
}
printf("%d\n",dp[n][V]);
return 0;
}
空间优化代码
#include<bits/stdc++.h>
using namespace std;
const int N=1003,M=20009;
int n,V,w[N],v[N],cnt[N],dp[M],pre[M];
class Deque{
public:
bool empty(){return tt<ff;}
int front(){return k[ff];}
int back(){return k[tt];}
void pop_front(){++ff;}
void pop_back(){--tt;}
void push_back(int d){k[++tt]=d;}
void clear(){ff=1,tt=0;}
void show(){for(int i=ff;i<=tt;i++)printf("%d ",k[i]);puts("<");}
private:
int k[M],ff=1,tt=0;
}q;
int main(){
scanf("%d%d",&n,&V);
for(int i=1;i<=n;i++)scanf("%d%d%d",&w[i],&v[i],&cnt[i]);
for(int i=1;i<=n;i++){
for(int j=1;j<=V;j++)pre[j]=dp[j];
for(int j=0;j<w[i];j++){
q.clear();
for(int k=0;k*w[i]+j<=V;k++){
while(!q.empty()&&k-q.front()>cnt[i])q.pop_front();
while(!q.empty()&&pre[q.back()*w[i]+j]-v[i]*q.back()<=pre[k*w[i]+j]-v[i]*k)q.pop_back();
q.push_back(k);
dp[j+k*w[i]]=pre[j+q.front()*w[i]]+v[i]*(k-q.front());
}
}
for(int j=1;j<=V;j++)dp[j]=max(dp[j],dp[j-1]);
}
printf("%d\n",dp[V]);
return 0;
}
后记:
单调队列背包 \(DP\) 一定要用手写队列!
因为系统的 \(Deque\) 常数巨大
交 \(AcWing\) 会直接 \(TLE\)
终于点亮该技能啦(逃)

浙公网安备 33010602011771号