• [算法入门]单调队列


    引入

    现在,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
    思考求最小值时,为什么队列中的数是单调递增的。我们发现,当新考虑一个数时,队列中比它大的数都会出队,因而在这个数之前的队列中的所有数都比它小,由于每次最多只有一个数出界,也就只用判断一次队首元素编号

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

    单调队列的使用并不广泛,但对于某些题目有特别的效果

    更新日志及说明

    更新

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

    个人主页

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

  • 相关阅读:
    combiner中使用状态模式
    了解Shell
    优秀工具推荐
    linux安装weblogic10.3
    FastDFS上传下载(上)
    java压缩工具类
    06链表
    05数组
    04时间复杂度
    03复杂度
  • 原文地址:https://www.cnblogs.com/Dfkuaid-210/p/13184367.html
Copyright © 2020-2023  润新知