单调队列与滑动窗口

以下记录了对于单调队列和滑动窗口关系的理解过程,有错误请指正

什么是单调队列呢:直接上例子理解吧。
对于一个序列{3,1,4,6,2,7},现在我们来看一个个元素加入一个单调递增队列中的变化情况:
加入1号元素:此时队列空,直接加入。{1}(注意存的是下标
加入2号元素:2号元素为1,比队尾的一号元素小,1号元素出队,此时队列为空,不能再比较,2号元素入队{2}
加入3号元素:3号元素为4,比队尾的2号元素大,直接放尾部。{2,3}
加入4号元素:理由同上。{2,3,4}
加入5号元素:5号元素比队尾的4号小,4号元素出队,继续与队尾比较,也比3号元素小,3号出队,继续比较,发现比2号元素大,此时即可将5号元素加入。{2,5}
加入6号元素:比队尾的5号元素大,直接放队尾。{2,5,6}
这就是每次加入元素的队列中的变化。按照给定元序列的顺序加入,何时都保持单调(单调指的是对应的值,队列中存的是下标)。看着可能和单调栈没有区别,但是,单调队列是可能要 去掉top 元素的。

刚开始写滑动窗口的题的时候对单调队列的理解还很浅,只是一葫芦画瓢写出来了。后面想想其实滑动窗口用到单调队列是很有道理的。

一开始如果直接去想滑动窗口,比如这个模板题题目链接,一开始可能就会想:直接记录第一个窗口内的最大和最小,然后每次窗口移动都会增加一个新元素,然后每次去判断新元素和上一个窗口的最大最小。但显然,这样的思路有一个很大的问题,那就是没有考虑被滑走的也就是被删去的元素的影响,如果被滑走的元素正好是上一个窗口最大或者最小,也就是上个窗口的边缘元素,那么我们是没办法迅速得出新窗口的最大或者最小值的。所以这时候想法就油然而生,有没有什么办法,让我们可以知道,如果上个窗口的最大或最小是左边缘的话,我也还是可以得到上个窗口范围内除去左边缘的最大或者最小呢。那此时,单调队列的作用就要体现出来了。我们可以很轻松地发现,加入某个元素对这个元素后面的元素在单调队列中的排列是没有一点影响的,只能影响前面的元素,那么我们就可以用单调队列很轻松的解决刚才的难处。看个例子 就会更清楚了:假设从1号元素开始到现在加入了5号元素后的单调递增队列是{2,3,5},那么显然,我们可以很轻松地知道,[ 1, 5 ]范围内的最小值就是top位置上的2号位的值了,那现在我想知道3号位置到5号位置的最小怎么办?直接不要看2,说白了就把2删了,那么现在top的位置就是3到5号的最小值的下标了,这自然就是因为加入元素不会对后加入的元素的排列造成任何影响,我们可以直接看成2后面的序列是, 从3号位置开始加入,一直加到5号位置的一个单调队列,3到5号位置的最小值自然就是去掉2后的top中放的下标对应的值。那么现在我们就能很好地用这个性质,完成滑动窗口了。对于那道模板题而言,现求最小,最大同理。直接先把一个窗口长度的元素都加入一个单调队列,然后往后再一个个判断,因为在加入一个元素时,现在top位的值一定是上一个窗口的最小值的下标,那么很显然,如果这个下标在上个窗口的边缘,直接pop,然后再把新元素加入单调队列,现在的top就是新窗口的最小值的下标了,如果上个窗口的最小不是窗口左边缘的话,直接将新元素加入队列即可。依次往后,就能很轻松的得到答案。模板题代码如下:

#include <iostream>
#include <cstdio>
#include <queue>
#include <list>
using namespace std;

list<int> L;

int a[1000010];

int main(){
	int n,k;
	scanf("%d%d",&n,&k);
	for(int i=0;i<n;++i){
		scanf("%d",&a[i]);
	}
	L.push_back(0);
	for(int i=0;i<k;++i){
		while(!L.empty() && a[i]<=a[ L.back() ]){
			L.pop_back();
		}
		L.push_back(i);
	}
	printf("%d",a[ L.front() ]);
	for(int i=k;i<n;++i){
		while(!L.empty() && L.front() < i-k+1){
			L.pop_front();
		}
		while(!L.empty() && a[ L.back() ] >= a[i]){
			L.pop_back();
		}
		L.push_back(i);
		printf(" %d",a[ L.front() ]);
	}
	printf("\n");
	L.clear();
	L.push_back(0);
	for(int i=0;i<k;++i){
		while(!L.empty() && a[i] >= a[ L.back() ]){
			L.pop_back();
		}
		L.push_back(i);
	}
	printf("%d",a[ L.front() ]);
	for(int i=k;i<n;++i){
		if(!L.empty() && L.front() < i-k+1){
			L.pop_front();
		}
		while(!L.empty() && a[ L.back() ] <= a[i]){
			L.pop_back();
		}
		L.push_back(i);
		printf(" %d",a[ L.front() ]);
	}
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值