0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P5788,日期见页头。两边不一致时信原站。
题目背景
模板题,无背景。
2019.12.12 更新数据,放宽时限,现在不再卡常了。
题目描述
给出项数为 n 的整数数列 a₁…ₙ。
定义函数 f(i) 代表数列中第 i 个元素之后第一个大于 aᵢ 的元素的下标,
即 f(i) = min{ j | i < j ≤ n, a_j > aᵢ }。若不存在,则 f(i) = 0。
试求出 f(1…n)。
输入格式
第一行一个正整数 n。
第二行 n 个正整数 a₁…ₙ。
输出格式
一行 n 个整数表示 f(1), f(2), …, f(n) 的值。
数据规模与约定
对于 30% 的数据,n ≤ 100;
对于 60% 的数据,n ≤ 5 × 10³;
对于 100% 的数据,1 ≤ n ≤ 3 × 10⁶,1 ≤ aᵢ ≤ 10⁹。
时限 1 秒,内存 125 MB。
输入输出样例
输入
5 1 4 2 3 5
输出
2 5 4 5 0
1 后面第一个更大的是 4(下标 2);4 后面第一个更大的是 5(下标 5);
2 后面是 3(下标 4);3 后面是 5(下标 5);5 后面没有了 ⇒ 0。
⚠ 这五个数互不相同 —— 记住这句话,第 ③ 步要用它。
1第一版:照定义两重循环
// P5788 的参照物:照定义两重循环 —— O(n²)//// 题面给的 60% 那一档是 n ≤ 5×10³ ⇒ 这一版**稳拿 60 分**(2.5×10⁷ 次比较)。// 而 100% 那档 n = 3×10⁶,它要 4.5×10¹² 次 —— 差六个数量级。#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<int> a(n + 1); for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= n; i++) { int ans = 0; for (int j = i + 1; j <= n; j++) if (a[j] > a[i]) { ans = j; break; } cout << ans << (i == n ? '\n' : ' '); } return 0;}点「运行 ▶」看结果
题面把档次分得很清楚:60% 的数据 n ≤ 5 × 10³。
5000² / 2 = 1.25 × 10⁷ 次比较 ⇒ 这一版直接交上去就有 60 分,一分钟写完。
顺手照题面顶格随机造一份数据(n = 3 × 10⁶、aᵢ ≤ 10⁹),拿这个 O(n²) 暴力跑一遍:
200 毫秒。跑完了,而且答案全对。
时限是 1 秒。⇒ 照这个结果,你会得出「暴力就够了,单调栈是多余的」这个结论 —— 而它是错的。
数「内层那个 for 一共比较了多少次」(p5788Count.cpp 的 brute 那一行):
n |
随机数据 | 严格递减 | 倍数 |
|---|---|---|---|
| 10 000 | 90 943 | 49 995 000 | 550 |
| 20 000 | 184 975 | 199 990 000 | 1081 |
| 40 000 | 382 972 | 799 980 000 | 2089 |
★ 随机那一列 n 翻倍、次数也只翻一倍(×2.03、×2.07)—— 它在随机数据上根本就是线性的,
每个位置平均只往右看 9.5 步就撞见更大的数了。
★★ 而严格递减那一列每次 ×4.00,是干干净净的 O(n²)。
⇒ 倍数在往上走(550 → 1081 → 2089),这就是 O(n²) 的签名(P5019 那条)。
顶格 n = 3 × 10⁶ 上把这两条外推:随机约 2.9 × 10⁷ 次、严格递减 4.5 × 10¹² 次
⇒ 差 十五万倍,后者本机要跑将近 40 分钟。
⇒ ★★★ 这又是一次「顶格 ≠ 最坏」,但这一次的方向对人最危险: 前面几次是「顺手随机跑得动、真数据跑不动」,这一次是「顺手随机让你以为不用学这一章」。
2★ 关键一步:倒着扫,栈里存下标
// P5788【模板】单调栈 —— ★ 这一版就能 AC//// f(i) = 第 i 个元素之后**第一个大于 a[i]** 的下标,不存在为 0。//// ★ 关键一步(本章第 5 步):倒着扫,栈里存**下标**,栈里的值自栈顶向下递增。// 要给 i 找答案,就把栈顶那些「不比 a[i] 大」的全弹掉 —— 它们永远轮不到了:// 任何比 i 更靠左的位置要往右看,都会先撞上 a[i]。//// ⚠ 这道题真正的关卡不是算法,是 I/O:n = 3×10⁶ ⇒ 输入约 32 MB、输出约 20 MB,// 时限 1 秒。所以这一版读和写都是手写的(见本页第 ⑤ 步那张表)。#include <bits/stdc++.h>using namespace std;
static const int N = 3000005;static int a[N], f[N], stk[N];
/* ⚠ 缓冲区是「读完就续」的窗口,不是「一口气全塞进来」—— 第 12 章 P1010 那一跤:拿一个固定大小的大数组去装整份输入, 一旦估小了就**一声不吭地少读一截**,而所有版本一起错、对拍全绿。 */static char ibuf[1 << 16];static int ipos, ilen;static inline char gc() { if (ipos == ilen) { ilen = (int)fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (ilen <= 0) return 0; } return ibuf[ipos++];}static inline int rd() { char c = gc(); while (c && (c < '0' || c > '9')) c = gc(); int x = 0; while (c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = gc(); } return x;}
static char obuf[1 << 16];static int opos;static inline void flushOut() { fwrite(obuf, 1, opos, stdout); opos = 0; }static inline void pc(char c) { if (opos == (int)sizeof(obuf)) flushOut(); obuf[opos++] = c; }static inline void wr(int x) { if (!x) { pc('0'); return; } char t[12]; int c = 0; while (x) { t[c++] = char('0' + x % 10); x /= 10; } while (c) pc(t[--c]);}
int main() { int n = rd(); for (int i = 1; i <= n; i++) a[i] = rd();
int top = 0; for (int i = n; i >= 1; i--) { while (top && a[stk[top]] <= a[i]) top--; // ★ 相等也要弹:题面要的是「大于」 f[i] = top ? stk[top] : 0; stk[++top] = i; }
for (int i = 1; i <= n; i++) { wr(f[i]); pc(i == n ? '\n' : ' '); } flushOut(); return 0;}点「运行 ▶」看结果
倒着扫。栈里存下标,对应的值自栈顶向下递增。
轮到 i 的时候,把栈顶那些「不比 aᵢ 大」的全弹掉 —— 它们这辈子都轮不到了:
任何比 i 更靠左的位置往右看,都会先撞上 aᵢ。
弹完之后栈顶要么空(f(i) = 0),要么就是第一个大于 aᵢ 的位置。
⚠ 那个 <= 里的等号是有讲究的:题面要的是「大于」,所以相等的也得弹掉。
第 ③ 步整整一步都在讲少了这个等号会怎样。
本章第 6 步给的理由是均摊:每个下标一辈子只被压进去一次、也只能被弹出来一次。
这句话可以直接数(p5788Count.cpp 的 amort 那一行):
顶格 n = 3 × 10⁶ |
|
|---|---|
| 弹栈总次数 | 2 999 983 |
n |
3 000 000 |
★ 少的那 17 次,就是跑完之后还留在栈里的那 17 个下标(它们一次都没被弹过)。
⇒ 不是「大约 O(n)」,是严格小于 n。
3★★★ 少一个等号:顺手数据抓不到,题面顶格也抓不到 —— 而两个 0 的原因不一样
// P5788 · 错法 ①:弹栈条件写成**严格小于** —— 相等的不弹//// while (top && a[stk[top]] < a[i]) top--; ← 错// while (top && a[stk[top]] <= a[i]) top--; ← 对//// ⇒ 它求的是「第一个**大于等于** a[i] 的下标」,而题面要的是「第一个**大于**」。// ⚠ 触发条件只有一句话:**存在 j > i,a[j] == a[i],而且 j 是第一个 ≥ a[i] 的位置**。// ⇒ 序列里没有重复元素时,它和正解**逐字节相同**(本页第 ③ 步量了这条曲线)。#include <bits/stdc++.h>using namespace std;
static const int N = 3000005;static int a[N], f[N], stk[N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; for (int i = 1; i <= n; i++) cin >> a[i];
int top = 0; for (int i = n; i >= 1; i--) { while (top && a[stk[top]] < a[i]) top--; // ✗ 少了那个等号 f[i] = top ? stk[top] : 0; stk[++top] = i; }
for (int i = 1; i <= n; i++) cout << f[i] << (i == n ? '\n' : ' '); return 0;}点「运行 ▶」看结果
它和正解只差一个字符:a[stk[top]] < a[i] 少了个 =。于是它求的是「第一个大于等于」。
第一层最好写,也最没用:序列里得有重复元素。
可这一层在题面顶格上几乎必然成立 —— 生日悖论:3 × 10⁶ 个数扔进 10⁹ 的值域,
期望有 C(n,2)/R ≈ 4499 对重复。实测那份顶格数据里有 4580 对。
而两版在那份数据上逐字节相同。
真正的触发条件要多一句话:那两个相等的数之间,不能有更大的数 —— 只有这样,「第一个 ≥」才会落在那个相等的位置上。它的期望个数是一道能算的题:
位置
i上的值是v时,右边第一个≥ v的元素恰好等于v的概率 ≈1/(R − v + 1)⇒ 期望位置数 ≈n · H(R) / R ≈ n · ln R / R。
顶格 n = 3 × 10⁶ |
值域 10⁹(题面顶格) |
值域 10⁶ |
|---|---|---|
| 重复对数(实测 / 期望) | 4580 / 4499 | 4 497 615 / 4 499 998 |
| 真正触发的位置数(算出来的期望) | 0(0.06) | 54(43) |
| ⇒ 两版逐字节比 | ★ 相同 | ★ 不同 |
⇒ ★★★ 第一层满足了 4580 次,而 bug 一次都没被触发。 这是本书那条曲线上最极端的一点 —— B3637 那次第一层 300 / 第二层 2(差 150 倍), 这一次第一层 4580 / 第二层 0。 ⇒ ★★ 第一层写得越「显然」,越要提防它一点区分度都没有; 而这一页还多给了一句:那个 0 是能算出来的(期望 0.06),所以它不是运气。
aᵢ ≤ 10⁹ 是上界,题目从没保证 aᵢ 撒得很开。
同样的 n,值域压到 10⁶(完全在题面之内),顶格那份数据里就有 54 个位置两版不同。
小数据上更是一抓一个准:
| 300 轮 | 档 0 顺手(值域 10⁹) |
★★ 档 1 值域 1~5 | 档 2 严格递增 | 档 3 严格递减 |
|---|---|---|---|---|
| 序列里有重复元素(第一层) | ★ 0 | 300 | ★ 0 | ★ 0 |
| 真正的触发条件(第二层) | ★ 0 | 296 | ★ 0 | ★ 0 |
| ⇒ 真被抓 | ★ 0 | ★ 296 | ★ 0 | ★ 0 |
★★ 第二层和抓获数一个不差(296 ≡ 296)—— 本书「触发条件 ≡ 抓获数」又中一次,
⚠ 而第一层差了 4 轮:「有重复元素」和「重复的那对之间没有更大的」不是一回事。
★ 档 0 那个 0 是结构性的:n ≤ 12 个数扔进 10⁹ 的值域,撞上的概率是百万分之几。
⇒ 顺手写生成器时随手照抄题面的值域上限,正好把这个 bug 的藏身处让了出来。
4★★ while 降成 if:本章第 6 步那句话的反面,两个「精确的 0」原因还不一样
// P5788 · 错法 ②:把 while 改成 if —— 一次只弹一个//// ⚠ 这个错法的来处很具体:本章第 6 步那句「for 里套 while,为什么还是 O(n)」// 正是给它准备的。看不懂均摊分析的人,很容易「为了保住 O(n)」把 while 降成 if。//// ⇒ 它一次只丢掉一个候选,于是栈里会留下比 a[i] 矮的元素,f[i] 指到它头上就错了。// 触发条件:某一步**需要连着弹两个或更多**。#include <bits/stdc++.h>using namespace std;
static const int N = 3000005;static int a[N], f[N], stk[N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; for (int i = 1; i <= n; i++) cin >> a[i];
int top = 0; for (int i = n; i >= 1; i--) { if (top && a[stk[top]] <= a[i]) top--; // ✗ while 降成了 if f[i] = top ? stk[top] : 0; stk[++top] = i; }
for (int i = 1; i <= n; i++) cout << f[i] << (i == n ? '\n' : ' '); return 0;}点「运行 ▶」看结果
本章第 6 步专门解释过「for 里套 while 为什么还是 O(n)」。
没看懂那段均摊分析的人,很容易「为了保住 O(n)」主动把 while 降成 if。
⇒ 它是看懂了复杂度、没看懂均摊的人才会写出来的错法 —— 正因如此才值得单列一步。
草稿里写着「严格递减的序列上每一步都要把整个栈弹空 ⇒ if 版必错 300 / 300」。
实测 0 / 300。
一行就能证明为什么:严格递减的序列倒着扫时,栈里永远只有一个元素 ——
每一步弹掉那唯一的一个、再压进自己。⇒ 弹一个就等于弹光,if 和 while 完全一样。
⇒ 于是这一页有两个能证的 0,而原因正好相反:
| 300 轮 | 档 2 严格递增 | 档 3 严格递减 |
|---|---|---|
| 「需要连弹两个」的组数 | ★ 0 | ★ 0 |
| 为什么 | 栈里的值全都大于 aᵢ ⇒ 一次都不弹 |
栈里永远只有一个元素 ⇒ 弹一个就到底 |
★★ 两个 0 长得一模一样,成因一个是「从不弹」、一个是「弹一下就够」。 ⇒ 又一次「说清楚它算了什么,比说它错了有用得多」。
| 300 轮 | 档 0 顺手 | 档 1 值域 1~5 | 档 2 递增 | 档 3 递减 |
|---|---|---|---|---|
| 「某一步需要连弹 ≥ 2 个」 | 284 | 295 | ★ 0 | ★ 0 |
| ⇒ 真被抓 | 284 | 295 | ★ 0 | ★ 0 |
★★ 四个档全部一个不差(284 ≡ 284、295 ≡ 295、0 ≡ 0、0 ≡ 0)—— 对拍代码是活的,上面那两个 0 是真的。
5⚠ 这道题真正的关卡是 I/O —— 而这一页第一次让「输出」当主角
n = 3 × 10⁶,每个 aᵢ 最多十位 ⇒ 输入 29 612 405 字节(28.2 MB);
输出是 3 × 10⁶ 个下标,最多七位 ⇒ 22 888 864 字节(21.8 MB)。
⇒ ★★ 前面几章量过的都是读入那一侧(第 6 章 P2367、 第 12 章 P1226、第 32 章 P3371)—— 这道题第一次让输出和输入同一个量级。
A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 顶格 n = 3×10⁶ · 3 次取最小 |
读 | 写 |
|---|---|---|
默认 cin / cout(同步开着) |
624 ms | — |
cin 关同步 |
94 ms | 71 ms |
scanf / fprintf |
133 ms | 101 ms |
| 手写快读 / 快写 | ★ 32 ms | ★ 26 ms |
★ 那张表只量了 I/O 那一段。真正该看的是端到端(同一份单调栈,只换读写):
| 端到端 · 顶格 · 3 次取最小 | 时限 1 秒 |
|---|---|
手写快读 + 快写(p5788.cpp) |
★ 90 ms |
cin 关同步(p5788Sync.cpp) |
192 ms |
scanf / printf(p5788Scanf.cpp) |
262 ms |
⚠ 默认 cin / cout(p5788Cin.cpp) |
728 ms |
(参照:第 ① 步那个 O(n²) 暴力,关同步的 cin) |
⚠ 200 ms |
「2019.12.12 更新数据,放宽时限,现在不再卡常了。」
⇒ 这句话的意思是:在那之前,默认 cin 是过不去的。
现在本机 728 ms / 时限 1000 ms —— 余量只有 1.37 倍,而评测机通常比本机慢。
★ 而「关同步」这一行只要一句话就能拿到 3.8 倍:
ios::sync_with_stdio(false); cin.tie(nullptr);⇒ 又一次「四种读法的倍数跨题几乎不变,变的是绝对时间,而分数线在绝对时间上」:
P1803 是 2 × 10⁶ 个数 / 3 秒 ⇒ 四种全够;
P2367 是 2 × 10⁷ 个数 / 1 秒 ⇒ 连 scanf 都不够;
这道题夹在中间 —— 关了同步就轻松,不关就在悬崖边上。
★ 顺带印证了老结论:关同步的 cin 比 scanf 还快(94 vs 133 毫秒)。
⚠⚠ 但这一条余量只有 1.39 倍(2026-09-01 独占重测:98 vs 136)——
同一句话在第 37 章 P3378 那页的闸门里被 8 路并行晃翻过一次。
⇒ 所以两处闸门现在都只钉「默认 cin 比另外三种都慢一倍以上、快读最快」这些
有 2 倍以上余量的部分;这两个相邻量的先后属于「独占实测」,不进闸门
(第 278 条:秒表断言的主语是余量,不是量级)。
6★ 换一种存法值多少 —— 而这张表不量「只算法」那一列就会得出反的结论
顶格 n = 3×10⁶ |
手写 stk 数组 |
std::stack<int> |
倍数 |
|---|---|---|---|
| 端到端 | 90 ms | 90 ms | ★ 1.00 |
| ★ 只跑单调栈那一层(摘掉 I/O) | 21 ms | 25 ms | ★ 1.19 |
★★ 端到端看着「换存法一分钱不值」,可那是因为 90 毫秒里有 69 毫秒是 I/O ——
真正在比的那一段被稀释成了 1%。
⇒ 又一次「不量『只读入』那一列就会得出完全错误的结论」。
★ 而两版的答案是逐字节相同的(同一棵搜索路径,f 数组逐个相等)——
所以这张表里唯一的变量真的只有那个容器
(和 P1074 9.5 倍、P2925 1.13 倍、P2853 14 倍凑成一组:
次数和秒表量的从来不是同一件事)。
7★ 方向背成死的:模板在不同的书上朝向不同
// P5788 · 错法 ③:方向背成了死的 —— 求的是「**前面**第一个更大的」//// 单调栈的模板在不同的书上朝向不同(有的从左往右、有的从右往左),// 背了模板却没看题面那句「第 i 个元素**之后**」,就会写成这一版。//// ⚠ 它算的量是能说清楚的:g(i) = 第 i 个元素**之前**最近的、大于 a[i] 的下标。// ⇒ 严格**递增**的序列上它恒输出 0,而正解恒输出 i+1(除了最后一个)。#include <bits/stdc++.h>using namespace std;
static const int N = 3000005;static int a[N], f[N], stk[N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; for (int i = 1; i <= n; i++) cin >> a[i];
int top = 0; for (int i = 1; i <= n; i++) { // ✗ 扫描方向反了 while (top && a[stk[top]] <= a[i]) top--; f[i] = top ? stk[top] : 0; stk[++top] = i; }
for (int i = 1; i <= n; i++) cout << f[i] << (i == n ? '\n' : ' '); return 0;}点「运行 ▶」看结果
单调栈的模板有从左往右写的,也有从右往左写的。背了模板却没看题面那句
「第 i 个元素之后」,写出来的就是这一版。四个档全是 300 / 300,一测就死。
★ 它算的是什么,也能说清楚:g(i) = 第 i 个元素之前最近的、大于 aᵢ 的下标。
⇒ 在严格递增的序列上它恒输出 0,而正解恒输出 i+1。
8★ 对拍这一页
参照物就是第 ① 步那个 O(n²) 暴力 —— 题面 60% 那一档(n ≤ 5 × 10³)已经写明它跑得动。
300 轮(n 随机 8~12) |
档 0 顺手 | ★★ 档 1 值域 1~5 | ★ 档 2 严格递增 | ★ 档 3 严格递减 |
|---|---|---|---|---|
| 单调栈(正解) | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
std::stack 版 |
★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 弹栈少个等号 | ★ 0 | 296 | ★ 0 | ★ 0 |
while 降成 if |
284 | 295 | ★ 0 | ★ 0 |
| 方向反了 | 300 | 300 | 300 | 300 |
| 错法 | 抓获率(最狠的那一档) | 官方样例挡住了吗 |
|---|---|---|
| 方向反了 | 300 / 300 | ★ 挡住(打出 0 0 2 2 0) |
while 降成 if |
295 / 300 | ★ 挡住(打出 2 4 4 5 0) |
| 弹栈少个等号 | 296 / 300 | ⚠ 放过 —— 样例那五个数互不相同 |
⇒ 又一次「它过了样例的第三种原因:这组样例在结构上问不出这个问题」。
★ 而这一页把话说得更死:1 4 2 3 5 里没有重复元素 ⇒
「第一个大于」和「第一个大于等于」在这组数据上是同一件事,
不是「碰巧对上了」,是恒等。
9度量程序和生成器
10一页纸
| ★★ 关键的一步 | 倒着扫,栈里存下标;弹掉「不比 aᵢ 大」的 —— 它们永远轮不到了 |
| ★★ 均摊 | 顶格弹栈总次数 2 999 983 < n = 3 × 10⁶,不是「大约 O(n)」 |
| ★★★ 顶格 ≠ 最坏 | 暴力在顶格随机上 200 毫秒就跑完,在严格递减上 4.5 × 10¹² 次(差 15 万倍) |
| ★★★ 触发条件三层 | 「有重复」顶格满足 4580 次,而真正触发 0 次 ——★ 那个 0 算得出来(期望 0.06) |
| ★ 而它绝不是无所谓 | 值域压到 10⁶(仍在题面内)⇒ 顶格 54 个位置错、小数据 296 / 300 |
| ★★★ 草稿被打回 | 「严格递减是 if 版的死穴」错了 —— 那一档栈里永远只有一个元素,是能证的 0 |
| ★★ 触发条件 ≡ 抓获数 | 两个错法七个档一个不差(296 ≡ 296;284 / 295 / 0 / 0 全等) |
| ⚠ 真正的关卡是 I/O | 输入 28.2 MB + 输出 21.8 MB;默认 cin 端到端 728 ms / 时限 1 秒 |
| ★ 一句话拿 3.8 倍 | ios::sync_with_stdio(false); cin.tie(nullptr); —— 而关同步的 cin 比 scanf 还快 |
| ★★ 换存法 | 端到端 1.00 倍(被 I/O 稀释掉了),只算法那一层 1.19 倍 |