答案等于区间最小值乘区间长度;按柱子高度枚举,用单调栈在弹出时同时定出左右第一个更矮的位置,均摊 O(n) 求最大矩形面积。
OJ: roj
题目 ID: 3032
难度:普及+/提高-
标签:单调栈栈
创建: 2026-10-01 11:16
更新: 2026-10-01 11:20
形式化题目
有
设这个矩形覆盖的列区间为
正解
思路
输入的每一行是一个独立的直方图,多个用例逐个求解即可(以 n=0 结束),下面只讨论单个用例。
朴素做法与瓶颈。 把上面的式子直接写成代码:枚举左端点
换一个枚举对象。 注意一个事实:任意最优矩形的高度一定等于某根柱子的高度
宽度由"第一个更矮"决定。 记
则以
枚举对象从
用单调栈同时拿到两个边界。 从左到右扫描,维护一个存下标的栈,并使栈内下标对应的高度严格递增:
- 扫描到位置
时,若 ,说明栈顶 top向右已经走不动了,就是它右边第一个更矮的位置,即 ; - 把
top弹出后露出的新栈顶,正是它左边第一个更矮的位置,即; - 于是一次弹出就同时确定了左右边界,立刻结算
,然后继续判断新的栈顶。
两个实现细节。
- 判断条件必须写成
(带等号)。等高时 并不是 top真正的右边界,但让等高的top先弹出、把留在栈里,宽度会在 i结算时把这些同高的列一起算进去,面积一样大;如果写成严格小于,等高的柱子会互相卡住、谁也不弹出,宽度就会算小。 - 在序列末尾追加一个高度为
的哨兵,保证扫描结束时栈里剩下的柱子(右边没有更矮者, )全都被弹出结算,不必在循环外另写一段清栈代码。高度为 的柱子天然只贡献面积 ,不需要特判。
下面的剖面图把样例 2 1 4 5 1 3 3 画了出来,X 标出的就是面积
5 | #
4 | XX
3 | XX ##
2 |# XX ##
1 |##XX###
+-------
0123456可以看出它由下标 4 1000 1000 1000 1000 是四根等高柱子,正好整段铺满,面积
| 扫描到 i | 本步弹出并结算 | 结算后栈(下标(高度)) | |
|---|---|---|---|
| 0 | 2 | — | 空 |
| 1 | 1 | 0(2): |
空 |
| 2 | 4 | — | 1(1) |
| 3 | 5 | — | 1(1) 2(4) |
| 4 | 1 | 3(5): |
空 |
| 5 | 3 | — | 4(1) |
| 6 | 3 | 5(3): |
4(1) |
| 哨兵 7 | 0 | 6(3): |
空 |
读者可以重点看第
代码
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 11:16
# update_at: 2026-10-01 11:20
import sys
from itertools import chain
def max_rect(heights: list[int]) -> int:
"""返回柱状图(每根宽 1、公共基线对齐)中最大轴对齐矩形的面积。
单调栈从底到顶按高度严格递增地存下标。弹出栈顶 j 时,新栈顶 left 是 j 左边
第一个更矮的柱子,而当前位置 right 是 j 右边第一个更矮的柱子;所以高度
取 heights[j] 的矩形最远只能铺满开区间 (left, right),宽度恰好 right-left-1。
追加一个高度 0 的哨兵(下标 n)收尾,保证每根柱子都被弹出且只结算一次。
"""
stack: list[int] = [] # 只存下标
best = 0
for right, height in enumerate(chain(heights, (0,))):
while stack and height <= heights[stack[-1]]:
popped = stack.pop()
left = stack[-1] if stack else -1 # 左侧第一个更矮的柱子,-1 表示没有更矮的
best = max(best, heights[popped] * (right - left - 1))
stack.append(right)
return best
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
# 每个测试用例以 n 打头:n 是这组柱子的根数,n=0 是结束标记、不用处理
for n in data:
if n == 0:
break
out.append(str(max_rect([next(data) for _ in range(n)])))
print('\n'.join(out))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
(均摊)。每个下标恰好入栈一次、出栈至多一次, while循环内所有结算的总次数为;多个测试用例时按总柱数线性。 - 空间复杂度:
。栈最多同时存 个下标;整份输入按 token 读入另占 。
总结
- 本题的出发点是把答案写成区间最小值乘区间长度;朴素的区间枚举是
,瓶颈在枚举对象。 - 关键观察是"最优矩形的高一定等于某根柱子的高度",于是改为按柱子枚举,答案变成
。 - 单调递增栈在一次弹栈中同时拿到"左边第一个更矮"(新栈顶)与"右边第一个更矮"(当前扫描位置),把边界查询降到均摊
。 - 两个必须记住的实现细节:弹出条件带等号(
h_i <= h_top),以及在末尾补一个高度的哨兵。前者保证等高的柱子合并结算,后者保证所有柱子都被结算。
图示解析
这张图串起本题从建模到得到答案的主线:
区间最值形式
`- 最优矩形的高 = 某根柱子的高度 h_j (抬高到区间最小值只会更大)
`- 以 h_j 为高能铺多宽?
`- 答案是左右第一个更矮柱子之间 h_j x (R_j - L_j - 1)
`- 单调递增栈弹出时同时给出左边界 L_j 与右边界 R_j
`- 末尾补哨兵 0,一次扫描结算全部柱子,均摊 O(n)先看每一层解决的瓶颈:第一层把 `- 的分支往下读,每一步都只依赖上一层已经证明过的结论。
