[算法学习笔记] 单调栈

学习完单调队列后,单调栈原理不难。关键在于应用。

实际上近几场 OI 赛制比赛中没有出现过单调栈。但还是要掌握一下的。

原理

和单调队列类似,只不过受栈的限制只能从一边出入。

假设我们初始栈如下:
image

假设我们需要维护这个栈从底到顶是单调递增的,我们接下来需要插入数字 3 ,显然需要把4 pop 掉。

就完了。

和单调队列同理的是,我们可能需要 pop 掉多个数,while 循环处理即可。

看着名字这么高大上,实际上这玩意不知道考场上自己就能手搓出来。

例题

image

题目很长,首先我们需要把题目看明白。翻译过来如上。

好多题解对本题都一笔带过,可能导致很多初学者难以理解。笔者会尽可能地详细讲解本题以使读者尽可能理解单调栈的内涵。

单调栈和单调队列从某种意义上类似,都有 维护对当前区间造成贡献的。对于本题,如果 \(h_i-h_j\) 都能看到,那么 \(h\) 在区间 \([i,j]\) 一定单调递减。即如果搜到一个不使得当前区间单调递减的数据则它及其以后的所有数都不可能对当前区间内的数造成贡献,直接 全 pop 掉即可。

具体实现,对于每个新的 \(h_j\) 如果 \(h_j < h_i\) ,则 \(ans + s.size()\) 即可。我们单调递减栈内维护的序列都是可以看到当前 \(h_j\) 的。统计答案后再将当前数入栈,这个顺序不能错,显然自己不能看到自己。

反之如果不满足单调递减,则原栈中所有元素所能看到的最大边界已经达到,他们的贡献已经计算完,全 pop 掉开新区间。

这样,我们线性地扫一遍单调递减栈,就可以把答案全部统计。若读者仍感觉很迷惑,建议手玩几组数据或者和单调队列结合理解。

本题单调栈原理如上,参考代码如下。

实现
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stack>
#define int long long
using namespace std;
const int N  = 100100;
int n;
int h[N];
int a[N];
int sum = 0;
stack <int> s;
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>h[i];
        while(!s.empty() && s.top() <= h[i])
        {
            s.pop();
        }
        sum += s.size();
        s.push(h[i]);
    }
    cout<<sum<<endl;
}

由于单调栈的考察实现是少,笔者水平有限没有合适的例题,若未来做到合适的例题可能会更新。

对于单调栈,我们应当深刻理解其思想,而不知略知一二,这样才能在考场上灵活应用。学习 OI 的过程也应当是这样的。很多时候我们注重刷题量,看了题解然后照着题解的思路写一遍,实际上可能自己并没有真正理解这个题。这样是无效的刷题。

posted @ 2024-01-25 00:16  SXqwq  阅读(23)  评论(0编辑  收藏  举报