“不必预先保存全树。用单调栈保存尚未找到右侧完整结果的左子树。每项是“已排好序的区间、其右边界power”,从栈底到栈顶power严格递增;另有一个当前段。”
形式陈述
单调栈是一种扫描序列时维护候选位置的方法:栈内下标按扫描顺序排列,对应数值保持单调;新元素到达时,依据问题的比较规则弹出一段栈顶,再将自己压入栈。
“单调”只是外观。正确性的核心是说明:弹出的元素为什么可以永久离开,留下的元素为什么仍包含所需答案。 不同问题中,弹出可能表示“这个位置的答案已确定”,也可能表示“它已被更近、更合适的候选替代”,不能把二者混成同一个理由。
下面固定一个任务展开:对数组
不存在时返回
直觉
从左到右扫描,栈里保留尚未找到右侧更大元素的下标。若当前值
因此从栈底到栈顶,下标严格递增,数值单调不增。相等的值可以同时留下,因为“相等”尚不足以回答“严格更大”。
最小实现
from collections.abc import Sequence
def next_greater_indices(a: Sequence[int]) -> list[int]:
"""返回右侧第一个严格更大元素的下标;不存在时为 -1。"""
answer = [-1] * len(a)
stack: list[int] = []
for i, value in enumerate(a):
while stack and a[stack[-1]] < value:
answer[stack.pop()] = i
stack.append(i)
return answer
assert next_greater_indices([2, 1, 4, 3, 3, 5]) == [2, 2, 5, 5, 5, -1]
assert next_greater_indices([2, 2]) == [-1, -1]
assert next_greater_indices([]) == []
代码使用 Python 3.9 及以上版本的类型标注;不修改输入。返回的是下标,取具体答案值前应先检查是否为
六步跟踪
对
| 当前下标和值 | 弹出的下标 | 本步确定的答案 | 压入后的栈 |
|---|---|---|---|
0:2 |
— | — | 0:2 |
1:1 |
— | — | 0:2, 1:1 |
2:4 |
1, 0 |
next(1)=2, next(0)=2 |
2:4 |
3:3 |
— | — | 2:4, 3:3 |
4:3 |
— | — | 2:4, 3:3, 4:3 |
5:5 |
4, 3, 2 |
next(4)=next(3)=next(2)=5 |
5:5 |
两个值为
正确性与复杂度
为什么找到的是“第一个”
假设
这也解释了栈的单调方向:上方候选的值更小,当前值若足以解决下方候选,就必能先解决挡在上方的候选。单调性使需要处理的位置恰好连成栈顶的一段,而无需在栈中间搜索。
为什么嵌套循环仍是线性
每个下标恰好压栈一次,弹出后不再回来,因此全部扫描期间最多发生
在数组访问、值比较为常数成本的模型下,总时间
下图展示后文的另一种用法:为当前元素寻找左侧最近更小值。它使用递增候选栈,与上面的右侧严格更大任务采用不同的弹栈理由。
例子与边界
相等元素决定比较符号
在同一个左到右、为被弹元素填写右侧答案的模板中:
| 所求关系 | 何时弹出栈顶 |
|---|---|
| 右侧第一个严格更大 | |
| 右侧第一个大于等于 | |
| 右侧第一个严格更小 | |
| 右侧第一个小于等于 |
例如 [2,2,3] 的严格更大下标为 [2,2,-1],大于等于下标却为 [1,2,-1]。四个比较符号只适用于这里固定的扫描和填答案方式,不能脱离模板死记。
求“当前元素左侧最近的严格更小元素”时,常见做法是先弹出值大于等于当前值的候选,再读剩余栈顶作为当前元素的答案。此时弹出理由变了:旧候选位置更远、值又不小于新元素,以后只要旧候选够小,新元素也够小且更近,所以旧候选被永久支配;并不是说当前值替它找到了右侧答案。
柱状图与并列归属
对于高度 [2,1,5,6,2,3] 中,高度
所有最优矩形都能选到一根达到其最小高度的柱,因此比较各柱的最大合法跨度足够。若算法改为统计子数组最小值的贡献,同一段中的相等最小值却不能重复认领;常用“一侧严格、另一侧非严格”的边界分工,为每个子数组指定唯一归属。求最大值与计数对重复结算的容忍度不同。
推论与应用
单调栈适合最近更大/更小元素、股票跨度和柱状图等静态邻近问题;它的本质是用顺序与值的关系,证明某些候选可以永久消失。并不是只要一个问题出现“最大值”就适合使用。
SMAWK也用候选栈,但淘汰的是矩阵整列:一个列在上方行已经失去资格,又在当前行被新列严格击败,才能由全单调性推出下方行也不需要它。这里复用的是栈的操作结构,不能直接借用一维最近更大元素的证明。
采用一致的并列规则后,相同的弹栈结构还能在线性时间构造笛卡尔树,并进一步把静态 RMQ 与树上祖先问题联系起来。但单调栈本身不直接回答任意区间查询,也不在数组更新后自动恢复已经删除的候选。数据能否改变、答案是当前结算还是未来候选,必须随问题重新检查。
元素的比较依赖全序;相等元素的处理则由所求边界采用严格还是非严格不等式决定。
参考资料
- USACO Guide, Stacks — Application: Nearest Smaller Element,候选支配证明、索引栈实现与摊还复杂度。
- Antti Laaksonen, Competitive Programmer’s Handbook,§8.2 “Nearest smaller elements”,扫描不变量与线性算法。
- Thomas H. Cormen et al., Introduction to Algorithms, 4th ed., MIT Press, 2022,摊还分析中的栈操作计费方法。