首页 > 其他分享 >单调栈

单调栈

时间:2024-10-05 08:49:39浏览次数:1  
标签:头牛 int top pop leq 单调

单调栈是一种内部元素具有单调性的栈,可以解决与“以某个值为最值的最大区间”等问题。

例题:P2866 [USACO06NOV] Bad Hair Day S

有 \(N \ (1 \le N \le 80000)\) 头奶牛,第 \(i\) 头牛的身高为 \(h_i \ (1 \le h_i \le 10^9)\)。每只奶牛往右边看,可以看到严格小于它身高的牛的头顶,直到看到了身高不小于它的牛(看不到这头牛的头顶),或者右边没有其他奶牛为止。第 \(i\) 头牛可以看到的头顶数量为 \(C_i\),求 \(\sum_{i=1}^n C_i\)。假设有 \(6\) 头牛,高度分别为 \(10,3,7,4,12,2\),那么它们分别可以看到 \(3,0,1,0,1,0\) 头牛的头发,其总和为 \(5\)。

分析:如果使用暴力枚举求解,对于每一头牛都往右边枚举身高小于它的牛,那么时间复杂度是 \(O(n^2)\) 的,无法通过本题。

可以转变一下思路,求每头牛能看见几头牛,等价于计算每头牛能被多少其他牛看见。一头牛能被哪些牛看见?左边比它高的牛,再左边比它高的牛,……。可以维护一个序列,满足这个序列里存的都是对于第 \(i\) 头牛来说,其左边的身高比它高的牛的身高(这会是一个单调递减的序列),可以使用栈来进行维护。

最开始栈是空的。身高为 \(10\) 的牛进栈,原来栈里没有牛,那么答案增加 \(0\);身高为 \(3\) 的牛来了,则需要从栈顶开始,把所有小于等于 \(3\) 的牛都出栈,此时左边剩 \(1\) 头牛,答案增加 \(1\),然后再将其入栈;身高为 \(7\) 的牛来了,从栈顶开始,把所有小于等于 \(7\) 的牛都出栈(比如刚才入栈的 \(3\) 就要出栈),此时左边有 \(1\) 头牛,答案增加 \(1\),然后再将其入栈,……,以此类推。

image

像这样维护一个值单调递减(或者递增)的数据结构,称为单调栈

参考代码
#include <cstdio>
#include <stack>
using std::stack;
using ll = long long;
const int N = 80005;
int h[N];
int main()
{
    int n; scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
    stack<int> s;
    ll ans = 0; // 极端情况下,答案是n的平方级别,超过了int范围,需要long long类型
    for (int i = 1; i <= n; i++) {
        while (!s.empty() && s.top() <= h[i]) s.pop();
        ans += s.size();
        s.push(h[i]);
    }
    printf("%lld\n", ans);
    return 0;
}

在本题中,单调栈能以 \(O(n)\) 的时间复杂度找到每一个元素右边第一个比它大的数字(就是把这个数字挤出栈的数字)。

例题:SP1805 HISTOGRA - Largest Rectangle in a Histogram

如果矩形的高度从左到右递增,那么答案是多少?显而易见,可以尝试以每个矩形的高度作为最终矩形的高度,并把宽度延伸到右边界,得到一个矩形,在所有这样的矩形面积中取最大值就是答案。

受这种思路启发,如果能够知道以每一个高度的矩形作为最终矩形的高度向左、向右最多能延伸到哪,则这一段之间的就是最终矩形的宽度,在所有这样的矩形面积中取最大的。而向左、向右最多能延伸到哪实际上就是找每个高度的左边、右边第一个高度低于自己的位置。这一点可以通过单调栈实现。

参考代码
#include <cstdio>
#include <stack>
#include <algorithm>
using std::max;
using std::stack;
using ll = long long;
const int N = 100005;
int h[N], l[N], r[N];
int main()
{
    while (true) {
        int n; scanf("%d", &n);
        if (n == 0) break;
        for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
        stack<int> s;
        for (int i = 1; i <= n; i++) {
            while (!s.empty() && h[s.top()] > h[i]) {
                r[s.top()] = i; s.pop();
            }
            s.push(i);
        }
        while (!s.empty()) {
            r[s.top()] = n + 1; s.pop();
        }
        for (int i = n; i >= 1; i--) {
            while (!s.empty() && h[s.top()] > h[i]) {
                l[s.top()] = i; s.pop();
            }
            s.push(i);
        }
        while (!s.empty()) {
            l[s.top()] = 0; s.pop();
        }
        ll ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = max(ans, 1ll * h[i] * (r[i] - l[i] - 1));
        }
        printf("%lld\n", ans);
    }
    return 0;
}

例题:P1950 长方形

小明今天突发奇想,想从一张用过的纸中剪出一个长方形。
为了简化问题,小明做出如下规定:
(1)这张纸的长宽分别为 \(n,m\)。小明将这张纸看成是由\(n \times m\)个格子组成,在剪的时候,只能沿着格子的边缘剪。
(2)这张纸有些地方小明以前在上面画过,剪出来的长方形不能含有以前画过的地方。
(3)剪出来的长方形的大小没有限制。
小明看着这张纸,想了好多种剪的方法,可是到底有几种呢?小明数不过来,你能帮帮他吗?

输入格式

第一行两个正整数 \(n,m\),表示这张纸的长度和宽度。
接下来有 \(n\) 行,每行 \(m\) 个字符,每个字符为 * 或者 .
字符 * 表示以前在这个格子上画过,字符 . 表示以前在这个格子上没画过。

输出格式

仅一个整数,表示方案数。

样例输入

6 4
....
.***
.*..
.***
...*
.***

样例输出

38

数据规模

对 \(10\%\) 的数据,满足 \(1\leq n\leq 10,1\leq m\leq 10\)
对 \(30\%\) 的数据,满足 \(1\leq n\leq 50,1\leq m\leq 50\)
对 \(100\%\) 的数据,满足 \(1\leq n\leq 1000,1\leq m\leq 1000\)

分析:本题可以通过枚举矩形的四条边,然后判断里面是否全是没有画过的部分,但这样时间复杂度很高,所以需要优化效率。

以行为单位处理,统计以每一行为底边的句型数量。举个例子,假设目前在统计第 \(4\) 行。

image

令 \(h_i\) 表示第 \(i\) 列从底往上延伸空格子的数量,\(l_i\) 表示这一列左边第一个满足 \(h\) 值小于等于 \(h_i\) 的列号(如果没有的话就是 \(0\)),\(r_i\) 表示这一列右边第一个满足 \(h\) 值小于 \(h_i\) 的列号(如果没有的话就是 \(m+1\))。

根据乘法原理,包括这一列最底下的小格子,同时又被这一列的高度限制的长方形的数量是 \((i - l_i) \times (r_i - i) \times h_i\)。为什么 \(l\) 和 \(r\) 的计算一个带等号另一个不带等号,因为这样计数才不会重复不会遗漏(思考为什么?)。每一行都用这种方式处理,然后将每一行的结果汇总就得到了整个问题的结果。

\(h_i\) 如何计算?如果这个格子是画过的格子,则 \(h_i = 0\),否则 \(h_i\) 的值就是同一列上一行的 \(h_i\) 的值再加 \(1\)。

如何求得 \(l_i\) 和 \(r_i\) 呢?使用单调栈。求 \(r_i\) 需要找到严格小于的,那么判断是否将栈顶元素挤出时,如果待处理的元素等于栈顶元素时不必出栈,只有遇到更小的才会被挤出来,满足栈是单调不减的;而求 \(l_i\) 需要找到小于等于的,则从后往前计算,判断是否将栈顶元素挤出时,如果这个元素小于等于栈顶元素,那么栈顶元素就会被挤出,满足栈是单调递增的。

参考代码
#include <cstdio>
#include <stack>
using std::stack;
using ll = long long;
const int N = 1005;
char ch[N][N];
int h[N][N], l[N], r[N];
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        scanf("%s", ch[i] + 1);
        for (int j = 1; j <= m; j++) {
            h[i][j] = ch[i][j] == '*' ? 0 : h[i - 1][j] + 1;
        }
    }
    ll ans = 0; // 需要考虑极端情况:n=m=1000,而且全是没画过的格子,长方形的数量是n的4次方数量级
    for (int i = 1; i <= n; i++) {
        // 为了方便在出栈时求出每个元素左边和右边的符合要求的位置
        // 栈里实际存储的是下标而不是元素本身
        stack<int> s;
        // 顺着求右边第一个小于这个数的位置
        for (int j = 1; j <= m; j++) {
            while (!s.empty() && h[i][s.top()] > h[i][j]) {
                r[s.top()] = j; s.pop();
            }
            s.push(j);
        }
        while (!s.empty()) {
            r[s.top()] = m + 1; s.pop();
        }
        // 倒着求左边第一个小于等于这个数的位置
        for (int j = m; j >= 1; j--) {
            while (!s.empty() && h[i][s.top()] >= h[i][j]) {
                l[s.top()] = j; s.pop();
            }
            s.push(j);
        }
        while (!s.empty()) {
            l[s.top()] = 0; s.pop();
        }

        for (int j = 1; j <= m; j++) {
            ans += 1ll * h[i][j] * (j - l[j]) * (r[j] - j);
        }
    }
    printf("%lld\n", ans);
    return 0;
}

标签:头牛,int,top,pop,leq,单调
From: https://www.cnblogs.com/ronchen/p/18447219

相关文章

  • [JSOI2008] 最大数 (单调栈)
    算法基础发现插入总在最后一个进行单调栈维护一个区间的\(max/min\)单调队列维护以一个值为\(max/min\)的最大区间显然可以使用单调栈维护其原理为当\(a,b\inseq,a<b,pos[a]<pos[b]\)那么显然\(a\)没有卵用因此可以用单调栈维护一个包含\(seq\)的......
  • 单调队列优化 DP
    单调队列可以\(O(n)\)求出子区间的最值,且顺序上要求区间的左端点和右端点单调不降。引入P1886滑动窗口/【模板】单调队列定义一个队列\(q\),我们让\(q\)中的元素满足单调不降。因此当\(x\)入队时,从前往后让不在当前范围的元素出队,从后往前将\(<x\)的元素全部出队,然......
  • 单调队列优化DP解题报告(uoj转)
    Fence\(K\)很小,考虑\(K\)开一维,\(N\)开一维\(f_{i,j}\)表示前\(i\)个工匠粉刷前\(j\)个木板的最大花费\(f_{i,j}=\min_{k=j-l_i}^{s_i-1}f_{i-1,k}+(j-k)\cdotp_i\)拆开为\(f_{i,j}=f_{i-1,k}-k\cdotp_i+j\cdotp_i\)\(i\)固定时维护\(f_{i-1,k}-k\cdotp_i\)的最小......
  • 单调栈 详解+例题
    昨天打AT碰到了一道单调栈的题,于是来复习一下单调栈栈内元素单调性有单调递增栈和单调递减栈实现:举个例子:假设入栈序列为142893要模拟一个单调递增栈:\(i=1\)时,栈为空,\(1\)入栈后仍然保持单调性,将\(1\)入栈;\(i=2\)时,栈顶元素为\(1\),\(4\)入栈后......
  • 单调栈-滑动窗口
    830.单调栈模板题:给定一个长度为N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出−1。输入格式第一行包含整数N,表示数列长度。第二行包含N个整数,表示整数数列。输出格式共一行,包含NN个整数,其中第i个数表示第i个数的左边第一个比它小的数,如果不存......
  • 高等数学 3.4 函数的单调性与曲线的凹凸性
    目录一、函数单调性的判定法二、曲线的凹凸性与拐点一、函数单调性的判定法定理1设函数\(y=f(x)\)在\([a,b]\)上连续,\((a,b)\)内可导。(1)如果在\((a,b)\)内\(f^{'}(x)\geqslant0\)且等号仅限在有限多个点处成立,那么函数\(y=f(x)\)在\([a,b]\)上单调增......
  • 滑动窗口+单调队列
    题目:2398.预算内的最多机器人数目答案:#fromtypingimportList#fromcollectionsimportdequeclassSolution:defmaximumRobots(self,chargeTimes:List[int],runningCosts:List[int],budget:int)->int:res,n,runningCostSum=0,len(chargeTi......
  • HISTOGRA - 最大矩形面积(单调栈)
    include<bits/stdc++.h>usingnamespacestd;definexfirstdefineysecondtypedefpair<int,int>PII;typedeflonglongll;typedefunsignedlonglongull;typedefunsignedintuint;typedefvectorVS;typedefvectorVI;typedefvector<vect......
  • 单调队列优化 DP
    单调队列优化DP回忆单调队列的作用,\(O(n)\)求出每一个大小为\(K\)的窗口中的最大、最小值。以最大值为例,我们可以得到如下DP转移方程:\[dp[i]=\max(val[j])+base[i],i-j\leqK\]其中\(base[i]\)是一个仅与\(i\)有关的式子,不受\(j\)影响,且可以预处理得到;而\(val[j]......
  • 单调队列优化 dp
    1.概念单调队列优化的本质是借助单调性,及时排除不可能的决策,保持候选集合的秩序性。2.例题P1714切蛋糕题目大意:给定一个序列,找出长度不超过\(m\)的连续子序列,使得子序列中所有数的和最大。思路:要求区间和,首先求出前缀和,然后考虑朴素dp,不难想到用\(dp[i]\)表示包含......