0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2866,日期见页头。两边不一致时信原站。
题目描述
农夫约翰有 N 头奶牛正在过乱头发节。
每一头牛都站在同一排面朝右,它们被从左到右依次编号为 1, 2, ⋯, N。
编号为 i 的牛身高为 hᵢ。第 N 头牛在最前面,而第 1 头牛在最后面。
对于第 i 头牛前面的第 j 头牛,如果 hᵢ > hᵢ₊₁, hᵢ > hᵢ₊₂, ⋯, hᵢ > hⱼ,
那么认为第 i 头牛可以看到第 i+1 到第 j 头牛。
定义 Cᵢ 为第 i 头牛所能看到的牛的数量。请帮助农夫约翰求出 C₁ + C₂ + ⋯ + C_N。
输入格式
输入共 N + 1 行。第一行为一个整数 N,代表牛的个数。
接下来 N 行,每行一个整数 aᵢ,分别代表第 1, 2, ⋯, N 头牛的身高。
输出格式
输出共一行一个整数,代表 C₁ + C₂ + ⋯ + C_N。
数据规模与约定
对于 100% 的数据,保证 1 ≤ N ≤ 8 × 10⁴,1 ≤ hᵢ ≤ 10⁹。
时限 1 秒,内存 125 MB。
输入输出样例
输入
6 10 3 7 4 12 2
输出
5
身高 10 3 7 4 12 2:第 1 头(10)看到 3、7、4 共 3 头,被 12 挡住;
第 3 头(7)看到 4 共 1 头;第 5 头(12)看到 2 共 1 头;其余为 0。
合计 5。
⚠ 这六个数互不相同,而且它正着数和倒着数一样大 —— 第 ②③ 步都要用这两句话。
1★★★ 关键的一步是换一个主语:从「我能看到几头」换成「几头能看到我」
// P2866 [USACO06NOV] Bad Hair Day —— ★ 这一版就能 AC//// ★★★ 关键的一步是**换一个主语**:题面问「第 i 头能看到几头」,// 而单调栈一句话能数的是「第 j 头**被几头看到**」——// 两个和必然相等(数的是同一批「可见的有序对 (i, j)」,只是先按 i 分组还是按 j 分组)。//// 从左往右扫,栈里存**还没被挡住的**牛(身高自栈底向上递减)。// 新来一头 i:把「不比它高」的全弹掉(那些牛被 i 挡死了);// ⇒ **剩在栈里的每一头都看得见 i** ⇒ ans += 栈的大小。//// ⚠ 这个「一句话」只对「被几头看到」成立 —— 换成「我能看到几头」就必须去找// 「右边第一个不比我矮的位置」再相减(见 p2866Direct.cpp),// 而**照抄同一句话、只把方向反过来**就是 p2866Rev.cpp 那个错法(本页第 ② 步)。//// ⚠ 答案要开 long long:顶格 n = 8×10⁴ 全严格递减 ⇒ n(n−1)/2 ≈ 3.2×10⁹(本页第 ③ 步)。#include <bits/stdc++.h>using namespace std;
static const int N = 80005;static int h[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 >> h[i];
long long ans = 0; int top = 0; for (int i = 1; i <= n; i++) { while (top && h[stk[top]] <= h[i]) top--; // 比我矮或一样高的,看不到我后面 ans += top; // 栈里剩的都比我高 ⇒ 它们都看得到我 stk[++top] = i; } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
题面问的是 ΣᵢCᵢ,Cᵢ = 第 i 头能看到几头。
可它要的是一个总和 —— 而总和数的是「可见的有序对 (i, j)」这一批东西,
按 i 分组和按 j 分组,加起来必然一样多。
按 j 分组之后,单调栈就只剩一句话了:
while (top && h[stk[top]] <= h[i]) top--; // 被 i 挡死的,弹掉
ans += top; // ★ 剩在栈里的每一头都看得见 i
stk[++top] = i;⇒ 栈里存的是「还没被挡住的牛」,身高自栈底向上递减。 ★ 连宽度都不用算 —— 这是本章第 5 步那套模板里最省事的一种用法。
2⚠⚠ 而这个「一句话」是有方向的 —— 这是写这一页时当场踩的坑
// P2866 · 错法 ①:把正解那句话原封不动地**反着扫一遍**//// 正解(p2866.cpp):从左往右,弹掉不比我高的,ans += 栈的大小// 这一版 :从右往左,同样一行不改//// ⚠⚠ 这是写这一页时**当场踩的坑** —— 它看着完全对称,其实不是:// 「ans += 栈的大小」数的是「**被**几头看到」,而这个量本身是有方向的。// ★ 说清楚它算了什么:**它给出的是把整排牛前后颠倒之后的正确答案**// ⇒ 在**回文**的身高序列上它恒等于正解(本页第 ② 步,那一档是能证的精确 0)。#include <bits/stdc++.h>using namespace std;
static const int N = 80005;static int h[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 >> h[i];
long long ans = 0; int top = 0; for (int i = n; i >= 1; i--) { while (top && h[stk[top]] <= h[i]) top--; // ★ 等高也要弹:题面要的是严格大于 ans += top; // 弹完之后栈里剩几个,就能看到几头 stk[++top] = i; } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
「从左往右数几头能看到我」和「从右往左数我能看到几头」听着完全对称 ——
于是我把同一句 ans += top 反着扫了一遍。四个档里三个当场爆掉。
★ 说清楚它算了什么,一切就都清楚了: 从右往左跑这套代码,等于把整排牛前后颠倒之后跑正解 ⇒ ⇒ 它给出的是「反过来站」那道题的答案。
| 300 轮 | 档 0 | 档 1 互不相同 | 档 2 值域 1~3 | ★ 档 3 回文 |
|---|---|---|---|---|
| 身高序列是回文 | 0 | 0 | 13 | 300 |
| ⇒ 「方向反了」被抓 | 274 | 279 | 238 | ★ 0 |
★★ 档 3 那个 0 是能证的:回文序列前后颠倒还是它自己。 ⚠ 而反过来不成立 —— 档 2 里两版相同的有 62 轮,其中只有 13 轮是回文, 另外 49 轮纯属碰巧凑出同一个数。 ⇒ 「A ⇒ B」量出来是对的,别顺手当成「A ⟺ B」。
⇒ ★★ 真要从右往左数「我能看到几头」,就不能只写一句 ans += top,
得去找右边第一个不比我矮的下标再相减:
// P2866 · 另一条正确的路:直接数「第 i 头能看到几头」//// ★ 它和 p2866.cpp 的主语相反,公式也完全不同:// Cᵢ = (右边第一个**不比我矮**的下标 j)− i − 1 (不存在就当 j = n+1)// 单调栈从右往左扫,弹掉严格矮于我的,栈顶就是那个 j。//// ⇒ 两版一行代码都不共享,拿它当参照物顶格也跑得动// ([P1803](/sol/p1803/) 那条:参照物不必是暴力)。#include <bits/stdc++.h>using namespace std;
static const int N = 80005;static int h[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 >> h[i];
long long ans = 0; int top = 0; for (int i = n; i >= 1; i--) { while (top && h[stk[top]] < h[i]) top--; // 弹掉严格比我矮的 int j = top ? stk[top] : n + 1; // 右边第一个不比我矮的 ans += j - i - 1; stk[++top] = i; } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
两版主语相反、公式不同、一行代码不共享,而 1200 轮加顶格两种形状逐字节相同 ⇒ 拿它当顶格也跑得动的参照物正合适。
3★★ 要不要 long long:那条线是 n = 65537,而它不是这道题的性质
ΣCᵢ 的上界是「所有有序对」= n(n−1)/2,在严格递减的一排牛上取到。
使 n(n−1)/2 越过 2³¹−1 的最小 n |
★ 65537(那时是 2 147 516 416) |
题面的 N 上限 |
8 × 10⁴ ⇒ 越过去了,1.22 倍 |
| 顶格上界 | 3 199 960 000 = int 上限的 149% |
★★ 这条线和第 11 章 P1908(逆序对)那条一模一样 ——
因为两道题的答案上界都是 n(n−1)/2。
⇒ 又一次「它不是那道题的性质,是 int 的性质」
(P1516 / P2872 那条线是 46 341,同样跨题不变)。
// P2866 · 错法 ①:答案用 int//// ⚠ 上界是 n(n−1)/2(全体严格递减时取到):顶格 n = 8×10⁴ ⇒ 3 199 960 000,// 而 int 只到 2 147 483 647 ⇒ **1.49 倍**,刚好越过去。// ★ 那条分界线是 n = 65537(本页第 ③ 步:它和第 11 章 P1908 那条**一字不差**)。// ⚠ 而小数据对拍**结构上**抓不到它 —— 只能算。// ★ 演示用 unsigned 累加、最后按 int 打出来 —— 这样绕回的那个数是**可复现**的// (第 45 章那条:别拿 UB 派生的数字写断言)。// ⚠ 注意:顶格答案 3 199 960 000 其实还塞得进 **unsigned**(上限 42.9 亿)——// 真正装不下的是 **有符号** int,所以这一版的错误发生在**打出来那一刻**。#include <bits/stdc++.h>using namespace std;
static const int N = 80005;static int h[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 >> h[i];
unsigned ans = 0; // ✗ 32 位 int top = 0; for (int i = 1; i <= n; i++) { while (top && h[stk[top]] <= h[i]) top--; ans += (unsigned)top; stk[++top] = i; } cout << (int)ans << '\n'; // ✗ 有符号 int 装不下 return 0;}点「运行 ▶」看结果
| 300 轮 | 档 0 | 档 1 | 档 2 | 档 3 |
|---|---|---|---|---|
| 「答案用 32 位」被抓 | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
对拍的 n 只有 5~10 ⇒ 答案撑死几十,结构上够不着 2³¹(加轮数一点用没有)。
那顶格随机呢?——也不行:
顶格 n = 8 × 10⁴ |
答案 |
|---|---|
| 严格递减(最坏形状) | 3 199 960 000 ⇒ 溢出 |
| ⚠ 随机身高 | 692 240 ⇒ 差 ★ 4622 倍,int 绰绰有余 |
⇒ ★★★ 又一次「顶格 ≠ 最坏」,而这一次它决定的是要不要换类型 ——
大而不对形状的数据,会让你得出「int 够用」这个结论。
⇒ 所以这件事只能算,不能测:8×10⁴ × 8×10⁴ / 2 是一道三十秒的乘法。
★ 顺带一句,同一件事也决定了「暴力能不能过」:
A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 顶格 n = 8×10⁴ · 时限 1 秒 |
单调栈 | O(n²) 暴力 |
|---|---|---|
| 随机身高 | 4 ms | ⚠ 4 ms(它在随机数据上是线性的) |
| 严格递减(最坏形状) | ★ 3 ms | 773 ms ⇒ 余量只剩 1.3 倍 |
⇒ 这道题的 n 只有 8×10⁴,所以暴力在最坏形状上本机居然差一点就过了 ——
⚠ 而评测机通常更慢。「差一点」不是能过。
4⚠ 弹栈少个等号:题面那句话里的「>」是严格的
// P2866 · 错法 ②:弹栈条件少了等号 —— 以为「一样高也看得过去」//// while (top && h[stk[top]] < h[i]) top--; ← 错(等高的留在栈里,被当成能看到的)// while (top && h[stk[top]] <= h[i]) top--; ← 对//// ⚠ 题面写得很死:`hᵢ > hᵢ₊₁, hᵢ > hᵢ₊₂, …` —— **严格大于**,等高就挡住了。// ⇒ 触发条件只有一句:**输入里得有两头一样高的牛**(本页第 ④ 步量了这条曲线)。#include <bits/stdc++.h>using namespace std;
static const int N = 80005;static int h[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 >> h[i];
long long ans = 0; int top = 0; for (int i = 1; i <= n; i++) { while (top && h[stk[top]] < h[i]) top--; // ✗ 少了那个等号 ans += top; stk[++top] = i; } cout << ans << '\n'; return 0;}点「运行 ▶」看结果
题面写死了 hᵢ > hᵢ₊₁, hᵢ > hᵢ₊₂, … ⇒ 一样高的牛就把视线挡住了。
| 300 轮 | 档 0 身高 1~20 | ★ 档 1 互不相同 | ★ 档 2 身高 1~3 | 档 3 回文 |
|---|---|---|---|---|
| 输入里有两头一样高(第一层) | 218 | ★ 0 | 300 | 300 |
| ⇒ 真被抓(第二层) | 135 | ★ 0 | 298 | 270 |
| 比 | 1.6 倍 | — | 1.01 倍 | 1.11 倍 |
★ 档 1 那个 0 是结构性的(身高是 1..n 的排列,压根没有相等)——
它同时给这一列当了自检:另外三档 135 / 298 / 270,说明这份对拍是活的。
★★ 而第一层和第二层的差距随值域收窄而收窄(1.6 → 1.01 倍)——
值域越小,「一样高」就越容易正好卡在视线上。
5★ 对拍这一页
参照物是照题面那句话逐字翻译的 O(n²) 暴力。
300 轮(n 随机 5~10) |
档 0 | ★ 档 1 互不相同 | ★★ 档 2 身高 1~3 | ★ 档 3 回文 |
|---|---|---|---|---|
| 数「几头看到我」(正解) | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 数「我看到几头」(另一条路) | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 答案用 32 位 | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 弹栈少个等号 | 135 | ★ 0 | 298 | 270 |
| 方向反了 | 274 | 279 | 238 | ★ 0 |
| 错法 | 抓获率(最狠的那一档) | 官方样例挡住了吗 | 为什么 |
|---|---|---|---|
| 方向反了 | 279 / 300 | ⚠ 放过 | 10 3 7 4 12 2 正着数和倒着数都是 5(碰巧) |
| 弹栈少个等号 | 298 / 300 | ⚠ 放过 | 那六个数互不相同,等号根本用不上 |
| 答案用 32 位 | 顶格才现形 | ⚠ 放过 | 答案才 5 |
⇒ 这是「样例是个『一测就死』的过滤器」那条规律的另一个极端 —— 本书前面拿到过「四个全挡住」(P1266)和「一个没挡住」(P3371、 P2865),这一页是后者, ⚠ 而值得单说的是:三个「放过」的原因分属三类 —— 一次是数值上的巧合、一次是结构上问不出、一次是规模不够。
6度量程序和生成器
7一页纸
| ★★★ 关键的一步 | 换主语:从「我能看到几头」换成「几头能看到我」⇒ 单调栈只剩一句 ans += top |
| ⚠⚠ 而那句话有方向 | 同一行反着扫算的是反过来站那道题的答案 —— 274 / 300(★ 这是本页作者当场踩的) |
| ★ 回文档那个 0 能证 | 回文序列颠倒还是它自己;⚠ 而反过来不成立(62 轮相同里只有 13 轮是回文) |
| ★★ long long 那条线 | n(n−1)/2 越过 2³¹ 的最小 n = 65537,和 P1908 一字不差 |
| ★★★ 两条路一起堵死 | 对拍小数据结构上够不着;而顶格随机答案只有 692 240,差最坏形状 4622 倍 |
| ⚠ 等号是严格的 | 一样高就挡住视线;第一层 218 / 第二层 135,值域越小两层越贴近(1.6 → 1.01 倍) |
| ★★ 官方样例 | 四个错法一个没挡住,而三个「放过」分属三类原因:巧合 / 结构 / 规模 |