NeFut Logo NeFut
EN 管理员登录

单调栈:高效求解下一个更大元素的算法解析

发布于:2026-05-27 09:17 最后更新:2026-06-21 02:30
#C++ #Tutorial

核心逻辑与数学原理

以求解“右侧下一个更大元素(Next Greater Element, NGE)”为例。 定义序列 $A$ 长度为 $N$。对于任意位置 $i$,需找到最小的 $j$ 满足 $j > i$ 且 $A[j] > A[i]$。

单调栈作为一种通过维护栈内元素单调性实现 $O(N)$ 扫描的线性数据结构,其核心思想在于利用时序与大小的双重单调性,及时处理或排除决策,将总时间复杂度从原本暴力解法的 $O(N^2)$ 彻底优化至 $O(N)$

在该机制下,每一个决策元素一辈子“至多入栈一次,至多出栈一次”,因此其单步操作在全局视角的均摊复杂度为 $O(1)$

从左向右扫描的“及时履约”逻辑

当我们从左到右遍历序列时,栈内存放的是尚未找到右侧更大元素的单调递减候选项


状态设计与对偶变体

在信息学竞赛中,单调栈一律存储元素下标而非数值。因为下标既能通过 $A[idx]$ 索引数值,又能直接通过下标作差($i - j$)计算区间长度。

通过统一采用从左到右的扫描方向,仅需微调弹栈触发条件答案结算时机,即可完美解决全部四种对偶问题。这种分类方式将单调栈抽象为两大核心机理:

目标问题 扫描方向 栈内数值单调性 弹栈触发条件 结算时机与机制(不重不漏)
右侧第一个更大值 从左到右 单调不增(大 $\to$ 小) A[i] > A[st[top]] 弹栈时结算他人:被弹出的 st[top] 找到了答案 i
右侧第一个更小值 从左到极 单调不减(小 $\to$ 大) A[i] < A[st[top]] 弹栈时结算他人:被弹出的 st[top] 找到了答案 i
左侧第一个更大值 从左到右 单调不增(大 $\to$ 小) A[i] >= A[st[top]] 入栈时结算自身:剔除冗余后,当前的 st[top] 即为 i 的答案
左侧第一个更小值 从左到右 单调不减(小 $\to$ 大) A[i] <= A[st[top]] 入栈时结算自身:剔除冗余后,当前的 st[top] 即为 i 的答案

核心算法模板

// 核心逻辑:从左到右扫描,求解每个元素右侧第一个严格更大元素的下标
// st[] 模拟栈,top = 0 表示栈空;ans[] 存储答案,未找到则默认为 0
int top = 0;
for (int i = 1; i <= n; ++i) {
    // 当栈不为空且当前元素大于栈顶对应元素时,触发弹栈
    while (top > 0 && a[i] > a[st[top]]) {
        ans[st[top]] = i; // 结算:栈顶元素的右侧第一个更大元素就是当前位置 i
        top--;            // 弹栈
    }
    st[++top] = i;        // 当前位置入栈,等待它的未来履约人
}

栈内状态不变性证明

设在算法运行的任意阶段,栈内从栈底到栈顶的元素下标依次为 $p_1, p_2, \dots, p_k$。我们通过数学归纳法证明,该结构在任意时刻都死锁了以下两个状态不变性

  1. 初始状态:扫描前栈为空,不变性显然成立。当压入第一个元素 $p_1$ 时,栈内仅有一项,两个不变性依然成立。
  2. 递推(状态转移):假设当前栈内状态满足上述两个不变性。此时新元素 $i$(必有 $i > p_k$)到来:

NOIP 实战避坑指南

数组模拟栈的边界控制

使用 int st[MAXN], top = 0; 时,必须确保 top > 0 才能访问 st[top]。在 while 循环中,必须将 top > 0 放在 && 的最左侧。若由于逻辑短路写错顺序(形如 while(a[i] > a[st[top]] && top > 0)),当栈空时会优先访问 a[st[0]] 甚至 a[st[-1]],导致 Runtime Error (RE)

严格最值与非严格最值的符号陷阱

根据题意严密确立 > 还是 >=

内存与常数优化

stack底层默认是deque,涉及动态内存分配,在大数据量下常数极大。手写st[++top] = itop--` 没有任何额外开销,运行速度比 STL 快 3 倍以上。


经典真题讲解

洛谷 P5788 【模板】单调栈

核心算法实现

// 核心逻辑:线性扫描求右侧 NGE
for (int i = 1; i <= n; ++i) {
    while (top > 0 && a[i] > a[st[top]]) {
        ans[st[top]] = i; // 结算被弹栈元素的答案
        top--;
    }
    st[++top] = i; // 当前下标入栈
}

洛谷 P1191 矩形

核心算法实现

// 核心逻辑:逐行更新直方图高度,利用单调栈维护局部最小值并做 DP 转移
long long total_ans = 0; // 计数问题务必开 long long

for (int i = 1; i <= n; ++i) {
    int top = 0;
    st[0] = 0; // 核心技巧:引入 0 号位置作为左边界的虚拟哨兵,h[0] = 0

    for (int j = 1; j <= n; ++j) {
        // 1. 动态更新当前行各列的直方图高度
        h[j] = (g[i][j] == 1) ? h[j] + 1 : 0;

        // 2. 维护严格单调递增栈,遇到高度 >= 的就弹栈
        while (top > 0 && h[st[top]] >= h[j]) {
            top--;
        }

        // 3. 此时 st[top] 即为左侧第一个严格矮于当前高度的位置 k
        int k = st[top];

        // 4. 状态转移并累加答案
        dp[j] = dp[k] + (long long)h[j] * (j - k);
        total_ans += dp[j];

        st[++top] = j; // 当前列下标入栈
    }
}

[h] 返回首页