滑动窗口/模板单调队列 题解

Posted eleven-qian-shan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了滑动窗口/模板单调队列 题解相关的知识,希望对你有一定的参考价值。

滑动窗口/【模板】单调队列

https://www.luogu.com.cn/problem/P1886

前言:

个人而言,这是一道很不错的题。为什么?因为OIer们可以使用多种不做法A掉这道题(当然,方法最多的貌似还是著名的A+B系列问题??

该篇题解通过三种做法(严格来算是两种)来讲解该题qwq

题目简述:

给定n个数,窗口长度k,窗口从元素1开始依次向右移动一个元素,要求求出每次窗口内的最小值和最大值

输出第一行输出全部最小值,第二行输出全部最大值


算法一&二:单调队列Deque

PS:双倍经验 洛谷P2032 扫描

  • 直接使用强大的STL中的deque开干

解题思路:

因为是分开输出最小值和最大值,所以我们分两次使用deque

注意:deque存的最好是下标,如果直接存元素会出现未知错误(因为这个调了好久,最后我还是不知道为啥QAQ)但是为了讲解方便,说成deque存的是元素,大家留意一下,不要被误导了!

第一次我们专门处理每个窗口段的最小值:

(1)如果deque的队尾元素deque.back()大于当前处理元素a[i],就弹出队尾元素,直到队列为空或当前队尾元素小于等于a[i]。因为这样就保证了队列中的是最小值

(2)如果当前处理元素的下标i≥k,则判断当前deque中的元素个数是否超过了窗口最大长度,是的话就将队首元素依次删除deque.front(),直到队列为空或当前队首元素大于等于a[i-k]。这样我们就可以防止前面的最小值本应该不在窗口段内却被处理成最小值的情况

第二次我们专门处理每个窗口段的最大值:

(1)处理完最小值后一定要清空deque

(2)处理步骤和上述差不多,只是改了一个地方:如果deque的队尾元素deque.back()小于当前处理的元素a[i],就弹出队尾元素,直到队列为空或当前队尾元素大于等于a[i]。因为这样就保证了队列中的是最大值

现在给出STL版deque做法的代码:

/* STL版的deque */
#include<bits/stdc++.h>
using namespace std;
int n,m,a[1000005];

deque<int> shan;

int main() {
	scanf("%d%d",&n,&m);
	for(register int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(register int i=1;i<=n;i++) {
		while(!shan.empty()&&a[shan.back()]>a[i]) shan.pop_back();
		shan.push_back(i);
		if(i>=m) {
			while(!shan.empty()&&shan.front()<=i-m) shan.pop_front();
			printf("%d ",a[shan.front()]);
		}
	}
	puts("");
	while(!shan.empty()) shan.pop_front();
	for(register int i=1;i<=n;i++) {
		while(!shan.empty()&&a[shan.back()]<a[i]) shan.pop_back();
		shan.push_back(i);
		if(i>=m) {
			while(!shan.empty()&&shan.front()<=i-m) shan.pop_front();
			printf("%d ",a[shan.front()]);
		}
	}
	return 0;
}

  • 再来讲讲手写版的deque

思路和STL版的一样,只是emmmm..应该叫表现方式不一样,直接给出代码吧qvq

/* 手写版的deque */
#include<bits/stdc++.h>
using namespace std;
int n,m,head=1,tail=0,a[10000005];
int ma[10000005],mi[10000005];

int main() {
	scanf("%d%d",&n,&m);
	for(register int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(register int i=1;i<=n;i++) {
		while(head<=tail&&mi[head]+m<=i) head++;
		while(head<=tail&&a[i]<a[mi[tail]]) tail--;
		mi[++tail]=i;
		if(i>=m) printf("%d ",a[mi[head]]);
	}
	puts("");
	head=1,tail=0;
	for(register int i=1;i<=n;i++){
		while(head<=tail&&ma[head]+m<=i) head++;
		while(head<=tail&&a[i]>a[ma[tail]]) tail--;
		ma[++tail]=i;
		if(i>=m) printf("%d ",a[ma[head]]);
	}
	return 0;
}

算法三:ST表

  • 补充知识:

ST算法——RMQ的求解方法(RMQ问题指询问某个区间内的最值)

相对于线段树而言,ST的程序实现更为简单且运行速度更快,它可以做到O(nlogn)的预处理,O(1)回答每个询问(ST的原理其实是动态规划

使用ST的条件是没有修改操作,因此ST适用于没有修改操作且询问次数较多(10^6级甚至更大)的情况

  • 代码Code:
/* ST表 */
#include <bits/stdc++.h>
using namespace std;
#define maxn 1000007
long long n,m,j,a[maxn],Max[maxn<<1],Min[maxn<<1];

int main() {
	scanf("%lld%lld",&n,&m);
	for(register long long i=1;i<=n;i++) {
		scanf("%lld",&a[i]);
		Max[i]=Min[i]=a[i];
	}
	for(j=1;j*2<=m;j*=2) {
		for(register long long i=1;i+j<=n;i++) {
			Min[i]=min(Min[i+j],Min[i]);
			Max[i]=max(Max[i+j],Max[i]);
		}
	}
	for(register long long i=1;i+m-1<=n;i++) {
		printf("%lld ",min(Min[i],Min[i+m-j]));
	}
	puts("");
	for(register long long i=1;i+m-1<=n;i++) {
		printf("%lld ",max(Max[i],Max[i+m-j]));
	}
	return 0;
} 

吐槽一下:

(1)这道题标签有线段树,于是就先打的线段树,结果...T到飞起(其实还好)数据貌似是随机的,所以线段树的分大概是60-80pts

看其他人的题解,加一些剪枝好像就过了,但是优化后时间复杂度也并不优越,所以就不在这里介绍了(还是因为我懒

(2)这题还能用模拟做,就是万能的for循环再加一些其他的比如sort...之类的,也能A掉,这里也不介绍了,大家可以下去研究qwq


以上是关于滑动窗口/模板单调队列 题解的主要内容,如果未能解决你的问题,请参考以下文章

滑动窗口模板题(对读写性能要求贼高)

Acwing 154 滑动窗口(单调队列)经典模板

P1886 滑动窗口 /模板单调队列

滑动窗口

ybtoj 单调队列课堂过关luogu P1886例题1滑动窗口

[模板]单调队列