[总结] 单调队列优化 DP

Fence

\(F[i,j]=P_i* j+max_{j-L_i\leq k\leq S_i-1} \ \{F[i-1,k]+P_i* k\}\)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 16010,maxm = 110;
struct node{
	int L,P,S;
}a[110];
bool operator <(const node a,const node b){
	return a.S<b.S;
}
int n,m;
int f[maxm][maxn],q[maxn];
int calc(int i,int k){
	return f[i-1][k]-a[i].P*k;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)scanf("%d%d%d",&a[i].L,&a[i].P,&a[i].S);
	sort(a+1,a+1+m);
	/////正解
	for(int i=1;i<=m;i++){
		int	head=1,tail=0;
		for(int k=max(0,a[i].S-a[i].L);k<=a[i].S-1;k++){
			while(head<=tail && calc(i,q[tail])<=calc(i,k))tail--;
			q[++tail]=k;
		}
		for(int j=1;j<=n;j++){//状态 
			f[i][j]=max(f[i-1][j],f[i][j-1]);
			////
			if(j>=a[i].S){
				while(head<=tail && q[head]<j-a[i].L)head++;
				if(head<=tail)f[i][j]=max(f[i][j],calc(i,q[head])+a[i].P*j);
			}
		}
	}
	printf("%d",f[m][n]);
	return 0;
}

注意初始化单调队列

多重背包(单调队列优化)

基本思想:对于每一个物品,按照余数分组(这才可以转移)

\(F[u+p* V_i]= max\{F[u+k* V_i]+(p-k)* W_i\}\)

等价于

\(F[u+p*V_i]=max\{F[u+k*V_i]-k*W_i \}+p*W_i\ \ (k\in[p-C_i,p-1])\)

这也体现了单调队列优化 DP 的基本思想:把状态和决策两部分分开来,维护\(k\) 这个决策集合

  • 注意单调队列不要开小

  • 因为是同余的数相互转移,因此最后需要取 \(max\),而不是 \(F[M]\)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 10 , maxm = 2e4 + 100;
int f[maxm],v[maxn],w[maxn],C[maxn],q[maxn];//体积,价值,个数,单调队列 
int n,M;
int calc(int i,int u,int k){
	return f[u+k*v[i]]-k*w[i];
}
int main(){
	scanf("%d%d",&n,&M);
	memset(f,0xcf,sizeof f);
	f[0]=0;
	for(int i=1;i<=n;i++){
		scanf("%d%d%d",&v[i],&w[i],&C[i]);
		for(int u=0;u<v[i];u++){
			int head=1,tail=0;
			int maxp=(M-u)/v[i];//对于当前余数最多能装几个
			for(int k=maxp-1;k>=max(0,maxp-C[i]);k--){
				while(head<=tail && calc(i,u,q[tail])<=calc(i,u,k))tail--;
				q[++tail]=k;
			}
			for(int p=maxp;p>=0;p--){
				while(head<=tail && q[head]>p-1)head++;
				if(head<=tail){//转移 
					f[u+p*v[i]]=max(f[u+p*v[i]],calc(i,u,q[head])+p*w[i]);
				}
				if(p-C[i]-1>=0){//加入新决策 
					while(head<=tail && calc(i,u,q[tail])<=calc(i,u,p-C[i]-1))tail--;
					q[++tail]=p-C[i]-1;
				}
			}
		}
	}
	int ans=-1;
	for(int i=1;i<=M;i++)ans=max(ans,f[i]);
	printf("%d",ans);
	return 0;
}

P2569 [SCOI2010]股票交易

分四种情况讨论即可,借这道题强调几个细节。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 2010;
#define LL long long
int n,maxp,W,from;
LL AP[maxn],BP[maxn],AS[maxn],BS[maxn];
LL f[maxn][maxn];
int q[maxn];
int head=1,tail=0;
LL calc1(int i,int k){return f[from][k]+k*AP[i];}
LL calc2(int i,int k){return f[from][k]+k*BP[i];}
int main(){
	//freopen("trade7.in","r",stdin);
	scanf("%d%d%d",&n,&maxp,&W);
	memset(f,0xcf,sizeof f);
	for(int i=0;i<=n;i++)f[i][0]=0;
	for(int i=1;i<=n;i++)scanf("%lld%lld%lld%lld",AP+i,BP+i,AS+i,BS+i);
	for(int i=1;i<=n;i++){
		head=1;tail=0;
		for(int j=0;j<=maxp;j++)f[i][j]=max(f[i][j],f[i-1][j]);
		from=max(0,i-W-1);
		//买入
		head=1;tail=0;
		for(int j=0;j<=maxp;j++){
			while(head<=tail && q[head]<j-AS[i])head++;
			while(head<=tail && calc1(i,q[tail])<=calc1(i,j))tail--;
			q[++tail]=j;
			if(head<=tail)f[i][j]=max(f[i][j],calc1(i,q[head])-j*AP[i]);
		}
		//卖出
		head=1;tail=0;
		for(int j=maxp;j>=0;j--){ 
			while(head<=tail && q[head]>j+BS[i])head++;
			while(head<=tail && calc2(i,q[tail])<=calc2(i,j))tail--;
			q[++tail]=j;
			if(head<=tail)f[i][j]=max(f[i][j],calc2(i,q[head])-j*BP[i]);
		}
	}
	//for(int i=1;i<=n;i++)for(int j=0;j<=maxp;j++)printf("f[%d][%d]: %lld\n",i,j,f[i][j]);
	LL ans=-1e19;
	for(int i=0;i<=maxp;i++)ans=max(ans,f[n][i]);
	printf("%lld\n",ans);
	return 0;
}

细节

  • 初始决策集合用不用处理根据题目性质(状态转移方程)决定

  • 有时需要处理滑动窗口的初始窗口

  • 有时需要边转移边处理然后再滑动,比如上面那道题

  • 对于这道题来说,因为可以之前什么都不干,所以在 \(i-W-1<0\) 的时候,需要进行 \(DP\) 的初始化:从 0 的阶段转移过来

posted @ 2021-08-12 16:59  ¶凉笙  阅读(32)  评论(0编辑  收藏  举报