题单 · 习题解析

洛谷 P2866 [USACO06NOV] Bad Hair Day S

★★★ 关键的一步是**换主语** —— 题面问「我能看到几头」,而单调栈一句话能数的是「几头能看到我」(同一批可见对,换个分组方式);⚠⚠ 而那句 `ans += 栈的大小` **是有方向的**:本页作者第一版就把它反着扫了,对拍当场抓出 274/300 —— ★ 说清楚之后一切都白送:**它算的是把整排牛前后颠倒之后的答案** ⇒ 回文档是能证的 0(⚠ 而反过来不成立,62 轮相同里只有 13 轮是回文);★★ 「要不要 long long」那条线是 **n = 65537**,和[第 11 章 P1908](/sol/p1908/) **一字不差**(上界都是 n(n−1)/2);★★★ 而对拍和顶格随机**两条路一起堵死** —— 小数据结构上够不着 2³¹,顶格随机答案只有 692 240、差最坏形状 **4622 倍**;★★ 官方样例**四个错法一个没挡住**,三个「放过」的原因分属三类:数值巧合 / 结构问不出 / 规模不够

原题:洛谷 P2866出自 第 35 章 单调栈与单调队列 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

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.cpp★ 这一版就能 AC(顶格 n = 8×10⁴,本机 3 毫秒)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 为什么要换主语:因为只有换过来,单调栈才是一句话

题面问的是 Σᵢ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⚠⚠ 而这个「一句话」是有方向的 —— 这是写这一页时当场踩的坑

p2866Rev.cpp✗ 同一行代码反着扫(官方样例照样打出 5)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 我第一版就是这么写的,对拍当场抓了出来

「从左往右数几头能看到我」和「从右往左数我能看到几头」听着完全对称 —— 于是我把同一句 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, 得去找右边第一个不比我矮的下标再相减:

p2866Direct.cpp★ 另一条正确的路:Cᵢ = (右边第一个不比我矮的下标)− i − 1
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

两版主语相反、公式不同、一行代码不共享,而 1200 轮加顶格两种形状逐字节相同 ⇒ 拿它当顶格也跑得动的参照物正合适。

3★★ 要不要 long long:那条线是 n = 65537,而它不是这道题的性质

★★ 和第 11 章 P1908 那条线一字不差 —— 因为上界是同一个式子

Σ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,同样跨题不变)。

p2866Int.cpp✗ 答案用 32 位(顶格严格递减打出 -1095007296)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 而对拍结构上抓不到它,同时「顶格随机」也抓不到 —— 两条路都堵死了
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⚠ 弹栈少个等号:题面那句话里的「>」是严格的

p2866Ge.cpp✗ 等高的没弹掉(官方样例照过 —— 那六个数互不相同)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面写死了 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)和「一个没挡住」(P3371P2865),这一页是后者, ⚠ 而值得单说的是:三个「放过」的原因分属三类 —— 一次是数值上的巧合、一次是结构上问不出、一次是规模不够。

6度量程序和生成器

p2866Count.cpp度量程序(本页所有数字都出自它)
p2866Gen.cpp(六个档位)数据生成器
p2866Brute.cpp参照物:照题面那句话逐字翻译

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 倍)
★★ 官方样例 四个错法一个没挡住,而三个「放过」分属三类原因:巧合 / 结构 / 规模