单调队列优化多重背包

Problem 多重背包问题 III

N种物品和一个容量是V的背包。
i种物品最多有si件,每件体积是wi,价值是vi
输出最大价值。
0<N1000
0<V20000
0<vi,wi,si20000

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(nVcnt[i])
是无法通过的
思考如何优化
我们发现问题关键在于如何快速求出max(dp[jw[i]k]+v[i]k)
发现一个事情:dp[i] 一定只会由 dp[ikw[i]] 转移而来
这话说得,跟说了话似的
而显然 i mod w[i]=(ikw[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+kw[i] 只会取到 0V中的所有值
下一步我们发现同一余数的 DP 值只会用到前 cnt[i] 个
想求max(dp[jw[i]k]+v[i]k)
这个式子中减去的 k 的最大值即为 cnt[i]
这其实就是一个类似滑动窗口的玩应
可以搞一个单调队列上去
但是有个问题:后半部分咋搞?
其实我们可以往单调队列里插一个 dp[kw[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 on   2025ing  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示