单调队列优化多重背包

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\)
终于点亮该技能啦(逃)

posted @ 2025-02-22 15:57  2025ing  阅读(25)  评论(0)    收藏  举报