浅谈单调队列解决滑动窗口问题

旧街凉风 提交于 2020-01-31 04:17:14

这次我们了解一下滑动窗口的问题
作者ID:qq_43341171


首先,让我们了解一下滑动窗口是什么?
这里有一张图(来自POJ),解释了滑动窗口的意思:
滑动窗口
我们可以看见,一个长度固定为3的框(窗口)从左端点移动到右端点,每次移动一个数,这就是我们所说的“滑动窗口”。
每次滑动窗口的最大值以及最小值就指的是滑动窗口在每次移动后区间内的最大值以及最小值。
例如:上图当中,第一次移动后(第一排),窗口区间包含了:“1,3,-1”。那么滑动窗口的长度为3(有3个数),在这3个数当中,最大值为3,最小值为-1,所以这个滑动窗口的最大值为3,最小值为-1。


了解了滑动窗口,让我们再来了解一下单调队列

单调队列,即单调递减或单调递增的队列。

但是,这个说法很笼统,到底什么是单调队列呢?
这里有张我做的图:
单调队列
从这张图里,我们能看出:

  • 单调递增队列(从队首到队尾递增)的维护方法:
    入队一个数,就把它前面所有比它大的数给弹掉
    准确地说,是把前面所有比它大的数给弹掉再入队
  • 单调递减队列(从队首到队尾递减)的维护方法:
    入队一个数,就把它前面所有比它小的数给弹掉
    准确地说,是把前面所有比它小的数给弹掉再入队
    但是问题来了:单调队列是如何把后面的数给弹掉的呢?(我们众所周知,队列只能在队首弹出)
    答案(敲黑板):其实单调队列的两端都可以弹出!!!
    那么这样,单调队列的问题就解决啦!
    下面是单调队列的模板:
#include<bits/stdc++.h>
using namespace std;
int n,a[1000005],Max[1000005],Min[1000005],Maxhead=1,Maxtail=0,Minhead=1,Mintail=0;
//这里我们定义两个单调队列Max与Min,Maxhead是Max的头指针,Maxtail是Max的尾指针,Min也一样 
int main()
{
	cin>>n;//n为数据个数 
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	//读入 
	for(int i=1;i<=n;i++)//单调递增队列 
	{
		while((Minhead<=Mintail)/*检测是否溢出*/&&(a[i]<=a[Min[Mintail]])/*检测前面比a[i]大的*/)
		{
			Mintail--;
		}
		Min[++Mintail]=i;//入队 
	}
	for(int i=1;i<=n;i++)//单调递减队列 
	{
		while((Maxhead<=Maxtail)/*检测是否溢出*/&&(a[i]>=a[Max[Maxtail]])/*检测前面比a[i]小的*/)
		{
			Maxtail--;
		}
		Max[++Maxtail]=i;//入队 
	}
	cout<<a[Min[Minhead]]<<endl<<a[Max[Maxhead]];
	return 0;
}

需要注意以上代码单调队列存的是数据的编号,并非数据!!!


单调队列以及滑动窗口都讲完了,接下来为大家讲一道题目,这道题就是典型的用单调队列解决滑动窗口的问题
【传送门】P1886 滑动窗口 /【模板】单调队列

如果打不开请复制网址:
https://www.luogu.com.cn/problem/P1886

这道题就是叫我们求滑动窗口的最大值以及最小值
暴力方法其实很简单,这就不再赘述
但暴力方法的确能得40分左右

我们再借用这张图分析:
滑动窗口
我们可以注意到一个细节,在第二行至第四行当中,最小值都是-3,那是因为-3在这3个滑动窗口中一直保持最小。但为什么第五行-3不是最小值了呢? 原因很简单:因为-3不在那个窗口之中了

但我们可以借此作为题目的突破口,我们可以总结出一个规律:

每一个数据入队时,把已经不在窗口之中的数据出队;并且将队列的最大/小值更新

那么,我们把题目转换成了两个问题:

  • 怎么将队列的最大/小值更新
    这个问题比较简单,我们为了省时间,不用每个窗口一一遍历,而用单调队列每次将最大/小值更新。那么队首就是最大/小值。
  • 怎么判断数据是否在窗口之中
    这个问题也比较简单,设数据的编号(数组下标)为n,窗口长度为k,窗口的左端点编号为left,那么,如果n+k<=i成立,那么数据在窗口之中,否则不在

那么这两个问题都解决后,我们就可以写代码了。
建议一些朋友再看一看解题思路哦!
上代码!!!

#include<bits/stdc++.h>
using namespace std;
int n,k,a[1000005],Max[1000005],Min[1000005],Maxhead=1,Maxtail=0,Minhead=1,Mintail=0;
//这里我们定义两个单调队列Max与Min,Maxhead是Max的头指针,Maxtail是Max的尾指针,Min也一样 
template<class T>inline void read(T &res)//快读,不加没关系 
{
	char ch;
	T flag=1;
	while(!isdigit(ch=getchar()))
	{
		if(ch=='-')
		{
			flag=-1;
		}
	}
	res=int(ch-'0');
	while(isdigit(ch=getchar()))
	{
		res=res*10+int(ch-'0');
	}
	res*=flag;
}
int main()
{
	read(n);read(k);
	for(int i=1;i<=n;i++)
	{
		read(a[i]);
	}
	//读入 
	for(int i=1;i<=n;i++)//求滑动窗口中的最小值 
	{
		while((Minhead<=Mintail)/*检测是否溢出*/&&(Min[Minhead]+k<=i)/*检测有没有超出窗口范围*/) 
		{
			Minhead++;
		}
		while((Minhead<=Mintail)/*检测是否溢出*/&&(a[i]<=a[Min[Mintail]])/*检测前面比a[i]大的*/)
		{
			Mintail--;
		}
		Min[++Mintail]=i;//入队 
		if(i>=k)//i<k时,滑动窗口长度没到k,所以不能输出 
		{
			printf("%d ",a[Min[Minhead]]);
		}
	}
	printf("\n");
	for(int i=1;i<=n;i++)//求滑动窗口中的最大值
	{
		while((Maxhead<=Maxtail)/*检测是否溢出*/&&(Max[Maxhead]+k<=i)/*检测有没有超出窗口范围*/)
		{
			Maxhead++;
		}
		while((Maxhead<=Maxtail)/*检测是否溢出*/&&(a[i]>=a[Max[Maxtail]])/*检测前面比a[i]小的*/)
		{
			Maxtail--;
		}
		Max[++Maxtail]=i;//入队 
		if(i>=k)//i<k时,滑动窗口长度没到k,所以不能输出
		{
			printf("%d ",a[Max[Maxhead]]);
		}
	}
	return 0;
}

那么,学完这些东西以后,建议大家趁热打铁,再做一道题:
【传送门】P1440 求m区间内的最小值

如果打不开请复制网址:
https://www.luogu.com.cn/problem/P1440

那么这道题跟上一道思路差不多,就不讲解了,给一个AC代码吧!

#include<bits/stdc++.h>
using namespace std;
int a[2000005],Min[2000005],Minhead=1,Mintail=0,n,m;
template<class T>inline void read(T &res)//快读,不加没关系
{
	char ch;
	T flag=1;
	while(!isdigit(ch=getchar()))
	{
		if(ch=='-')
		{
			flag=-1;
		}
	}
	res=int(ch-'0');
	while(isdigit(ch=getchar()))
	{
		res=res*10+int(ch-'0');
	}
	res*=flag;
}
int main()
{
	read(n);read(m);
	for(int i=1;i<=n;i++)
	{
		read(a[i]);
	}
	printf("0\n");
	for(int i=1;i<n;i++)
	{
		while((Minhead<=Mintail)&&(Min[Minhead]+m<=i))
		{
			Minhead++;
		}
		while((Minhead<=Mintail)&&(a[i]<=a[Min[Mintail]]))
		{
			Mintail--;
		}
		Min[++Mintail]=i;
		printf("%d\n",a[Min[Minhead]]);
	}
	return 0;
}

本次题解到此结束,谢谢您的观看!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!