zoukankan      html  css  js  c++  java
  • 单调栈及单调队列

    单调栈和单调队列的关系

    单调栈和单调队列的本质,顾名思义,就是单调:利用单调性来解决一些问题。

    由于所有元素只会入栈/队1次,所以其复杂度为O(n)

    单调队列是单调栈的升级版

    单调栈

    单调栈就是一个栈,栈中元素有单调的特性。我们向栈中加入元素时,依照单调性,弹出加入新元素后不符合单调性的元素,从而维护栈的单调。

    举个例子 单调栈S:1 2 4 9 想要加入的元素是 3

    那么我们先弹出 9 再弹出 4 最后把 3 加在栈顶,一个操作就完成了

    这时 S:1 2 3 维护了其单调性

    那么利用单调栈可以解决什么问题呢?

    P1901 发射站

    题目描述

    某地有 N 个能量发射站排成一行,每个发射站 i 都有不相同的高度 Hi,并能向两边(当 然两端的只能向一边)同时发射能量值为 Vi 的能量,并且发出的能量只被两边最近的且比 它高的发射站接收。

    显然,每个发射站发来的能量有可能被 0 或 1 或 2 个其他发射站所接受,特别是为了安 全,每个发射站接收到的能量总和是我们很关心的问题。由于数据很多,现只需要你帮忙计 算出接收最多能量的发射站接收的能量是多少。

    输入输出格式

    输入格式:
    第 1 行:一个整数 N;

    第 2 到 N+1 行:第 i+1 行有两个整数 Hi 和 Vi,表示第 i 个人发射站的高度和发射的能量值。

    输出格式:
    输出仅一行,表示接收最多能量的发射站接收到的能量值,答案不超过 longint。


    分析一下数据范围:我们发现只有O(n)的复杂度才可以过这题

    因为能量只能被最近的比他高的雷达站吸收,所以我们构建一个单调栈:对于每一个信号塔(元素),它发射的电波能被最近的比他高的接受,所以加入这个信号塔(元素)入栈时,弹出高度比他小的(因为弹出的矮,起码比新信号塔矮,所以有能量也只会传到新元素,至少不会是它,所以我们可以将其弹出),完成弹出之后,将能量加到第一个比他高的(也就是此时的栈顶),最后将新元素加到栈里,这次操作就完成了

    对于这题,要从左到右,从右到左做两遍,具体依照题意

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #define ll long long
    using namespace std;
    int RD(){
        int out = 0,flag = 1;char c = getchar();
        while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
        while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
        return flag * out;
        }
    const int maxn = 10010010;
    int num;
    ll sum[maxn];
    struct S{int h;int v;}I[maxn];
    struct Que{int index,h;}que[maxn];
    
    void getmax(){//做两遍单调栈累计能量
        int tail = 0;
        for(int i = 1;i <= num;i++){
            while(tail > 0 && I[i].h >= que[tail].h)tail--;
            if(tail != 0){
                sum[que[tail].index] += I[i].v;
                }
            que[++tail].h = I[i].h;
            que[tail].index = i;
            }
        }
    void getanothermax(){
        int tail = 0;
        for(int i = num;i >= 1;i--){
            while(tail > 0 && I[i].h >= que[tail].h)tail--;
            if(tail != 0){
                sum[que[tail].index] += I[i].v;
                }
            que[++tail].h = I[i].h;
            que[tail].index = i;
            }
        }
    int main(){
        num = RD();
        for(int i = 1;i <= num;i++){
            I[i].h = RD();
            I[i].v = RD();
            }
        getmax();
        getanothermax();
        ll ans = -1;
        for(int i = 1;i <= num;i++){
            ans = max(ans,sum[i]);
            }
        cout<<ans<<endl;
        return 0;
        }
    

    单调队列

    前面提到过,单调队列是单调栈的升级版。单调队列是有限制的单调栈。

    试想向一个队列里加入元素:元素的加入除了大小关系,肯定有先后之分。若题意要求我们按一定规则弹出旧元素,这时我们就得用到单调队列了。

    比如说最典型的问题:区间长度确定的最值求解问题

    P2032 扫描

    题目描述

    有一个 1 ∗ n 的矩阵,有 n 个正整数。

    现在给你一个可以盖住连续的 k 的数的木板。

    一开始木板盖住了矩阵的第 1 ∼ k 个数,每次将木板向右移动一个单位,直到右端与

    第 n 个数重合。

    每次移动前输出被覆盖住的最大的数是多少。

    输入输出格式

    输入格式:
    从 scan.in 中输入数据

    第一行两个数,n,k,表示共有 n 个数,木板可以盖住 k 个数。

    第二行 n 个数,表示矩阵中的元素。

    输出格式:
    输出到 scan.out 中

    共 n − k + 1 行,每行一个正整数。

    第 i 行表示第 i ∼ i + k − 1 个数中最大值是多少。


    直接是区间最值得模板

    因为题意需要我们弹出比较旧的元素,所以我们给每个元素除了值之外的另一个属性:序号

    队尾加入新元素操作与单调栈相同,升级了的是:在每次新元素插入后,利用元素的序号判断队首元素是否需要弹出

    以这题为例:每次加入新的元素(方法同单调栈),然后判断队首是否应该被弹出(若新元素的序号 - 队首的序号 > k 则弹出)

    (注意,不同的题目的弹出队首方法依题意不同,具体请审题)

    通过分析可以看到:这里的单调队列并不是一个真的队列,他可以双头弹出,是个双端队列(不过这里没用到其全部功能,双端队列可以将元素插到队首),是可以用来玄学优化SPFA的,并且有STL里的deque可以实现,这里就不加赘述了。

    #include<iostream>
    #include<algorithm>
    #include<cstdio>
    using namespace std;
    int RD(){
        int out = 0,flag = 1;char c = getchar();
        while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
        while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
        return flag * out;
        }
    const int maxn = 2000100;
    struct MAX{int index,v;}que[maxn];
    int a[maxn];
    int num,k;
        
    void getmax(){
        int head = 1,tail = 0;
        for(int i = 1;i <= k;i++){
            while(head <= tail && a[i] >= que[tail].v)tail--;
            que[++tail].v = a[i];//赋值
            que[tail].index = i;//给与其序号
            }
        for(int i = k + 1;i <= num;i++){
            printf("%d
    ",que[head].v);
            while(head <= tail && a[i] >= que[tail].v)tail--;//操作同单调栈
            que[++tail].v = a[i];
            que[tail].index = i;
            while(que[head].index <= i - k)head++;//是否弹出队首
            }
        printf("%d
    ",que[head].v);
        }
        
    int main(){
        num = RD();k = RD();
        for(int i = 1;i <= num;i++)a[i] = RD();
        getmax();
        return 0;
        }
    

    类似的题目还有

    P1440 求m区间内的最小值

    P3088 [USACO13NOV]挤奶牛Crowded Cows

    P2251 质量检测

    P2947 [USACO09MAR]向右看齐Look Up


    带有技巧的单调队列

    有些题目单用单调队列是不能解决问题的,看这一题:

    P1714 切蛋糕

    题目描述

    今天是小Z的生日,同学们为他带来了一块蛋糕。这块蛋糕是一个长方体,被用不同色彩分成了N个相同的小块,每小块都有对应的幸运值。

    小Z作为寿星,自然希望吃到的第一块蛋糕的幸运值总和最大,但小Z最多又只能吃M小块(M≤N)的蛋糕。

    吃东西自然就不想思考了,于是小Z把这个任务扔给了学OI的你,请你帮他从这N小块中找出连续的k块蛋糕(k≤M),使得其上的幸运值最大。

    输入输出格式

    输入格式:
    输入文件cake.in的第一行是两个整数N,M。分别代表共有N小块蛋糕,小Z最多只能吃M小块。

    第二行用空格隔开的N个整数,第i个整数Pi代表第i小块蛋糕的幸运值。

    输出格式:
    输出文件cake.out只有一行,一个整数,为小Z能够得到的最大幸运值。


    这是一道好题(认真脸)

    刚开始看的时候,觉得一个单调队列就可以解决了,后来死活过不了样例,再仔细看题发现:是最多吃M块蛋糕,考虑到蛋糕有负的幸运值,吃完M块不一定就是幸运值最大的方案。

    那么怎么办呢?

    思考一下,怎么样才能得到Fmax呢?我们可以预处理一下前缀和:sum[1] ~ sum[n],因为对于某块蛋糕来说,其前缀和是固定的,那么Fmax不就等于sum[i] - (不超过范围,满足M块这一条件的)sum[j]min了吗?

    然后就开始明朗的:利用前缀和的思想求解前缀和,再利用单调队列求某一区间内前缀和的最小值,相减即为本次的答案,对于每块蛋糕都有一个不超过M块的最大值,每次更新一下求最大的最大就行了

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    using namespace std;
    int RD(){
        int flag = 1,out = 0;char c;c = getchar();
        while(c < '0' || c > '9'){if(c == '-')flag = -1;c = getchar();}
        while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
        return flag * out;
        }
    const int maxn = 500100;
    struct Que{int index,v;}que[maxn];
    int num,a[maxn],k;
    int ans = -999999999;
    
    void getmax(){
        int head = 1,tail = 0;
        for(int i = 1;i <= num;i++){
            while(head <= tail && a[i] <= que[tail].v)tail--;
            que[++tail].v = a[i];
            que[tail].index = i;
            while(i - k > que[head].index)head++;
            ans = max(ans,a[i] - que[head].v);//队首元素的值即为区间最小值
            }
        }
    
    int main(){
        num = RD();k = RD();
        int temp;
        for(int i = 1;i <= num;i++){
            temp = RD();
            a[i] = a[i - 1] + temp;
            }
        getmax();
        cout<<ans<<endl;
        return 0;
        }
    
  • 相关阅读:
    低代码:时代的选择
    AI+云原生,把卫星遥感虐的死去活来
    网络货运平台要智能,安全的数据底座少不了
    基于昇腾CANN的卡通图像生成可在线体验啦!十分钟带你了解CANN应用开发全流程
    什么是强化学习?
    高可用架构演进之单元化
    AOC萌新探索:搭建和体验在线AOC环境
    如何将知识引入机器学习模型提升泛化能力?
    零代码以“王者荣耀”为例解析设计七原则
    从零开始搭建前端脚手架
  • 原文地址:https://www.cnblogs.com/Tony-Double-Sky/p/9283238.html
Copyright © 2011-2022 走看看