单调栈详解

 

单调递增或单调减的栈,跟单调队列差不多,但是只用到它的一端,利用它可以用来解决一些ACM/ICPC和OI的题目,如RQNOJ 的诺诺的队列等。

单调栈是一种特殊的栈,特殊之处在于栈内的元素都保持一个单调性。 
假设下图是一个栈内元素的排列情况(单调递增的栈):

此时插入情况有两种: 
(1).插入元素大于栈顶元素 
当插入7时,因7 > 6,满足单调递增的条件,故可以直接加入栈 
此时: 


(2).插入的元素小于栈顶元素 
当插入3时,为了满足单调递增栈的性质,需要先将栈顶的4,6弹出,再插入,此时:

功能
以上的内容和图我相信是非常容易理解的,但单调栈的作用和功能并不能得到很好的体现,故下面将用文字 + 图示的形式来展示单调栈的作用

先上结论: 
利用单调栈,可以找到从左/右遍历第一个比它小/大的元素的位置

举个例子: 
假设有一个单调递增的栈 S和一组数列: 
a : 5 3 7 4

用数组L[i] 表示 第i个数向左遍历的第一个比它小的元素的位置

如何求L[i]?

首先我们考虑一个朴素的算法,可以按顺序枚举每一个数,然后再依此向左遍历。 
但是当数列单调递减时,复杂度是严格的O(n^2)。

此时我们便可以利用单调栈在O(n)的复杂度下实现

我们按顺序遍历数组,然后构造一个单调递增栈

(1). i = 1时,因栈为空,L[1] = 0,此时再将第一个元素的位置下标1存入栈中

此时栈中情况:

 
(2).i = 2时,因当前3小于栈顶元素对应的元素5,故将5弹出栈 
此时栈为空 
故L[2] = 0 
然后将元素3对应的位置下标2存入栈中

此时栈中情况:

(3).i = 3时,因当前7大于栈顶元素对应的元素3,故 
L[3] = S.top() = 2 (栈顶元素的值)

然后将元素7对应的下标3存入栈 
此时栈中情况:

(4).i = 4时,为保持单调递增的性质,应将栈顶元素3弹出 
此时 L[4] = S.top() = 2;

然后将元素4对应的下标3存入栈 
此时栈中情况:

至此 算法结束 
对应的结果: 
a : 5 3 7 4 
L : 0 0 2 2

总结:一个元素向左遍历的第一个比它小的数的位置就是将它插入单调栈时栈顶元素的值,若栈为空,则说明不存在这么一个数。然后将此元素的下标存入栈,就能类似迭代般地求解后面的元素

代码:

Stack<int> S;
    for(int i=1 ;i<=n ;i++){
        while(S.size() && a[S.top()] >= a[i]) S.pop();

        if(S.empty())     L[i] = 0;
        else              L[i] = S.top();

        S.push(i);
    }
1
2
3
4
5
6
7
8
9
看到这里我相信你一定会有疑问,不知道这个功能有什么作用。 
但其实通过下面的例题你会发现,用好单调栈,我们就可以解决一些看似非常复杂的问题。

例题:
HDU 1506
题目链接:

首先考虑最大面积的矩形X的左右边界的性质:

设其左边界为L,右边界为R,则其高H = min{h[i] | L <= i <= R}

此时最大面积为 (R - L + 1) * H

若此时左边界的左边那个矩形的高度 h[L-1] >= H 
则左边界可以向左拓展,则新的面积为:

(R - (L-1) + 1) * H > 原面积

则与原假设条件冲突

故左边界左边的那个矩形的高度 :h[L-1] < H 
同理右边界右边的那个矩形的高度: h[R+1] < H

设H = h[i]

所以左边界L是满足h[j-1] < h[i]的最大的j,即从i点向左遍历的第一个高度比i小的点的右边一个点

而右边界R是满足 h[j+1] < h[i]的最小的j,即从i点向右遍历第一个高度比i小的点的左边一个点

所以我们可以利用单调栈的性质得到每个确定点,即确定高度的最大面积矩形的左右边界,然后枚举取最大即可。

代码:

#include<bits/stdc++.h>

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

#define rep(i, a, b)              for(int i(a); i <= (b); ++i)
#define dec(i, a, b)              for(int i(a); i >= (b); --i)
#define MP                        make_pair

const int N = 100000 + 100;

stack<int> S;
ll h[N];
int R[N],L[N];

int main(){
    int n;
    while(~scanf("%d",&n) && n){
        for(int i=0 ;i<n ;i++)  scanf("%I64d",&h[i]);

        while(S.size()) S.pop();

        for(int i=0 ;i<n ;i++){
            while(S.size() && h[S.top()] >= h[i]) S.pop();

            if(S.empty())     L[i] = 0;
            else              L[i] = S.top() + 1;

            S.push(i);
        }

        while(S.size()) S.pop();
        for(int i=n-1 ;i>=0 ;i--){
            while(S.size() && h[S.top()] >= h[i]) S.pop();

            if(S.empty()) R[i] = n;
            else          R[i] = S.top();

            S.push(i);
        }

        ll ans = 0;
        for(int i=0 ;i<n ;i++){
            ans = max(ans,h[i] * (R[i] - L[i]));
        }
        printf("%I64d\n",ans);
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
HDU 5033
题目链接

这是北京区域赛的一道题,确实才接触时感觉难度很大,想了两天 加 在网上查看了题解才真正AC。这道题让我对单调栈的理解加深了不少。

题意不难理解,但难在如何利用单调栈的性质快速求解。

之前的想法是一次初始化,二分查询再跳跃式地查找区间左右边界点。 
但奈何复杂度非常高,此题是明显地过不了的。

所以正确需要离线处理,先读入所有的查询,将每个查询点视为高度为0的楼,然后再通过比较两栋相邻楼顶连线的斜率大小维护一个单调栈。

一些细节性的东西网上有很多题解,便不再赘述。

#include<bits/stdc++.h>

using namespace std;

const int A = 100000 + 100;
const double PI = acos(-1.0);
const double eps = 1e-8;

class Build{
public:
    int id;
    double x,h;

    friend bool operator<(Build &a,Build &b){
        return b.x > a.x;
    }
};

Build build[A*2];
double R[2*A],L[2*A];
int S[A*2];
int num,cnt,q,n;

int dcmp(double x){
    return x < -eps ? -1 : x > eps;
}

double f(int i,int j,int type){
    if(type == 1)
        return (build[i].h - build[j].h) / (build[j].x - build[i].x);
    return     (build[j].h - build[i].h) / (build[j].x - build[i].x);
}

inline void solve(){
    cnt = 0;

    for(int i=0 ;i<num ;i++){
       while(cnt >= 2 && f(S[cnt-1],S[cnt],1) - f(S[cnt],i,1) >= 0) {
            cnt--;
       }

        if(build[i].id >= 0){
           if(cnt == 0) L[build[i].id] = 0.0;
           else         L[build[i].id] = atan(f(S[cnt],i,1)) / (2.0*PI) * 360.0;
        }
        S[++cnt] = i;
    }

    cnt = 0;
    for(int i=num-1 ;i >= 0;i--){
       while(cnt >= 2 && f(S[cnt],S[cnt-1],2) - f(i,S[cnt],2) >= 0){
            cnt--;
       }

        if(build[i].id >= 0){
           if(cnt == 0) R[build[i].id] = 0;
           else         R[build[i].id] = atan(f(i,S[cnt],2)) / (2.0*PI) * 360.0;
        }
        S[++cnt] = i;
    }

    for(int i=0 ;i<q ;i++){
        printf("%.10f\n",180.0 - (L[i] + R[i]));
    }
}

int main(){
    int T,_=1;
    scanf("%d",&T);

    while(T--){
        scanf("%d",&n);

        for(int i=0 ;i<n ;i++){
            scanf("%lf%lf",&build[i].x,&build[i].h);
            build[i].id  = -1;
        }

        scanf("%d",&q);
        for(int i=n ;i < n+q ;i++){
            scanf("%lf",&build[i].x);

            build[i].h = 0;
            build[i].id = i - n;
        }

        num = n + q;
        sort(build,build+num);

        printf("Case #%d:\n",_++);
        solve();
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
PKU 2796
题目链接

题意很好理解,但此题不仅要求区间的边界点,还要求区间的和,所以用一个树状数组维护一个区间和即可。 
另外注意全部为一个值和最大值为0的情况。

#include <iostream>
#include<iomanip>
#include <cstdio>
#include <cstdlib>
#include<cstring>
#include <string>
#include <algorithm>
#include <cmath>
#include <cctype>
#include<queue>
#include<map>
#include<stack>

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

const int A = 1e5 + 100;
int n,cnt;
int a[A];
int S[A],L[A],R[A];
ll Tree[A];

int lowbit(int x){
    return x & (-x);
}

ll getsum(int pos){
    ll res = 0;
    while(pos >= 1){
        res += Tree[pos];
        pos -= lowbit(pos);
    }

    return res;
}

void add(int pos,int val){
    while(pos <= n){
        Tree[pos] += val;
        pos += lowbit(pos);
    }
}

void solve(){
    cnt = 0;

    for(int i=1 ;i<=n ;i++){
        while(cnt > 0 && a[S[cnt]] >= a[i]) cnt--;

        if(cnt == 0) L[i] = 1;
        else         L[i] = S[cnt] + 1;
        S[++cnt] = i;
        //printf("%d : L = %d\n",i,L[i]);
    }

    cnt = 0;
    for(int i=n ;i>=1 ;i--){
        while(cnt > 0 && a[S[cnt]] >= a[i]) cnt--;

        if(cnt == 0) R[i] = n;
        else         R[i] = S[cnt] - 1;
        S[++cnt] = i;
       // printf("%d : R = %d\n",i,R[i]);
    }

    ll ans = 0;
    int left,right;
    for(int i = 1;i<=n ;i++){
        ll sum = getsum(R[i]) - getsum(L[i]-1);
        sum *= a[i];

        if(sum >= ans){
            left = L[i];
            right = R[i];

            ans = sum;
        }
    }

    printf("%lld\n",ans);
    printf("%d %d\n",left,right);
}

int main(){
    while(~scanf("%d",&n)){
        memset(Tree,0,sizeof(Tree));
        for(int i=1 ;i<=n ;i++){
            scanf("%d",&a[i]);
            add(i,a[i]);
        }
        a[0] = 0;

        solve();
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
PKU 3250
题目链接

很裸的单调栈,而且只需要跑一个方向。 
简单入门题

#include <iostream>
#include<iomanip>
#include <cstdio>
#include <cstdlib>
#include<cstring>
#include <string>
#include <algorithm>
#include <cmath>
#include <cctype>
#include<queue>
#include<map>
#include<stack>

using namespace std;

typedef long long ll;
typedef unsigned long long ull;

const int INF = 1e9 + 7;
const int A   = 1e5 + 10;

int a[A],S[A],num[A];
int cnt,n;

void solve(){
    cnt = 0;

    a[n] = INF;
    for(int i=n ;i>=0 ;i--){
        while(cnt>0 && a[S[cnt]] < a[i]) cnt--;

        if(cnt == 0) num[i] = 0;
        else         num[i] = S[cnt] - i - 1;
        S[++cnt] = i;
    }

    ll ans = 0;
    for(int i=0 ;i<n ;i++){
        ans += num[i];
    }
    printf("%I64d\n",ans);
}

int main(){
    scanf("%d",&n);

    for(int i=0 ;i<n ;i++){
        scanf("%d",&a[i]);
    }
    solve();
    return 0;
}
--------------------- 
作者:Wubaizhe 
来源:优快云 
原文:https://blog.youkuaiyun.com/wubaizhe/article/details/70136174 

<think>嗯,用户让我介绍一下单调栈算法题,我需要先理解他们可能的需求。可能他们是在准备面试,或者在学习数据结构和算法时遇到了困难。首先,我得明确单调栈的定义和基本概念,然后解释它的应用场景,再给出一些经典例题,这样用户能更好地理解。 首先,单调栈是什么?它是一种特殊的栈结构,里面的元素按照某种顺序排列,比如递增或递减。这种结构通常用来解决需要寻找下一个更大或更小元素的问题。比如,LeetCode上的“每日温度”或者“柱状图中最大的矩形”这类题目。 接下来,我需要讲清楚单调栈的工作原理。比如,当遍历数组时,如何维护栈内元素的单调性。比如,当寻找下一个更大元素时,栈应该是单调递减的。遇到比栈顶大的元素时,就弹出栈顶,并记录结果。这个过程需要详细说明,可能还要举个具体的例子,比如用数组[73,74,75,71,69,72,76,73]来说明每日温度的问题。 然后,应用场景部分需要列举几个常见的问题类型,比如找下一个更大元素、上一个更小元素、或者处理矩形面积等。每个场景最好对应一个例题,并简要说明解决思路。例如,柱状图中最大的矩形,这题需要用单调栈来找到每个柱子左右两边第一个比它矮的柱子,从而计算面积。 可能用户还会想知道实现时的注意事项,比如栈中存储的是元素的值还是索引?通常是索引,因为需要计算位置差或者处理元素的位置关系。另外,处理循环数组的情况,可能需要将数组长度翻倍,或者使用取模的方法来模拟循环。 还有时间复杂度的分析,单调栈通常是O(n),因为每个元素最多入栈和出栈一次。这点需要强调,因为它比暴力解法高效很多。 在编写过程中,要确保例子清晰,步骤详细。比如,用具体的数字一步步推演栈的变化和结果的计算过程。这样用户能够直观地看到每一步发生了什么,理解为什么单调栈有效。 此外,可能需要提醒用户常见的错误,比如在比较元素大小时忘记处理栈为空的情况,或者在处理边界条件时出错。例如,当所有元素都没有下一个更大元素时,应该如何处理返回值。 最后,总结一下单调栈的核心思想,以及解决问题的步骤:确定单调方向、遍历元素、维护栈结构、计算结果。并鼓励用户通过练习题目来巩固理解,推荐一些经典的题目供他们练习。 现在,我需要把这些思考组织成一个结构清晰的回答,分点说明,使用例子来辅助解释,确保用户能够逐步理解单调栈的原理和应用。</think>### 单调栈算法详解 #### 一、基本概念 **单调栈**是一种特殊的栈结构,其内部元素始终保持单调递增或递减的顺序。常用于解决需要寻找元素**下一个更大(或更小)元素**的问题。 #### 二、核心原理 1. **单调性维护** - 递增栈:栈底到栈顶元素递增,用于寻找下一个更小元素。 - 递减栈:栈底到栈顶元素递减,用于寻找下一个更大元素。 2. **操作流程** 遍历数组时,比较当前元素与栈顶元素: - 若符合单调性,压入栈。 - 若破坏单调性,弹出栈顶并记录结果,直到满足单调性。 #### 三、典型应用场景 1. **下一个更大元素** - 例题:[LeetCode 739. 每日温度](https://leetcode.com/problems/daily-temperatures/) - 示例:数组 `[73,74,75,71,69,72,76,73]`,求每个元素下一次更高温度的天数差。 - 过程: ```text 索引 元素 栈操作 结果 0 73 入栈 [ ] 1 74 弹出73,入74 res[0]=1-0=1 2 75 弹出74,入75 res[1]=2-1=1 3 71 入栈 [75,71] 4 69 入栈 [75,71,69] 5 72 弹出69、71,入72 res[4]=5-4=1, res[3]=5-3=2 6 76 弹出72、75,入76 res[5]=6-5=1, res[2]=6-2=4 7 73 入栈 [76,73] ``` - 结果:`[1,1,4,2,1,1,0,0]` 2. **最大矩形面积** - 例题:[LeetCode 84. 柱状图中最大的矩形](https://leetcode.com/problems/largest-rectangle-in-histogram/) - 核心思路:对每个柱子,找到左右第一个比它矮的柱子,确定宽度边界。 3. **循环数组问题** - 处理方式:将数组长度翻倍遍历,或索引取模模拟循环。 #### 四、代码模板(以递减栈为例) ```python def monotonic_stack(nums): stack = [] res = [0] * len(nums) for i in range(len(nums)): while stack and nums[i] > nums[stack[-1]]: top = stack.pop() res[top] = i - top stack.append(i) return res ``` #### 五、复杂度分析 - **时间复杂度**:$O(n)$,每个元素最多入栈和出栈一次。 - **空间复杂度**:$O(n)$,栈空间最坏情况存储全部元素。 #### 六、注意事项 1. 栈中通常存储**索引**而非值,便于计算位置差。 2. 遍历结束后,栈中剩余元素需单独处理(如无后续更大元素时设为0)。 #### 七、经典习题推荐 1. [下一个更大元素 I](https://leetcode.com/problems/next-greater-element-i/) 2. [接雨水](https://leetcode.com/problems/trapping-rain-water/) 3. [子数组的最小值之和](https://leetcode.com/problems/sum-of-subarray-minimums/) 通过练习上述题目,可以深入掌握单调栈的变种应用和边界处理技巧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值