首页 > 其他分享 >单调队列优化dp

单调队列优化dp

时间:2023-05-18 11:57:48浏览次数:48  
标签:en 队列 long st int maxn dp now 单调

单调队列优化dp

对于一些比较简单的题目,我们可以直接凭借经验进行优化。

但是对于类似\(max(f[j-kw]+kv)\)(多重背包)我们就很难凭借经验找到优化方式了

这个时候,我们就可以尝试一下下面的方法:

我们观察一些简单的,可以单调队列优化的方程,他们都有这样的形式:

\(max/min(g[i-k])(L\leq k \leq R)\),这个时候我们就可以考虑对g数组单调队列

也就是我们要构造出这个g数组

使用待定系数法,我们可以将g数组写成\(g[B][x]=f[Ax+B]+Cx+D\)

至于B的大小,如果存在\(B_n=A\)的话,\(B=0,x=1\)和\(B=B_n=A,x=0\)对应的是同一个f数组,就会有一些\(f[Ax+b]\)重复入队出队,而一般而言我们只能进一次出一次,否则会导致复杂度的出错

因此我们有\(0\leq B < A\),且对于一个j,有且仅有一个x与之对应

我们要把\(f[j-kw]+kv\)与\(g[B][x]\)一一对应,计算时,对于不同的k,我们就取\(j-kw\)对应的x

也就是要把\(f[j-kw]+kv\)与\(f[Ax+B]+Cx+D\)一一对应

对于\(f[j]\)来说,我们迟早会有一个\(x_{now}\),使得\(f[j]=g[x_{now}]\)

因此我们取k=0或1(虽然不一定在单调队列的符合的范围)

所以我们就有

\[\left\{ \begin{aligned} &j-0*w=Ax_{now}+B\\ &j-1*w=A(x_{now}-1)+B\\ &0*v=Cx_{now}+D\\ &1*v=C(x_{now}-1)+D\\ \end{aligned} \right.\\ \]

算出

\[\left\{ \begin{aligned} &A=w\\ &B=j-0*w-Ax_{now}=j-wx_{now}=j\%w\\ &C=-v\\ &D=0*v-Cx_{now}=vx_{now} \end{aligned} \right.\\ \]

最终我们就将\(max(f[j-kw]+kv)\)转化成了\(max(g[B][x])\),就是\(max(f[w*x+j\%w]+(-v)*x+vx_{now})\)

这里我们要注意一下\(x\)和\(x_{now}\)的区别,计算同一条式子时,\(x\)是会变的,但是\(x_{now}\)是不会变的

最后就是取值范围的问题,一开始我们有\(L\leq k \leq R\),

\(k=0\)时,\(f[j-kw]=f[j-0*w]=f[j]\)对应\(g[B][x_{now}]\)

\(k=1\)时,\(f[j-kw]=f[j-1*w]=f[j-w]\)对应\(g[B][x_{now}-1]\)

...

\(k=L\)时,\(f[j-kw]=f[j-L*w]\)对应\(g[B][x_{now}-L]\)

\(k=R\)时,\(f[j-kw]=f[j-R*w]\)对应\(g[B][x_{now}-R]\)

所以x的取值范围是\(x_{now}-R\leq x \leq x_{now}-L\)

最终,对于单调队列,为了避免初始化的问题,

我们可以先添上新的符合条件的数\((x_{now}-L)\),更新优先队列

注意我们要判断一下\((x_{now}-L)\)有没有意义

然后退出不符合条件的数,这样即使新增加的数不符合条件(如距离)也会被弹出去

之后才计算答案

对于一些复杂的单调队列,最好先写朴素dp,再进行优化

例题

放假

经过几个月辛勤的工作,FJ决定让奶牛放假。

假期可以在1…N天内任意选择一段(需要连续),每一天都有一个享受指数W。

但是奶牛的要求非常苛刻,假期不能短于P天,否则奶牛不能得到足够的休息;

假期也不能超过Q天,否则奶牛会玩的腻烦。

FJ想知道奶牛们能获得的最大享受指数。

输入格式

第一行:N,P,Q.

第二行:N个数字,中间用一个空格隔开。

数据范围:

50% 1≤N≤10000

100% 1≤N≤100000

1 <= p <= q <= n

输出格式

一个整数,奶牛们能获得的最大享受指数

分析

朴素dp方程:\(f[i]=s[i]-min(s[k])(i-q\leq k\leq i-p)或f[i]=s[i]-min(s[i-k])(p\leq k\leq q)\)

一般来说,对于第一个方程,我们已经可以直接优化了

但是尝试一下新的做法

我们解得\(A=1,B=0,C=0,D=0\)

我们还知道\(x_{now}=i,L=p,R=q\)

\(x_{add}=x_{now}-L\)

然后就差不多做完了

代码

方法一:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int maxx=-(1e9+9),n,P,Q,a[maxn],s[maxn],f[maxn],q[maxn],st=1,en=0;
signed main(){
	scanf("%lld%lld%lld",&n,&P,&Q); 
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]); 
		s[i]=s[i-1]+a[i];
	}
	for(int i=1;i<=n;i++){
		int now=i-P;
		if(now<0)continue;
		while(st<=en&&s[q[en]]>=s[now])en--;
		while(st<=en&&q[st]<i-Q)st++;
		en++,q[en]=now;
		f[i]=s[i]-s[q[st]];
		maxx=max(maxx,f[i]);
	}
	cout<<maxx;

	return 0;
}

方法二:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int a[maxn],s[maxn],n,P,Q,q[maxn],maxx=-1e9-9;
int calc(int A,int B,int C,int D,int x){
	return s[A*x+B]+C*x+D;
}
signed main(){
	cin>>n>>P>>Q;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		s[i]=s[i-1]+a[i];
	}
	int st=1,en=0;
	for(int i=1;i<=n;i++){
		int A=1,B=0,C=0,D=0;
		int X=i,L=P,R=Q;
		int add_X=X-L;
		if(add_X<0)continue;
		while(st<=en&&calc(A,B,C,D,q[en])>=calc(A,B,C,D,add_X))en--;
		q[++en]=add_X;
		while(st<=en&&q[st]<X-R)st++;
		maxx=max(maxx,s[i]-calc(A,B,C,D,q[st]));
	}
	cout<<maxx<<endl;

	return 0;
}

实际上B,C,D现在没用,所以可以优化一下

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int a[maxn],s[maxn],n,P,Q,q[maxn],maxx=-1e9-9;
int calc(int x){
	return s[x];
}
signed main(){
	cin>>n>>P>>Q;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		s[i]=s[i-1]+a[i];
	}
	int st=1,en=0;
	for(int i=1;i<=n;i++){
		int X=i,L=P,R=Q;
		int add_X=X-L;
		if(add_X<0)continue;
		while(st<=en&&calc(q[en])>=calc(add_X))en--;
		q[++en]=add_X;
		while(st<=en&&q[st]<X-R)st++;
		maxx=max(maxx,s[i]-calc(q[st]));
	}
	cout<<maxx<<endl;

	return 0;
}

松果

有N棵松果树从左往右排一行,桃桃是一只松鼠,它现在在第一棵松果树上。

它想吃尽量多的松果,但它不想在地上走,而只想从一棵树跳到另一棵树上。

松鼠的体力有个上限,每次不能跳的太远,也不能跳太多次。

每当它跳到一棵树上,就会把那棵树上的松果全部都吃了。

它最多能吃到多少个松果?

输入格式

第一行,三个整数:N、D、M。N表示松果树的数量,

D表示松鼠每次跳跃的最大距离,M表示松鼠最多能跳跃M次。

接下来有N行,每行两个整数:Ai和Bi。

其中Ai表示第i棵树上的松果的数量,Bi表示第i棵树与第1棵树的距离,其中B1保证是0。
数据保证这N棵树从左往右的次序给出,即Bi是递增的,不存在多棵树在同一地点。

输出格式

一个整数。
【数据范围】
Ai <= 10000, D <= 10000
对于40%的数据,M<N<= 100, Bi <= 10000
对于100%的数据,M < N <= 2000, Bi <= 10000

分析

这题和上一题很类似,A=1,B=0,C=0,D=0

新增加的数为\(x_{add}=j-1\)

至于距离,我们要判断的是\(A*q[st]+B\)到\(A*x_{now}+B\)的距离,就不要用朴素方程的来判断了

代码

方法一:

#include<bits/stdc++.h>
using namespace std;
const int maxn=2*1e3+5;
int n,d,m,a[maxn],b[maxn],f[maxn][maxn];
int q[maxn],st,en,maxx=0; 
int main(){
	scanf("%d%d%d",&n,&d,&m);
	for(int i=1;i<=n;i++)scanf("%d%d",&a[i],&b[i]);
	memset(f,-0x3f,sizeof f);
	f[0][1]=a[1];
	for(int i=1;i<=m;i++){
		st=1,en=0;
		for(int j=1;j<=n;j++){
			int k=j-1;
			if(k<1)continue;
			while(st<=en&&b[j]-b[q[st]]+1>d)st++;
			while(st<=en&&f[i-1][q[en]]<f[i-1][k])en--;
			en++,q[en]=k;
			f[i][j]=f[i-1][q[st]]+a[j];
			maxx=max(maxx,f[i][j]);
		}
	}
	cout<<maxx;

	return 0;
}

方法二:

#include<bits/stdc++.h>
using namespace std;
const int maxn=2*1e3+5;
int n,d,m,a[maxn],b[maxn],f[maxn][maxn];
int q[maxn],st,en,maxx=0;
int calc(int A,int B,int C,int D,int x,int whi){
	return f[whi][A*x+B]+C*x+D;
} 
int main(){
	scanf("%d%d%d",&n,&d,&m);
	for(int i=1;i<=n;i++)scanf("%d%d",&a[i],&b[i]);
	memset(f,-0x3f,sizeof f);
	f[0][1]=a[1];
	for(int i=1;i<=m;i++){
		st=1,en=0;
		for(int j=1;j<=n;j++){
			int A=1,B=0,C=0,D=0;
			int X=j;
			int add_X=j-1;
			if(add_X<=0)continue;
			while(st<=en&&calc(A,B,C,D,q[en],i-1)<=calc(A,B,C,D,add_X,i-1))en--;
			q[++en]=add_X;
			while(st<=en&&b[A*q[st]+B]<b[A*X+B]-d)st++;
			f[i][j]=a[j]+calc(A,B,C,D,q[st],i-1);
			maxx=max(maxx,f[i][j]);
		}
	}
	cout<<maxx;

	return 0;
}

P2254 [NOI2005] 瑰丽华尔兹

分析

难度不大,主要是简化代码

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=205;
int n,m,X,Y,K,f[maxn][maxn][maxn],x,y,op,maxx=0,qx[maxn],qy[maxn],qz[maxn];
int dirx[]={0,-1,1,0,0};
int diry[]={0,0,0,-1,1};
bool mp[maxn][maxn];
char c[205];
bool check(int x,int y){
	return 1<=x&&x<=n&&1<=y&&y<=m;
}
void work(int x,int y,int whi,int len,int pos){
	int st=1,en=0;
	for(int i=1;check(x,y);i++,x+=dirx[whi],y+=diry[whi]){
		if(!mp[x][y]){st=1,en=0;continue;}
		while(st<=en&&f[pos-1][qx[en]][qy[en]]+i-qz[en]<=f[pos-1][x][y])en--;
		while(st<=en&&abs(x-qx[st])+abs(y-qy[st])>len)st++;
		en++,qx[en]=x,qy[en]=y,qz[en]=i;
		f[pos][x][y]=f[pos-1][qx[st]][qy[st]]+i-qz[st];
	}
}
int main(){
	scanf("%d%d%d%d%d",&n,&m,&X,&Y,&K);
	memset(mp,true,sizeof mp);
	for(int i=1;i<=n;i++){
		scanf("%s",c);
		for(int j=1;j<=m;j++){
			if(c[j-1]=='x')mp[i][j]=false;
		}
	}
	memset(f,-0x3f,sizeof f);
	f[0][X][Y]=0;
	for(int whi=1;whi<=K;whi++){
		scanf("%d%d%d",&x,&y,&op);
		y=y-x+1;
		if(op==1)for(int i=1;i<=m;i++)work(n,i,op,y,whi);
		if(op==2)for(int i=1;i<=m;i++)work(1,i,op,y,whi);
		if(op==3)for(int i=1;i<=n;i++)work(i,m,op,y,whi);
		if(op==4)for(int i=1;i<=n;i++)work(i,1,op,y,whi);
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			maxx=max(maxx,f[K][i][j]);
	cout<<maxx<<endl;
	return 0;
}

P1776 宝物筛选

分析

多重背包,绝大部分分析就在最开头

实际上,B的意义就是,你要开B+1条单调队列

在打代码的时候,我们会发现我们无法同时开下多个单调队列

因此我们考虑一条一条队列处理,其他的就不难了

由于是单调队列优化,我们不可以逆着过来做,而我们先计算出的答案又会影响后面的答案

因此我们用滚动数组避免这个问题

顺便也把二进制拆分的代码贴上去(思想就是一个数拆成一位一位的二进制,最后剩下的再额外算)

代码

二进制优化

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=4*1e4+5;
int maxx=0,f[maxn],n,W,v,w,m;
void solve(int v,int w){
	for(int i=W;i>=w;i--)f[i]=max(f[i-w]+v,f[i]),maxx=max(maxx,f[i]);
}
signed main(){
	scanf("%lld%lld",&n,&W);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld%lld",&v,&w,&m);
		int M=1;
		while(M<=m){
			solve(v*M,w*M);
			m-=M;
			M*=2;
		}
		if(m!=0)
		solve(v*m,w*m);
	}
	cout<<maxx;
	return 0;
}

单调队列优化

#include<bits/stdc++.h>
using namespace std;
const int maxn=4*1e4+5;
int dp[2][maxn],n,W,v,w,m,st,en,q[maxn],maxx=0,whi;
inline int calc(int A,int B,int C,int D,int whi,int x){
	return dp[whi][A*x+B]+C*x+D;
}
int main(){
	memset(dp,-0x3f,sizeof dp);
	dp[1][0]=0;whi=1;
	cin>>n>>W;
	for(int i=1;i<=n;i++){
		cin>>v>>w>>m;
		whi=!whi;
		for(int j=0;j<w;j++){
			st=1,en=0;
			for(int k=j;k<=W;k+=w){
				int X=k/w;
				int L=0,R=m,A=w,B=j,C=-v,D=(L+X)*v;
				int add_X=X-L;
				while(st<=en&&calc(A,B,C,D,!whi,q[en])<=calc(A,B,C,D,!whi,add_X))en--;
				q[++en]=add_X;
				while(st<=en&&q[st]<X-R)st++;
				int used_X=q[st];
				dp[whi][A*X+B]=max(dp[!whi][A*X+B],calc(A,B,C,D,!whi,used_X));
				maxx=max(maxx,dp[whi][A*X+B]);
			}
		}
	}
	cout<<maxx;
	

	return 0;
}

P1070 [NOIP2009 普及组] 道路游戏

分析

一样是先写出朴素dp,优化时我们要注意:

我们增加数的时候,转移方程是依附于之前的(包括现在的)时间节点的数组f的最终的答案

因此我们要先计算出最终答案再增加数

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+5;
int n,m,p,a[maxn][maxn],f[maxn],pay[maxn],s[maxn][maxn],st[maxn],en[maxn],q1[maxn][maxn],q2[maxn][maxn];
int change(int x){
	x=(x%n+n)%n;
	return x;
}
int main(){
	scanf("%d%d%d",&n,&m,&p);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			scanf("%d",&a[j][i]);
			if(i==n)a[j][0]=a[j][i];
		}
	}
	for(int i=0;i<n;i++)scanf("%d",&pay[i]);
	for(int i=1;i<=m;i++){
		for(int j=0;j<n;j++){
			s[i][j]=s[i-1][change(j-1)]+a[i][j];
		}
	}
	for(int i=0;i<n;i++)st[i]=1,en[i]=0;
	memset(f,-0x3f,sizeof f);
	f[0]=0;
	for(int T=0;T<=m;T++){
		for(int i=0;i<n;i++){
			int whi=((i-T)%n+n)%n;
			while(st[whi]<=en[whi]&&q2[whi][st[whi]]+p<T)st[whi]++;
			if(st[whi]<=en[whi])f[T]=max(f[T],q1[whi][st[whi]]+s[T][i]);
		}
		for(int i=0;i<n;i++){
			int whi=((i-T)%n+n)%n;
			while(st[whi]<=en[whi]&&q1[whi][en[whi]]<=f[T]-s[T][i]-pay[i])en[whi]--;
			en[whi]++,q1[whi][en[whi]]=f[T]-s[T][i]-pay[i],q2[whi][en[whi]]=T;
		}
	}
	cout<<f[m];
	return 0;
}

P3957 [NOIP2017 普及组] 跳房子

分析

对于这一题,我们还是采用两种方法,不过对于第二种方法,我们要额外记录一个last入队

其他差不多

代码

方法一:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=5*1e5+5;
int n,d,k,a[maxn],b[maxn];
int q1[maxn],q2[maxn],st1,st2,en1,en2;
int f[maxn];
signed main(){
	cin>>n>>d>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i];
	}
	int l=-1,r=1e17;
	while(l+1<r){
		int mid=(l+r)/2;
		int L=max(1ll,d-mid),R=d+mid;
		st1=1,st2=1,en1=0,en2=1;
		q2[st2]=0;
		bool flag=false;
		for(int i=1;i<=n;i++){
			while(st2<=en2&&a[q2[st2]]+L<=a[i]){
				while(st1<=en1&&f[q1[en1]]<=f[q2[st2]])en1--;
				q1[++en1]=q2[st2++];
			}
			while(st1<=en1&&a[q1[st1]]+R<a[i])st1++;
			if(st1<=en1){
				f[i]=f[q1[st1]]+b[i];
				if(f[i]>=k){
					flag=true;
					break;
				}
				q2[++en2]=i;
			}
		}
		if(flag)r=mid;
		else l=mid;
	}
	if(r==1e17)r=-1;
	cout<<r<<endl;
	

	return 0;
}

方法二:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=5*1e5+5;
int n,d,k,a[maxn],b[maxn];
int q[maxn],st,en;
int f[maxn];
bool vis[maxn];
int calc(int A,int B,int C,int D,int x){
	return f[A*x+B]+C*x+D;
}
signed main(){
	cin>>n>>d>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i];
	}
	int l=-1,r=1e17;
	while(l+1<r){
		int mid=(l+r)/2;
		int L=max(1ll,d-mid),R=d+mid;
		st=1,en=0;
		bool flag=false;
		int last=0;
		for(int i=1;i<=n;i++){
			f[i]=0;
			vis[i]=false;
			int A=1,B=0,C=0,D=0;
			int X=i;
			while(last<i&&a[last]<=a[X]-L){
				int add_X=last;
				if(!vis[add_X]){
					last++;
					continue;
				}
				while(st<=en&&calc(A,B,C,D,q[en])<=calc(A,B,C,D,add_X))en--;
				q[++en]=add_X;
				last++;
			}
			while(st<=en&&a[q[st]]<a[X]-R)st++;
			if(st<=en)
				f[A*X+B]=f[A*q[st]+B]+b[i],vis[A*X+B]=true;
			if(f[A*X+B]>=k){
				flag=true;
				break;
			}
		}
		if(flag)r=mid;
		else l=mid;
	}
	if(r==1e17)r=-1;
	cout<<r<<endl;
	

	return 0;
}

标签:en,队列,long,st,int,maxn,dp,now,单调
From: https://www.cnblogs.com/Ayaka-T/p/17411494.html

相关文章

  • kube-proxy修改日志级别并观察endpoint变化
    k8sv1.15.0修改日志级别keditdskube-proxy-nkube-system增加kube-system命名空间下corednsPodkgetendpointskube-dns-nkube-system-oyaml持续输出kube-proxy日志dockerlogs-f`dockerps|grepkube-proxy|grep-vpause|awk'{print$1}'`pkg/prox......
  • 消息队列篇
    本文章学习资源黑马程序员Kafka视频教程及资料,黑马程序员YYDS一、简介1.消息队列简介消息队列,英文名:MessageQueue,经常缩写为MQ。从字面上来理解,消息队列是一种用来存储消息的队列。我们可以简单理解消息队列就是将需要传输的数据存放在队列中。很多时候消息队列不是一个永久性......
  • MATLAB仿真8DPSK信号在AWGN信道下的误码率分析 商品形式:程序 实现功能
    MATLAB仿真8DPSK信号在AWGN信道下的误码率分析商品形式:程序实现功能:MATLAB仿真8DPSK信号在AWGN信道下的误码率与误比特率分析,与理论值进行比较。ID:6250644391639967......
  • STM32环形串口队列程序 大数据串口收发 实时不丢包 串口程序平常产品开发中编写或移
    STM32环形串口队列程序大数据串口收发实时不丢包串口程序平常产品开发中编写或移植的程序并亲自测试通过,均为工程文件格式,可直接编译使用。注:毫无基础的请勿拍,程序文件不接受退货。该程序为大数据量吞吐的串口收发例程,中断接收,边收边发,采用大数据环形队列,处理过程超快不丢包,接......
  • STM32F107单片机驱动Dp83848以太网芯片程序 项目开发用到了Dp83848
    STM32F107单片机驱动Dp83848以太网芯片程序项目开发用到了Dp83848这一个以太网芯片,本人发现其配置起来比较麻烦,所以整理了一份STM32F107单片机驱动Dp83848的程序代码例程,方便大家学习相关代码的配置ID:3821630931468138......
  • 7935: 最大值问题 单调队列
    描述 给定n个正整数,crq先选了第1~k个数,要求yuyu求出最大值,yuyu轻松完成,crq直接甩出一堆,要求第2~k+1个,3~k+2个,...,n-k+1~n个,全部都求出来,之后便轻松休息了。  输入 第一行两个整数 n和k(1≤k≤n≤106)。第二行 n个整数,表示编号1~n方格中的数字ai(1≤ai≤3×107)。......
  • ThreadPoolTaskExecutor与ThreadPoolExecutor的区别及优缺点
    ThreadPoolTaskExecutor和ThreadPoolExecutor都是线程池的实现,但它们有以下几点区别:1.ThreadPoolTaskExecutor是Spring框架中编写的,它对ThreadPoolExecutor进行了封装,提供了更加丰富的功能,更易于在Spring中使用。而ThreadPoolExecutor是JDK中的实现。2.ThreadPoolTaskExe......
  • 【Antd】表单调整输入框对齐方式:
    constformItemLayout={labelCol:{//左边文字xs:{span:24},sm:{span:6},},wrapperCol:{//右边输入框xs:{span:24},sm:{span:16},},};consttailFormItemLayout={wrapperCol:{xs......
  • wordpress 优化备份还原插件duplicator-pro-4_5_3_2的使用填坑
     创建备份我这边没有出错,就不说了 插件下载地址:https://www.wpjzb.com/wp-plugins/duplicator-pro/我是应的是  https://pan.baidu.com/share/init?surl=YRss-vqBVY2Twv1tBid9fQ   提取码:ibnshttps://pan.baidu.com/share/init?surl=6VSX3FUlugtfBfTPj4wLbg 提取......
  • dpkg命令用法、Ubuntu下deb包的解压、打包、安装、卸载及常用命令参数
    dpkg命令的用法不带图简装:https://blog.csdn.net/wanghuohuo13/article/details/78916821?ops_request_misc=&request_id=&biz_id=102&utm_term=dpkg&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-6-.first_rank_v2_pc_rank_v29&am......