单调栈及单调队列

单调栈和单调队列的关系

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

由于所有元素只会入栈/队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\n",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\n",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;
    }

猜你喜欢

转载自www.cnblogs.com/Tony-Double-Sky/p/9283238.html