单调队列基础

经典例题:滑动窗口

链接:luoguP

题意简述:你有 N N N个数,有一段长度为 k k k的区间从左至右滑动,每次滑动一个单位问每次滑动后区间内的最大值与最小值。 ( n ≤ 1 0 6 ) (n\le10^6) (n106)

下面是一个例子。

考虑暴力,时间复杂度 O ( n 2 ) O(n^2) O(n2),显然不能通过 1 0 6 10^6 106的数据。

再考虑使用我们的线段树 O ( n l o g n ) + O ( n l o g n ) O(nlogn)+O(nlogn) O(nlogn)+O(nlogn)或ST表 O ( n l o g n ) + O ( n ) O(nlogn)+O(n) O(nlogn)+O(n),效率都不是太理想,勉强能过。

那么我们有没有更好的查询数列的方法呢?

单调队列

定义

一个队列里的数都是单调递减或单调递增的队列。

大体思路

以查询最小值为例,我们维护一个单调递减的队列,队首始终为最小值(具体维护方法将在下文提及)。

假设我们已经维护了这个队列,那么单次查询的复杂度为 O ( 1 ) O(1) O(1),为了更有效的处理问题,我们需要使维护队列的总时间复杂度为 O ( n ) O(n) O(n),那么,我们又该如何维护呢?

维护方式

首先,我们定义两个指针,分别对应队首和队尾。

int head=1,tail=0;

此处如此定义是因为我们认为当 h e a d ≤ t a i l head\le tail headtail时队列中存在一些元素,所以我们定义时只需使 h e a d = t a i l + 1 head=tail+1 head=tail+1即可保证队列开始无元素且插入一个数后 h e a d ≤ t a i l head\le tail headtail

在本题的背景中,窗口一直在滑动,我们的队首元素不可避免地会过时,为了应对这种情况,我们用一个 p p p数组来记录队列中每一个数在原数组里的下标。如果 p [ h e a d ] p[head] p[head]已经不在我们的区间 [ L , R ] [L,R] [L,R]中,那么我们就让它出队。

while(head<=tail&&p[head]<=i-k){
	head++;
}

在我们插入一个数时,如果队尾元素 x x x大于我们要插入的那个数 y y y,那么我们就使该元素出队。

while(head<=tail&&q[tail]>a[i]){
	tail--;
}

为什么这样做是对的呢?原因也很简单,因为窗口一直向右滑动,所以 y y y永远比 x x x后出队,那么 x x x就永远不可能成为最小值。

然后我们向队列中插入数 x x x,更新 p p p数组与 q q q数组,记录答案。

	q[++tail]=a[i];
	p[tail]=i;
	printf("%d ",q[head]);

在一般情况下,我们其实不需要用到 p p p数组,我们只需要用 q q q数组代替 p p p数组,然后用 a [ q [ h e a d ] ] a[q[head]] a[q[head]]来代替 q q q数组。

Code(省略p数组版)
#include<bits/stdc++.h>
using namespace std;
int Read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')  f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+ch-'0';
		ch=getchar();
	}
	return x*f;
}
int n,k,a[1000005],ans[1000005],q[1000005];
signed main(){
	n=Read(),k=Read();
	for(int i=1;i<=n;i++){
		a[i]=Read();
	}
	int head=1,tail=0;
	for(int i=1;i<=n;i++){
		while(head<=tail&&i-q[head]+1>k){
			++head;
		}
		while(head<=tail&&a[q[tail]]>a[i]){
			tail--;
		}
		q[++tail]=i;
		ans[i]=a[q[head]];
	}
	for(int i=k;i<=n;i++){
		printf("%d ",ans[i]);
	}
	cout<<endl;
	head=1,tail=0;
	for(int i=1;i<=n;i++){
		while(head<=tail&&i-q[head]+1>k){
			++head;
		}
		while(head<=tail&&a[q[tail]]<a[i]){
			tail--;
		}
		q[++tail]=i;
		ans[i]=a[q[head]];
	}
	for(int i=k;i<=n;i++){
		printf("%d ",ans[i]);
	}
}
时间复杂度分析

每个元素只会入队一次,出队一次,所以时间复杂度为 O ( n ) O(n) O(n)

拓展

给定一个数列 a a a,对于每个数 i i i,我们需要求出对于 j ∈ [ i − L , i − R ] j∈[i-L,i-R] j[iL,iR] a [ j ] a[j] a[j]的最小值。

一样的用单调队列操作,在执行操作是判断区间长度是否在 [ L , R ] [L,R] [L,R]间即可。

Code
	int head=1,tail=0;
	double ans=0;
	for(int i=1;i<=n;i++){
		while(head<=tail&&i-q[head]>R){
			++head;
		}
		if(i>=L){
			while(head<=tail&&sum[q[tail]]>sum[i-L]){
				--tail;
			}
			q[++tail]=i-L;
			ans=max(ans,sum[i]-sum[q[head]]);
		}
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值