🚀拓展案例
拓展 1 · 单调栈求柱状图中最大的矩形
题目:给出 n 个宽度为 1、高度为 h[i] 的柱子紧挨着排成一行,求其中能框出的最大矩形面积。
核心转化:固定「矩形的高度必须等于某根柱子的高度 h[i]」,那么以 h[i] 为高的最大矩形,其左右边界就是「左边第一个比 h[i] 矮的柱子」和「右边第一个比 h[i] 矮的柱子」之间的全部柱子。设左右边界下标为 L 和 R,则该矩形面积为 h[i] × (R - L - 1)。
做法:用单调栈从左往右扫一遍求出每个 i 的 L[i](栈内保持递增,弹出时是「遇到更矮的」),再从右往左扫一遍求 R[i];最后对每个 i 取面积最大值。总复杂度 O(n)。
C++柱状图最大矩形(单调栈两遍扫描)
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, h[N], L[N], R[N], st[N];
int main() {
while (scanf("%d", &n) == 1 && n) {
for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
// 左边界:左边第一个严格矮于 h[i] 的下标
int top = 0;
for (int i = 1; i <= n; i++) {
while (top && h[st[top]] >= h[i]) top--;
L[i] = top ? st[top] : 0; // 0 表示左边没有更矮的
st[++top] = i;
}
// 右边界:右边第一个严格矮于 h[i] 的下标
top = 0;
for (int i = n; i >= 1; i--) {
while (top && h[st[top]] >= h[i]) top--;
R[i] = top ? st[top] : n + 1; // n+1 表示右边没有更矮的
st[++top] = i;
}
long long ans = 0;
for (int i = 1; i <= n; i++)
ans = max(ans, (long long)h[i] * (R[i] - L[i] - 1));
printf("%lld\n", ans);
}
return 0;
}
运行结果
输入:7
2 1 4 5 1 3 3
输出:8
(高度 4 与 5 两根柱子可框出 4×2 = 8 的矩形;
也可以用三根高度 3 的柱子框出 3×2 = 6,都不如 8)
为什么这个方法对?对任意一个最大矩形,把它向上顶到不能再顶(即上边界贴着某根柱子的顶),就得到一个「以某根柱子的高为高」的矩形,且它的宽度恰好是「左右第一个更矮的柱子之间」的宽度。所以枚举所有柱子的高度就覆盖了最优解。
🔍 代码逐段解析 · 柱状图最大矩形
① 外层 while:while (scanf("%d", &n) == 1 && n) 多组数据、n=0 结束——每组数据内部 top 等指针都要重新初始化。
② 求左边界(第 11~16 行):从左往右扫,while (top && h[st[top]] >= h[i]) top--; 弹掉所有「不比 h[i] 矮」的栈顶——它们不可能是任何右侧柱子的「左边第一个更矮」;L[i] = top ? st[top] : 0,0 作虚拟左哨兵。栈内保持递增。
③ 求右边界(第 17~21 行):完全对称,从右往左扫,R[i] = top ? st[top] : n+1,n+1 作虚拟右哨兵。
④ 统计(第 22~24 行):h[i] * (R[i] - L[i] - 1) —— L、R 本身是「更矮的」柱子要排除,所以宽度减 1;强转 long long 防止 10⁵×10⁵ 溢出。
易错点:① 两遍扫描的比较条件都是 >=(弹掉等高柱子才不会把矩形截断在等高处);② 宽度公式漏减 1,答案整体偏大。
拓展 2 · 单调队列 + 二分答案
题目(思路框架):一条数轴上从左到右有若干格子,第 i 格有一个分数 a[i] 或(负)标记。从原点出发,每次可以跳到 [d-g, d+g] 范围内前方的格子上,求能拿到 ≥ K 分所需的最小 g。
为什么能二分:「g 越大,可跳范围越大,能拿到的最大分数不减」——单调性成立。所以可以二分 g,用 check(g) 判断可行性。
check(g) 怎么做到 O(n):设 dp[i] 为跳到第 i 格时的最大得分,则
dp[i] = a[i] + max{ dp[j] } 其中 j < i、可跳到、且 d-g ≤ i-j ≤ d+g(或 i < d-g 时起点直接可达)
候选区间 j ∈ [i-d-g, i-d+g] 是随 i 单调滑动的定长窗口,且要与 dp 取 max——正是单调队列的形态。于是 check 是 O(n),整体 O(n log n)。
🧩 提炼出的通用套路:当 DP 的候选区间左端点也随答案参数变化时,先用二分固定参数,再在 check 内部用单调队列优化 DP。这类「二分答案 + 单调队列优化 DP」的组合在提高组和省选里非常高频。
拓展 3 · 单调队列优化多重背包
问题:背包容量 V,有 n 种物品,第 i 种体积 w[i]、价值 v[i]、数量 c[i]。求最大价值。朴素做法是枚举每种物品取几个,复杂度 O(V·Σc)。
关键观察:按「模 w 的余数」把容量分组。对固定余数 r,考虑容量 r, r+w, r+2w, … 这些位置,设 p = (V - r) / w,则
dp[r + k·w] = max over j ∈ [max(0, k-c), k-1] { dp[r + j·w] + (k - j)·v }
= k·v + max over j ∈ [max(0, k-c), k-1] { dp[r + j·w] - j·v }
把 dp[r + j·w] - j·v 看作新数组,则候选区间 j ∈ [k-c, k-1] 是定长滑动窗口,取最大值——又是单调队列。复杂度从 O(V·Σc) 降到 O(V·n)。
💡 拓展 3 的启示:单调队列不是「某类题专用的工具」,而是一种把「区间最值」这件事做快的通用加速器。只要你能在 DP 转移中分离出「与决策无关的项」,并把剩下的部分写成滑动区间的最值,就能套上它。识别这一模式,比记住具体题目重要得多。
🤔思考与启发
思考 1:单调队列在「队尾淘汰」时,如果新元素与队尾元素值相等,该不该把队尾弹掉?如果弹(用 >=)和不弹(用 >),对最终答案有没有影响?对「求最值」和「求最值的位置」两类问题,答案一样吗?
提示:对「求最值」没影响(谁留都行,值相同);但对「求最值第一次出现的位置」有影响——弹掉旧元素会得到更晚的位置,保留则会得到更早的位置。所以要看题目要的是哪一个。
思考 2:为什么单调队列的复杂度是 O(n) 而不是 O(nk)?while 循环明明可能一次弹出很多个元素,为什么总代价不是 k 倍?
提示:这就是均摊分析。每个元素最多被压入队列一次,也就最多被弹出一次;因此「所有 while 循环体”总共执行的次数 ≤ n。把目光从「单次操作」移到「整体操作次数」,是复杂度分析的重要思维转变。
思考 3:如果窗口长度 k 不固定,而是「满足某个条件的最大区间」(例如「区间和 ≤ S 的最长区间」),还能用单调队列吗?如果不能,该用什么?
提示:单调队列要求窗口边界随 i 单调移动。若左边界随 i 单调不减(比如区间和超限就右移左端点),那么仍然是「单调队列 / 双指针」的范围;若左边界会来回摆动,就需要换成别的结构(如线段树、平衡树)。
思考 4:「求窗口最大值」既可以用单调队列(O(n))、也可以用优先队列做懒惰删除(O(n log n))。二者各有什么优劣?什么时候必须用单调队列?
提示:优先队列写法更简单(只需「堆顶过期就弹」),但堆里会堆大量已过期的元素,空间可能到 O(n),并且常数更大。而单调队列空间只要 O(k)、时间严格 O(n),且能在线处理——当 n 达到 107 或内存吃紧时,单调队列是唯一选择。