单调栈和单调队列解决同一类问题:在滑动窗口里随时拿到最值。手法只有一句——把“永远不可能成为答案”的元素当场删掉。删得干净,每个元素只进一次、出一次,均摊 O(n)。
| 任务 | 结构 | 弹出条件 | 均摊代价 |
|---|---|---|---|
| 右边第一个更大元素 | 单调栈,栈内值不增 | 栈顶值 < 当前值就弹 | O(n) |
| 柱状图中最大矩形 | 单调栈,左右各跑一遍 | 栈顶值 ≥ 当前值就弹 | O(n) |
| 长度 k 的窗口最值 | 单调队列,队内值单调 | 队尾更差就弹 + 队头过期就弹 | O(n) |
| f[i] = a[i] + max f[j] | 单调队列,窗口 [i−R, i−L] | 队尾 f 更小就弹 + 队头出窗 | O(n) |
四段代码是同一份骨架:while 弹队尾 → push 当前 → while 弹队头 → 用队头更新答案。真正的差别只有两处:弹队尾用 < 还是 ≤,以及队头什么时候算过期。这两处写错,样例照样过,随机数据一片 WA。
给出项数为 n 的整数数列 a1, a2, …, an。定义 f(i) 为数列中第 i 个元素之后第一个大于 ai 的元素的下标;若不存在这样的元素,则 f(i) = 0。
输入:第一行一个整数 n(1 ≤ n ≤ 3×106);第二行 n 个整数 a1..n(1 ≤ ai ≤ 109)。
输出:一行 n 个整数 f(1), f(2), …, f(n),相邻两数之间用一个空格隔开。
写代码前先把两个工具拆开。它们都很短,但每一条格式要求背后都对应着一类 WA。
| 组成 | 含义 / 格式要求 |
|---|---|
<deque> | <bits/stdc++.h> 已包含。声明写成 deque<int> q; 或 deque<pair<int,int>> q;。 |
push_back / pop_back | 队尾进、队尾出。单调队列用它们维护“值单调”这一侧。 |
push_front / pop_front | 队头进出。这是它和 stack 的唯一区别:两端都能动,所以能弹掉过期元素。 |
front() / back() | O(1) 取两端。单调队列的队头永远是当前窗口的最值下标。 |
必须存下标 | 存值就无法回答“这个元素还在不在窗口里”。窗口滑动时只靠下标判断过期。 |
q[i] 随机访问 | deque 支持 O(1) 下标访问,但单调队列只用两端,用 q[i] 遍历反而慢。 |
底层结构 | 分段连续存储,每段定长;两端增删都是 O(1),比 vector 多一层指针,常数略大。 |
什么时候用手写数组 | 窗口长度和值域都固定时,用 int q[N], head, tail 更快(无函数调用、无内存分配)。 |
#include <bits/stdc++.h>
using namespace std;
int main() {
int a[] = {5, 3, 8, 1}; // 下标 0..3
deque<int> q; // 只存下标,值从队头到队尾单调递增
for (int i = 0; i < 4; ++i) {
while (!q.empty() && a[q.back()] >= a[i]) q.pop_back(); // 队尾更差的永久退休
q.push_back(i);
cout << a[q.front()] << " "; // 队头就是当前最小值:5 3 3 1
}
cout << "\n";
return 0;
}在空的 deque 上调用 front() / back() 是未定义行为,通常读到垃圾值。每一处 front() 之前都要想清楚“它会不会是空的”。
deque 虽然支持 q[i],但它的迭代器比 vector 重;频繁按下标遍历时用 vector。
本节所有题目都可以用 deque 实现,熟练后再换成手写数组压常数。
| 组成 | 含义 / 格式要求 |
|---|---|
top = 0 表示空栈 | 元素存在 stk[1..top],top 同时就是栈的大小。stk[0] 永远空着,可以当值为 0 的哨兵。 |
stk[++top] = i | 先加后写:第一个元素落在 stk[1]。写成 stk[top++] 会让下标从 0 开始,后续判空要小心。 |
a[stk[top]] | 栈里存下标,随时能取回原值、原位置。存值就丢了位置信息,答案要求下标时直接报废。 |
--top | 出栈只减指针,O(1),不动任何内存。 |
比 std::stack 快 2~3 倍 | std::stack 的 top()、push()、pop() 是三次函数调用,且无法按下标回溯;3×106 的题手写数组明显更快。 |
栈必须是全局或 static | n = 3×106 时栈数组 12 MB,开在 main 里会让栈帧过大直接爆栈;开成全局变量(在 BSS 段)最稳。 |
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 3000005;
int stk[MAXN], top = 0; // 全局:3e6 个 int 进静态区,不占调用栈
int main() {
int a[] = {0, 5, 3, 8, 1}; // 1 基下标,a[0] 空着不用
int n = 4;
for (int i = 1; i <= n; ++i) {
while (top > 0 && a[stk[top]] < a[i]) --top; // 弹掉挡不住当前元素的
stk[++top] = i; // 自己进栈
cout << "i=" << i << " 栈内值:";
for (int k = 1; k <= top; ++k) cout << a[stk[k]] << " ";
cout << "\n";
}
return 0;
}
/* 输出(栈内值始终不增):
i=1 栈内值:5
i=2 栈内值:5 3
i=3 栈内值:8
i=4 栈内值:8 1 */判空必须写 top > 0。少了它,空栈时读 stk[0](值恒为 0)可能侥幸不崩,但逻辑已经错了——这类“不崩的错”最难查。
数组大小按题目上限 +5 开,n ≤ 3×106 就开 3000005。开小一个数量级只会得到 RE,不会给你提示。
朴素做法:对每个 i 向后扫,找到第一个更大的位置。n = 3×106 时最坏 9×1012 次比较,按每秒 108 次算要二十多个小时。瓶颈不是“找得慢”,而是重复扫描。
关键是一条支配关系:若 i < j 且 ai ≤ aj,那么对任何 k < i,j 都比 i 更近、且不小于 ai,所以 i 永远不可能是 k 的答案——i 可以从候选集合里删掉。
按这条规则删下去,剩下的候选永远是一串“下标递增、值不增”的位置。新元素 ai 一到,值比它小的候选全部当场确定答案 i(i 确实是它们右边第一个更大的位置),然后 i 自己进候选集合。
“只在一端增删、还要保持单调”——这就是栈。新元素从栈顶开始弹,弹干净再压入,栈里值自然不增。
for (int i = 1; i <= n; ++i) {
while (top > 0 && a[stk[top]] < a[i]) { ans[stk[top]] = i; --top; }
stk[++top] = i;
}为什么总复杂度是 O(n):每个下标进栈恰好 1 次、出栈至多 1 次,while 里的比较总次数 ≤ 2n。单次 while 最坏能弹 n 次,但那是把别人先前存下的“入栈次数”一次花掉;摊到全部 n 个元素上就是常数。
两处细节决定对错:弹的条件用 < 还是 ≤——题目要“严格大于”,值相等的候选必须留着,所以用 <;栈里存下标而不是值——答案要位置,且重复值无法区分。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 3000005;
int n, a[MAXN], ans[MAXN];
int stk[MAXN], top = 0; // 手写栈:stk[1..top] 存下标,top = 0 是空
inline int readInt() { // 3×10^6 个数,逐字符读比 cin 快 3 倍以上
int x = 0; char c = getchar();
while (c < '0' || c > '9') c = getchar();
while (c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = getchar(); }
return x;
}
int main() {
n = readInt();
for (int i = 1; i <= n; ++i) a[i] = readInt();
for (int i = 1; i <= n; ++i) {
// 栈里每个下标都还没等到答案,它们的值从栈底到栈顶单调不增
while (top > 0 && a[stk[top]] < a[i]) {
ans[stk[top]] = i; // i 是它右边第一个更大的位置,答案当场定下
--top; // 它再也不会被用到,出栈
}
stk[++top] = i; // 自己进栈,继续等右边
}
// 结束时栈里剩下的下标右边没有更大的数,ans 保持 0
for (int i = 1; i <= n; ++i) printf("%d ", ans[i]);
printf("\n");
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
const int MAXN = 3000005; | 题给 n ≤ 3×106,多开 5 个位置防越界。开成 1e6 只会得到 RE。 |
int stk[MAXN], top = 0; | 全局数组放在静态区,不占 8 MB 的调用栈;top = 0 表示空栈,元素从 stk[1] 开始存。 |
while (c < '0' || c > '9') c = getchar(); | 跳过空格与换行。本题 ai ≥ 1,可以不管负号。 |
x = x * 10 + (c - '0'); | 逐位累加。手写快读的本质就是自己解析数字,绕开 cin 的格式检查。 |
while (top > 0 && a[stk[top]] < a[i]) | 弹的边界是“严格小于”:等于 a[i] 的候选不能弹,因为等于不算“大于”。前半句 top > 0 是判空,少了它会读 stk[0]。 |
ans[stk[top]] = i; | 栈顶那个下标等的就是现在。注意先记答案再 --top,顺序反了就记错了人。 |
stk[++top] = i; | 当前元素必定进栈:它自己也要等右边更大的数。先 ++ 后赋值,保证元素落在 stk[1..top]。 |
printf("%d ", ans[i]); | 每个答案后面跟一个空格。题目不严格卡行末空格,但 3×106 次 cout 会慢一截。 |
第一件事是把“下一个更大”换成“左右第一个更矮”,问题就从模板题变成一道真正的模型题:柱状图中最大的矩形。
n 根宽 1、高 hi 的柱子并排。结论是:最大矩形总可以“往上抬”到顶住某根柱子的顶端,所以它一定是以某个 hi 为高的矩形。于是枚举 i,求以 hi 为高时能伸多宽——答案是左右两边各自“第一个比 hi 矮”的位置之间。
设 Li、Ri 为左右第一个比 hi 矮的下标(没有就是 0 与 n+1),则面积 = hi × (Ri − Li − 1)。求 L、R 就是把上一题的不等号反过来,各跑一遍单调栈。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
int n;
cin >> n;
vector<int> h(n + 2, 0); // h[0] 与 h[n+1] 是高度 0 的哨兵
for (int i = 1; i <= n; ++i) cin >> h[i];
vector<int> L(n + 2), R(n + 2), stk;
stk.reserve(n + 2);
// 左边界:i 左边第一个比 h[i] 矮的位置
for (int i = 1; i <= n; ++i) {
while (!stk.empty() && h[stk.back()] >= h[i]) stk.pop_back();
L[i] = stk.empty() ? 0 : stk.back();
stk.push_back(i);
}
stk.clear();
// 右边界:i 右边第一个比 h[i] 矮的位置
for (int i = n; i >= 1; --i) {
while (!stk.empty() && h[stk.back()] >= h[i]) stk.pop_back();
R[i] = stk.empty() ? n + 1 : stk.back();
stk.push_back(i);
}
ll best = 0;
for (int i = 1; i <= n; ++i)
best = max(best, (ll)h[i] * (R[i] - L[i] - 1));
cout << best << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
vector<int> h(n + 2, 0); | 首尾各留一个高度 0 的哨兵。任何真实柱子都比它高,于是“左边没有更矮的”这种情况由 0 号位兜住,代码里少一个分支。 |
h[stk.back()] >= h[i] | 弹掉所有“不比 h[i] 高”的柱子并保留等号:等高的柱子可以被同一个矩形一起覆盖,让最靠左的那根算全宽度就够了,最大值不会漏。 |
L[i] = stk.empty() ? 0 : stk.back(); | 弹干净后栈顶就是左边第一个更矮的位置;栈空说明左边全是高个子,左边界顶到 0。 |
for (int i = n; i >= 1; --i) | 右边界完全对称,只是方向反过来。两遍共用一个栈,中间必须 stk.clear(),否则上一遍的残留会污染结果。 |
(ll)h[i] * (R[i] - L[i] - 1) | 高度到 109、宽度到 105,乘积到 1014,必须 long long。这是本题最隐蔽的 WA。 |
R[i] - L[i] - 1 | 减 1 是因为 L、R 都是“更矮”的位置,不参与矩形。写成 R−L 会把矩形撑到矮柱子上,答案偏大。 |
把 h[0] = h[n+1] = 0 当哨兵,令 i 从 0 扫到 n+1:只要 h[栈顶] > h[i] 就弹出栈顶 p,此时栈顶(弹完后的新栈顶)就是 p 的左边界,i 是右边界,直接算面积 h[p] × (i − 栈顶 − 1)。
每个柱子只在被弹出时结算一次,所以只用一个栈、一个变量就能出答案,常数比两遍扫描小一半。
二维版本是“每行当一次柱状图”:先对每一行求出“以这一行为底、向上连续 1 的个数”,再对每行跑一次上面的算法,总复杂度 O(nm)。洛谷上的玉蟾宫(P4147)就是这个模型。
单调队列 = 单调栈 + 队头能出。窗口 [i−k+1, i] 右移一格,左边出去一个、右边进来一个。队尾负责“进”时弹掉更差的,队头负责“出”时弹掉过期的。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
deque<int> q; // 存下标;对应值从队头到队尾单调递增
for (int i = 0; i < n; ++i) {
while (!q.empty() && a[q.back()] >= a[i]) q.pop_back(); // ① 队尾更差的永久退休
q.push_back(i);
while (q.front() <= i - k) q.pop_front(); // ② 滑出窗口的从队头退休
if (i >= k - 1) cout << a[q.front()] << " "; // ③ 窗口满 k 个才有答案
}
cout << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
a[q.back()] >= a[i] | ① 队尾的值不小于新元素,说明队尾既不更小、又比新元素先过期,永远轮不到它当最小值,直接弹。等号也弹:留下最新的那个,窗口右移时它活得更久。 |
q.push_back(i); | 弹干净后当前元素一定入队。它可能是后面若干个窗口的答案。 |
q.front() <= i - k | ② 窗口左端是 i−k+1,下标 ≤ i−k 的都已出窗。写成 < 就少弹一轮,过期元素会霸占队头,答案偏小。 |
if (i >= k - 1) | ③ 前 k−1 个位置窗口还没满,不能输出。0 基下标下这是唯一的正确边界,写成 i > k−1 会少一个数。 |
cout << a[q.front()] | 队头是整个窗口的最小值,因为队内值单调递增且队头没有过期。 |
求最大值 | 把 ① 的 >= 改成 <=,其余一个字都不动。两类最值只差一个符号。 |
复杂度:每个下标入队 1 次、出队至多 1 次,总操作 ≤ 2n,与窗口长度 k 无关。用 multiset 也能维护窗口最值,但每次 O(log k),n = 106 时慢 20 倍。
单调队列真正的价值在优化 DP。看一个典型转移:fi = ai + max{ fj : i−R ≤ j ≤ i−L }。题意:一条走廊有 n 个格子,从 0 出发,每次必须向前跳 L 到 R 格,落到格子 i 得 ai 分(可以是负数),求跳到 n 的最大得分。
暴力枚举 j 是 O(nR)。但 fj 的取值区间 [i−R, i−L] 随 i 单调右移、长度固定,正是“滑动窗口最值”。把它换成单调队列,转移降到 O(1),总复杂度 O(n)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll NEG = LLONG_MIN / 4; // "不可达"标记,加 a[i] 也不会溢出
int main() {
int n, L, R;
cin >> n >> L >> R;
vector<ll> a(n + 1), f(n + 1, NEG);
for (int i = 1; i <= n; ++i) cin >> a[i];
f[0] = 0; // 起点,得分为 0
deque<int> q; // 存下标 j,f 值从队头到队尾单调递减
// 不要把 0 提前塞进队列:j = 0 只有在 i == L 那一轮才合法(一步正好跳 L 格)
for (int i = 1; i <= n; ++i) {
// ① j = i - L 从这一步起才允许用来跳到 i,进队
if (i - L >= 0 && f[i - L] != NEG) {
while (!q.empty() && f[q.back()] <= f[i - L]) q.pop_back();
q.push_back(i - L);
}
// ② j < i - R 跳得太远,够不到 i,出队
while (!q.empty() && q.front() < i - R) q.pop_front();
// ③ 队头就是 [i-R, i-L] 里 f 最大的那个
if (!q.empty()) f[i] = f[q.front()] + a[i];
}
cout << (f[n] == NEG ? -1 : f[n]) << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
f[i] = f[q.front()] + a[i] | 原来要对 j 扫一遍 max,现在队头 O(1) 给出最大值。转移从 O(R) 降到 O(1)。 |
if (i - L >= 0 && f[i - L] != NEG) | 入队时机是本题的关键:j 只有在 j ≤ i−L 时才允许跳到 i,所以每轮新加入的候选恰好是 i−L 一个;起点 0 会在 i = L 那一轮自动进队。 |
f[q.back()] <= f[i - L] | 队尾的 f 值不更大就弹:它既不更优、又比新来的先过期,永远用不上。 |
q.front() < i - R | 过期条件用 <:j = i−R 这一步跳 R 格,正好合法,不能弹。 |
f[i - L] != NEG | 不可达状态不能入队。否则 NEG 加几次之后会变成一个大负数,最后判 f[n] == NEG 失效,输出一个天文数字。 |
const ll NEG = LLONG_MIN / 4 | 用“足够小的数”当不可达标记。直接用 LLONG_MIN,加上 a[i] 会下溢成正数。 |
WA,答案整体错位一格。—— 单调栈里存了值而不是下标。一旦出现重复值,或者题目要求输出位置,就彻底失效。
WA,只在有重复值的数据上错。—— 求“第一个严格大于”却写成 while (a[stk[top]] <= a[i])。相等的候选被提前弹掉,答案会记成一个“不大于它”的位置。样例通常没有重复值,看不出来。
WA,窗口答案偶尔偏小。—— 滑动窗口忘了弹队头,或者过期条件写成 q.front() < i - k。过期的元素还霸着队头,后面几个窗口全部被一个“幽灵”污染。用 n=2, k=1, a=3 5 一试就知道。
WA,第一行少输出一个数。—— 输出条件写成 i > k - 1,应该是 i >= k - 1。
TLE,明明复杂度是 O(n)。—— 3×106 的输入用 cin 却没关同步;或者把“弹队尾”写成了每次扫一遍窗口找最小值(那就退回 O(nk))。
RE(数组越界)。—— MAXN 开成 106 而 n 到 3×106;或者空队列时调用 q.front()(deque 空时取值是未定义行为,多数时候读到垃圾值)。
WA,第二组数据开始全错。—— 多测没有清空栈 / 队列 / top 指针。上组数据的残留让第一组之后的答案全歪。
WA,大测试点变成负数。—— 柱状图面积写成 h[i] * (R[i] - L[i] - 1) 而没转 long long,109 乘 105 直接溢出成负数。
关掉页面,从空文件把 P5788 的完整代码敲一遍并提交到洛谷。
改一改 ①:输入 4
1 4 4 5,正确答案是 2 4 4 0。把弹栈条件 < 改成 <=,答案变成 2 3 4 0——亲手看清“严格大于”只差这一个等号。
改一改 ②:把栈里存的下标改成存值(stk[++top] = a[i]),再用它当答案数组的下标——ans[stk[top]] 立刻越界(RE)。答案要的是位置,所以只能存下标。
改一改 ③:滑动窗口输入 2 1 / 3 5,正确输出 3 5;把 q.front() <= i - k 改成 q.front() < i - k,输出变成 3 3。
改一改 ④:DP 的入队下标从 i - L 改成 i - L - 1,允许的步长整体变成 L+1 … R+1——最小的那一步 L 被禁掉了。用 L=2, R=3 的数据跑一下,答案可能变大、可能变小、也可能直接变成 -1。
均摊 O(n) 怎么证。用会计法:给元素 i 进栈时预存 2 次操作额度,一次用于将来出栈、一次用于本次比较。while 循环每弹一个元素就消耗掉它进栈时存下的额度,所以 n 次扫描中 while 体执行总次数 ≤ n。单次 while 最坏 O(n),但 n 次合计 O(n)——这就是“均摊”与“最坏”的区别。
滑动窗口同理。n 个下标各入队 1 次、出队至多 1 次,总代价 ≤ 2n,与 k 无关。对比 multiset 的 O(n log k):k = 106 时 log k ≈ 20,等于凭空多 20 倍常数。
笛卡尔树。把 Li、Ri(左右第一个更矮)当作父子关系,单调栈恰好建出一棵二叉树:节点 i 的子树区间正好是 (Li, Ri),子树大小 = Ri − Li − 1。于是“以 hi 为高的最大矩形” = hi × 子树大小——柱状图问题就是笛卡尔树上的“高度 × 子树大小”。等高柱子怎么挂都不影响面积。
贡献法(本节 tag 里的第三件事)。要求“所有子数组的最小值之和”,必须让每个子数组只归属一个最小值,否则等高元素会被重复计数。规则:Li 取左边第一个严格小于 hi 的位置,Ri 取右边第一个小于或等于 hi 的位置——于是每个子数组归给它最靠右的最小值。
按这个规则,以 hi 为最小值的子数组个数 = (i − Li) × (Ri − i):左端点可选 Li+1 … i 共 i − Li 种,右端点可选 i … Ri−1 共 Ri − i 种。答案 = Σ hi × (i − Li) × (Ri − i)。注意这里是“左小于、右小于等于”的不对称写法;柱状图求面积时两边都用 ≥ 也对,因为面积与“归给谁”无关。
量级感受。n = 3×106 时,O(n²) 是 9×1012 次比较,1 GHz 也要两小时;O(n) 是 3×106 次。同一道题,两个复杂度之间差的是“能不能交”。
| 式子 | 含义 | 典型用途 |
|---|---|---|
| Ri − Li − 1 | 以 hi 为高的矩形宽度 | 柱状图最大矩形 |
| (i − Li) × (Ri − i) | 以 hi 为最小值的子数组个数 | 所有子数组最小值之和 |
| Σ (i − Li)(Ri − i) | 应等于子数组总数 n(n+1)/2 | 贡献法写完后的自检 |
某地有 N 个能量发射站排成一行,第 i 个发射站的高度是 hi、能量值是 vi,所有 hi 互不相同。每个发射站同时向左右两边发射能量,能量只被两边最近的、且比它高的发射站接收;如果那边不存在更高的发射站,这份能量就损失掉。
一个发射站收到的能量,是所有能到达它的发射站发出的能量之和。求收到的能量最多的那个发射站收到了多少。
输入:第一行一个整数 N(1 ≤ N ≤ 106);接下来 N 行,每行两个整数 hi、vi(1 ≤ hi, vi ≤ 109),数据保证所有 hi 互不相同。
输出:一行一个整数,表示最大的接收能量。
每个发射站只关心两件事:左边第一个比它高的、右边第一个比它高的。不用管能量会不会传到第三个人。
“左边第一个比它高的”,和本节例题“右边第一个比它大的”,是同一个模板把方向反过来。
从左往右跑一遍、从右往左跑一遍,两次都往一个累加数组里加。接收的能量总和可能到 1015。
把任务拆成两半:对每个 i,找到 左边第一个 h 比自己大的位置 p,让 rec[p] += vi;再找右边第一个比自己大的位置 q,让 rec[q] += vi。两遍互不干扰。
求“左边第一个更大”用单调栈:从左往右扫,维护一个高度递减的下标栈。遇到 i 时,把栈里所有高度小于 hi 的弹掉——它们挡不住 i,而且已经被 i 截住了,以后不可能再成为任何人的“最近的更高者”。
弹干净之后,栈顶就是左边第一个比 hi 高的位置,把 vi 记在它头上。
为什么栈顶一定对:设 j 是 i 左边第一个比 i 高的位置。若 j 被弹出,只能是被某个 k(j < k < i)以 hk > hj > hi 弹掉的,那 k 比 j 更靠右且也比 i 高,与 j 的“第一个”矛盾。所以 j 一定还在栈里,且栈里没有比它更靠右的合法候选,它就是栈顶。
右边同理,从右往左再跑一遍同一个模板,栈清空后重新用。
复杂度 O(N):每个下标进栈一次、出栈至多一次,两遍合计 4N 次操作,N = 106 轻松通过。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1000005;
int n, h[MAXN], v[MAXN];
int stk[MAXN], top = 0; // 单调栈:存下标,高度从栈底到栈顶递减
ll rec[MAXN]; // rec[i] = 第 i 个发射站收到的总能量
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d%d", &h[i], &v[i]);
// 第一遍:每个站把能量交给左边最近的更高者
for (int i = 1; i <= n; ++i) {
while (top > 0 && h[stk[top]] < h[i]) --top; // 更矮的挡不住我,弹掉
if (top > 0) rec[stk[top]] += v[i]; // 栈顶就是左边第一个更高的
stk[++top] = i; // 自己进栈
}
// 第二遍:反方向再来一次,处理右边
top = 0;
for (int i = n; i >= 1; --i) {
while (top > 0 && h[stk[top]] < h[i]) --top;
if (top > 0) rec[stk[top]] += v[i];
stk[++top] = i;
}
ll best = 0;
for (int i = 1; i <= n; ++i) best = max(best, rec[i]); // 注意用 ll 接
printf("%lld\n", best);
return 0;
}| 片段 | 作用 |
|---|---|
while (top > 0 && h[stk[top]] < h[i]) --top; | 高度严格小于就弹。本题保证高度互不相同,所以不会遇到“相等该不该弹”的问题;如果高度可能相等,题意“比它高”要求不弹等高的那个。 |
if (top > 0) rec[stk[top]] += v[i]; | 弹干净后栈顶是左边第一个更高的站。注意是 rec += v[i]:能量记在接收方头上。 |
stk[++top] = i; | 自己入栈。能量已经交出去了,入栈是为了将来接收右边那些比它矮的站交来的能量。 |
top = 0; 第二遍循环 | 同一个模板反着跑。清空栈这一步最容易漏,漏了会把上一遍的残留混进来。 |
ll rec[MAXN]; | 一个站最多收 106 份、每份 109,总和 1015,int 直接爆。 |
printf("%lld\n", best); | long long 必须配 %lld。用 %d 打印只会输出低 32 位,等于随机数。 |