引入
现在,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})
则有下列表格:
思考求最小值时,为什么队列中的数是单调递增的。我们发现,当新考虑一个数时,队列中比它大的数都会出队,因而在这个数之前的队列中的所有数都比它小,由于每次最多只有一个数出界,也就只用判断一次队首元素编号
如上面所说,在求最小值时,考虑每一个数,将它与队中元素比较,队中比这个数大的都出队(在当前范围内,比这个数大的一定不是该范围的最小值),这个数由于出现晚,即使比现在队伍中的数大,但会在屏幕上持续出现更久(这个数会出现在后面的范围),所以必须将他入队,若当前队首已出屏幕可查看的范围,将其出队,那么这个队列一定是单调递增的,这样得到的队首,正是该区间的最小值
单调队列的使用并不广泛,但对于某些题目有特别的效果
更新日志及说明
更新
- 初次完成编辑 - (2020.6.24)
本文若有更改或补充会持续更新