[算法入门]单调队列

引入

现在,KuaiD有一台电脑,他要完成一个任务。他拿到了一个只有10个数字的序列和两个数字\(x,y\),数列会完整的显示在电脑屏幕上,他要找出区间\([x,y]\)之间的最小值,由于KuaiD懒得很,他决定写代码解决这个问题

初次尝试

KuaiD沉思了一会,决定简单(粗暴)地解决这个问题,读入整个序列,从\(x\)\(y\)一一遍历,随时更新就行了,于是他写出了下面的代码:

#include <iostream>
#include <cstdio>
#define Max 0x3ffffff
using namespace std;

int a[11],x,y,minn = Max;

int main(){
	for (int i = 1;i <= 10;i ++)
	  cin >> a[i];
	cin >> x >> y;
	for (int i = x;i <= y;i ++)
	  minn = min(a[i],minn);
	cout << "Min:" << minn << endl;
	return 0;
}

思考·Ⅰ

KuaiD很轻易的解决了问题,但是他还有很充足的时间,他开始了胡思乱想。
“我又没有更能装13,或者更麻烦(?)的解决方法呢?”
他拿出了一个数据,开始尝试用一个队列解决,数列为:\(7,6,8,12,9,10,3,1,5,6\);\(x=3,y=7\);

KuaiD是这样想的

考虑每一个数时,先判断是否在范围之内,若在范围中,将它与队中元素比较,队中比这个数大的都出队,这个数由于出现晚,且必在范围内,将他入队(此时KuaiD觉得这样更加麻烦了,但他还是想分析下去)。

KuaiD开始了模拟

考虑第1个数:7,但7并不在要求的范围内,忽略,此时队列为空
考虑第2个数:6,但6并不在要求的范围内,忽略,此时队列为空
考虑第3个数:8,此时8在范围内,且队中为空,于是将8入队,此时队列为\(\{8\}\)
考虑第4个数:12,队列中8比12小,但12出现的晚,于是将12入队,此时队列为\(\{8,12\}\)
考虑第5个数:9,队列中12比9大,于是将12出队,9入队,此时队列为\(\{8,9\}\)
考虑第6个数:10,队列中8,9比10小,但10出现的晚,于是将10入队,此时队列为\(\{8,9,10\}\)
考虑第7个数:3,队列中8,9,10都比3大,于是将8,9,10出队,3入队,此时队列为\(\{3\}\)
后面的数超出了范围,必定都不入队,KuaiD发现,队列中总是一个单调上升队列,且队首元素即为当前范围内的最小值,故答案为3,KuaiD停止了思考,写出了以下代码:

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

int a[11],x,y,q[11];

int main(){
	for (int i = 1;i <= 10;i ++)
	  cin >> a[i];
	cin >> x >> y;
	int head = 1,tail = 0;
	for (int i = 1;i <= 10;i ++){
		if (i < x || i > y)
		  continue;
		while (head <= tail && q[tail] >= a[i])
		  tail --;
		tail ++;
		q[tail] = a[i];
	}
	cout << q[head];
	return 0;
}

任务升级

KuaiD刚打完代码,新的任务就到了,他拿到了一张图片,图片上有一个长度为\(n\)的数列,由于显示比例问题,KuaiD的电脑屏幕只能同时显示\(k\)个数,不知为何,并没有给出让KuaiD求什么。
KuaiD想了想,看时间还充沛,决定自己给自己出一个任务,当他从左向右查看图片时,每一时刻向后查看一个数,此时屏幕最左边的数会被隐藏,他想求出每一时刻屏幕上的最大值和最小值(KuaiD有一些特殊的手段能瞬间从图片中得到所需的信息)

暴力求解

KuaiD并不想耗费太多的脑力,所以他决定使用暴力的方法,对状态进行枚举;
片刻之后,他得到了以下代码:

#include<iostream>
using namespace std;

int n,k;
int a[3000003];
int f[3000003];

int main() {
	cin >> n >> k;
	int tot = 0;
	for(int i = 1;i <= n;i ++)
          cin >> a[i];
	for(int i = k;i <= n;i ++) {
		int maxn = -0x3fffff,minx = 0x3fffff;
		for(int j = i - k + 1;j <= i;j ++) {
			if(a[j] > maxn) maxn = a[j];
			if(a[j] < minx) minx = a[j];
		}
		cout << minx << " ";
		f[++ tot] = maxn;
	}
	cout << endl;
	for(int i = 1;i <= tot;i ++)
          cout << f[i] << " ";
        return 0;
}

思考·Ⅱ

KuaiD用上面的代码完成了几个数据的处理,可是他发现,这样的时间复杂度为\(O(n \cdot k)\),在处理大数据时会花费掉他很多时间,甚至耽搁他过会儿的行程,KuaiD可不想打乱自己看学习资源的计划。KuaiD开始思考优化。

KuaiD突然想到了自己无聊时的想法,能不能用队列进行优化?

在求最小值时,考虑每一个数,将它与队中元素比较,队中比这个数大的都出队(在当前范围内,比这个数大的一定不是该范围的最小值),这个数由于出现晚,即使比现在队伍中的数大,但会在屏幕上持续出现更久(这个数会出现在后面的范围),所以必须将他入队,若当前队首已出屏幕可查看的范围,将其出队,那么这个队列一定是单调递增的,这样得到的队首,不正是该区间的最小值吗?
求最大值同理

如何判断当前队首是否超出范围呢?每个数有一个编号,为\(1,2,...,n\),当前考虑的是第\(i\)个数,当第\(i\)个数进入屏幕后,最左侧为第\(i-k+1\)个数,也就是说,如果当前队首的编号小于等于\(i-k\),这个数必定是超出范围的。

于是,他得到了以下代码:

#include <iostream>
#include <cstdio>
#define Max 2000005
using namespace std;

struct Num{
	int index,x;  //index为数字编号,x为数字本身
};

int a[Max]; //原数列
Num q[Max]; //队列

int main(){
	int n,m;
	int front = 1,back = 0; //设置头指针和尾指针,当头指针大于尾指针是队列为空
	cin >> n >> m; //输入,不多bb
	for (int i = 1;i <= n;i ++)
	  cin >> a[i];
	for (int i = 1;i <= n;i ++){ //找最小值
		while (front <= back && q[back].x >= a[i]) 
                  //当队列不为空时,从队尾开始,凡是比这个数大的都出队,因为绝不是这个范围的最小值
		  back --;
		back ++;
		q[back].x = a[i];
		q[back].index = i; //这个数是必定入队的,自己想想为什么
		if (front <= back){
			if (q[front].index + m <= i) //如果当前队首的数字出了范围
			  front ++;
			if (i >= m) 
                        //即使是第一个范围,也是同时显示k(m)个数,而处理第1~k-1数时,队列中有数但并不是所求范围的解
			  cout << q[front].x << " "; //当前队首即为当前范围的解
		}
	}
	cout << endl;
	front = 1;
	back = 0; //记得需要清空队列
	for (int i = 1;i <= n;i ++){ //求最大值,同上
		while (front <= back && q[back].x <= a[i])
		  back --;
		back ++;
		q[back].x = a[i];
		q[back].index = i;
		if (front <= back){
			if (q[front].index + m <= i)
			  front ++;
			if (i >= m)
			  cout << q[front].x << " ";
		}
	}
	return 0;//完美结束
}

通过计算发现这样的时间复杂度竟然只有\(O(n)\)!
KuaiD定睛一看,这不就是单调队列吗!

单调队列说明

以这个数据为例:\(n=8,k=3\)
\(a[]=\{1,3,-1,-3,5,3,6,7\}\)
则有下列表格:
Markdown
思考求最小值时,为什么队列中的数是单调递增的。我们发现,当新考虑一个数时,队列中比它大的数都会出队,因而在这个数之前的队列中的所有数都比它小,由于每次最多只有一个数出界,也就只用判断一次队首元素编号

如上面所说,在求最小值时,考虑每一个数,将它与队中元素比较,队中比这个数大的都出队(在当前范围内,比这个数大的一定不是该范围的最小值),这个数由于出现晚,即使比现在队伍中的数大,但会在屏幕上持续出现更久(这个数会出现在后面的范围),所以必须将他入队,若当前队首已出屏幕可查看的范围,将其出队,那么这个队列一定是单调递增的,这样得到的队首,正是该区间的最小值

单调队列的使用并不广泛,但对于某些题目有特别的效果
以上是本人无知时提出的暴论。。。单调队列对某些 DP 具有很好的优化效果

更新日志及说明

更新

  • 初次完成编辑 - \(2020.6.24\)
    本文若有更改或补充会持续更新

个人主页

欢迎到以下地址支持作者!
Github戳这里
Bilibili戳这里
Luogu戳这里

posted @ 2020-06-24 14:40  Dfkuaid  阅读(775)  评论(1编辑  收藏  举报