0题目原文(本地存了一份)
原题在洛谷上(页头有链接,它是 SPOJ 的远程评测题)。别人的网站不归我们管, 打不开、改版、题号调整都可能发生。所以每个解析页都把题面转录一份存在本地, 跟着仓库一起进版本库。
转录自洛谷 SP1805,日期见页头。两边不一致时信原站。
⚠ 题面里那张图也存了一份(public/sol/sp1805-histogram.png)——
P1228 那次的教训:只转文字等于存了半篇。
题目背景
如遇 SPOJ 评测服务不稳定,可以在洛谷 B4273 评测本题,注意 n 和 hᵢ 范围的小变化。
题目描述

如图所示,在一条水平线上有 n 个宽为 1 的矩形,求包含于这些矩形的最大子矩形面积
(图中的阴影部分的面积即所求答案)。
输入格式
有多组测试数据,每组数据占一行。输入零时读入结束。
每行开头为一个数字 n(1 ≤ n ≤ 10⁵),接下来在同一行给出 n 个数字
h₁, h₂, …, h_n(0 ≤ hᵢ ≤ 10⁹),表示每个矩形的高度。
输出格式
对于每组数据,输出最大子矩阵面积,一组数据输出一行。
说明/提示
时限 409 毫秒,内存 1.5 GB。
输入输出样例
输入
7 2 1 4 5 1 3 3 4 1000 1000 1000 1000 0
输出
8 4000
第一组 2 1 4 5 1 3 3:最大的是 4 和 5 那两根撑出来的 高 4 × 宽 2 = 8
(对比:整排取高 1 是 1 × 7 = 7,末尾两根取高 3 是 3 × 2 = 6);
第二组四根全高 1000 ⇒ 1000 × 4 = 4000。
⚠ 末尾那个 0 是结束标志,不是一组数据 —— 第 ③ 步专门讲它。
1算法本身,本章第 5~7 步已经讲完了
// SP1805 HISTOGRA —— ★ 这一版就能 AC//// 本章前半章那道题的原题:柱状图里最大的矩形。// ⚠ 但它比正文那份多了**三处不在算法里的东西**(本页第 ② 步):// ① 多组数据,读到 n = 0 才结束;② n 和那 n 个高度**在同一行**;// ③ 每组之间栈必须清空(这里靠 top = 0 重置)。//// ★ 面积要开 long long:顶格 10⁵ 根 × 高 10⁹ = 10¹⁴,int 差四个数量级(本页第 ③ 步)。#include <bits/stdc++.h>using namespace std;
static const int N = 100005;static long long h[N];static int stk[N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; while ((cin >> n) && n) { for (int i = 1; i <= n; i++) cin >> h[i]; h[n + 1] = -1; // ★ 哨兵:逼着循环末尾把栈清干净 int top = 0; // ★ 每组重置 long long ans = 0; for (int i = 1; i <= n + 1; i++) { while (top && h[stk[top]] >= h[i]) { long long H = h[stk[top]]; top--; long long left = top ? stk[top] : 0; // 左边第一个更矮的下标 ans = max(ans, H * (i - left - 1)); // ★ 宽度 = 右界 − 左界 − 1 } stk[++top] = i; } cout << ans << '\n'; } return 0;}点「运行 ▶」看结果
枚举「以第 i 根柱子的高度为高」的最大矩形 ⇒ 它能往左右伸到第一个比它矮的为止。
单调栈一遍扫完:弹栈的那一刻,右界是当前的 i、左界是弹完之后的新栈顶。
ans = max(ans, H * (i - left - 1)); // ★ 宽度 = 右界 − 左界 − 1⇒ 这道题就是本章前半章那道题的原题,正文第 5~7 步(含那个动画)讲的就是它。 ⇒ ★ 所以这一页不重讲算法,只讲它比正文那份多出来的三件事 —— 而那三件一件都不在算法里。
2★★★ 多出来的第一件:有多组数据 —— 而它派生出两个不同的 bug
// SP1805 · 错法 ①:把「有多组测试数据」看漏了 —— 只处理第一组//// ⚠ 前面三十几道题都是「一次输入一组」,读题时那半句// 「有多组测试数据,每组数据占一行。输入零时读入结束」很容易滑过去。// ⇒ 它在官方样例上打出 8 就停了(该有两行)。#include <bits/stdc++.h>using namespace std;
static const int N = 100005;static long long h[N];static int 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]; h[n + 1] = -1; int top = 0; long long ans = 0; for (int i = 1; i <= n + 1; i++) { while (top && h[stk[top]] >= h[i]) { long long H = h[stk[top]]; top--; long long left = top ? stk[top] : 0; ans = max(ans, H * (i - left - 1)); } stk[++top] = i; } cout << ans << '\n'; // ✗ 只打了一组 return 0;}点「运行 ▶」看结果
// SP1805 · 错法 ②:多组之间**忘了把栈清空**//// int top = 0; ← 这一行挪到了 while 外面//// ⇒ 第二组开始时,栈里还压着上一组留下的下标(那些下标对应的 h[] 也还是老值)。// ★ 这是「多组数据」这类题最经典的一发 WA,而且**第一组永远是对的** ——// 只有一组数据时它是**精确的 0**(本页第 ④ 步:两个 bug 共用同一个盲区)。#include <bits/stdc++.h>using namespace std;
static const int N = 100005;static long long h[N];static int stk[N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, top = 0; // ✗ top 在循环外面,组间不重置 while ((cin >> n) && n) { for (int i = 1; i <= n; i++) cin >> h[i]; h[n + 1] = -1; long long ans = 0; for (int i = 1; i <= n + 1; i++) { while (top && h[stk[top]] >= h[i]) { long long H = h[stk[top]]; top--; long long left = top ? stk[top] : 0; ans = max(ans, H * (i - left - 1)); } stk[++top] = i; } cout << ans << '\n'; } return 0;}点「运行 ▶」看结果
两个 bug 都要求「这份输入不止一组数据」—— 这是同一个第一层。可结果完全不同:
| 300 轮 | 档 0 顺手 | ★ 档 1 只有一组 | 档 2 高度大 | 档 3 高度 0~2 |
|---|---|---|---|---|
| 不止一组数据(共同的第一层) | 204 | ★ 0 | 204 | 204 |
| ⇒ 「只处理第一组」被抓 | ★ 204 | ★ 0 | ★ 204 | ★ 204 |
| ⇒ 「组间忘清空」被抓 | 76 | ★ 0 | 148 | 75 |
★★ 上面那个是一个不差的 ≡(204 ≡ 204,四档全对); 下面那个差 2.7 倍 —— ⇒ 又一次「能不能写成 ≡,取决于你第一层写得够不够细」。 ★ 而档 1(只有一组)那一列的两个 0 都是能证的:没有第二组,两个 bug 都无从发作。 ⇒ ★★ 两个 bug 共用同一个盲区 —— 顺手写生成器时只造一组数据,两个一起漏掉。
先问一句:每组跑完,栈里到底还剩什么?
结尾那个哨兵 h[n+1] = -1 比谁都矮 ⇒ 它把栈弹空,然后自己被压进去。
⇒ 每组跑完,栈里恒定只剩一个元素,就是下标 n+1(度量程序 left1 那一行验过:恒为 1)。
于是「忘了清空」留下的只有那一格。它要真的害人,还得再满足一句:
上一组的
n_prev + 1比这一组的n还大 —— 这样h[n_prev+1]不会被这一组的读入覆盖,它还是-1,永远弹不掉, 于是「左界」再也回不到 0。
| 300 轮 | 档 0 | 档 1 | ★ 档 2 高度大 | 档 3 |
|---|---|---|---|---|
| 那一格永远弹不掉 | 148 | ★ 0 | 148 | 148 |
| ⇒ 真被抓 | 76 | ★ 0 | ★ 148 | 75 |
★★ 在高度大的那一档,这一层就是 ≡ 了(148 ≡ 148); 而高度只有 0~10 的档上还要再掉一半 —— 因为那时最优矩形常常压根不顶到最左边。 ⇒ 说清楚「它算了什么」之后,连「哪一档会 ≡」都是推得出来的。
3⚠ 多出来的第二件:那个 0 是结束标志,不是一组数据
// SP1805 · 错法 ③:读到 EOF 才停,而不是读到 0 就停//// while (cin >> n) ← 这一版// while ((cin >> n) && n) ← 正解//// ⇒ 末尾那个 `0` 也被当成一组数据处理了,于是多打一行 `0`。// ★ 题面那句话是「输入零时读入结束」——**零本身不是一组数据**。#include <bits/stdc++.h>using namespace std;
static const int N = 100005;static long long h[N];static int stk[N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; while (cin >> n) { // ✗ 少了 && n for (int i = 1; i <= n; i++) cin >> h[i]; h[n + 1] = -1; int top = 0; long long ans = 0; for (int i = 1; i <= n + 1; i++) { while (top && h[stk[top]] >= h[i]) { long long H = h[stk[top]]; top--; long long left = top ? stk[top] : 0; ans = max(ans, H * (i - left - 1)); } stk[++top] = i; } cout << ans << '\n'; } return 0;}点「运行 ▶」看结果
while (cin >> n) // ✗ 末尾那个 0 也被当成一组处理了
while ((cin >> n) && n) // ★ 正解
四个档全是 300 / 300 —— 每组都错,官方样例一测就死。 ★ 又一次「样例是个『一测就死』的过滤器」:它挡住的三个全是「每组都错」型。
4⚠ 多出来的第三件:面积要开 long long,而顺手数据一次都抓不到
// SP1805 · 错法 ④:面积用 int//// 顶格 10⁵ 根 × 高 10⁹ = 10¹⁴,而 int 的上限是 2 147 483 647 —— 差 **四万倍**。// ⚠ 而顺手写的生成器高度只有 0~10、n 只有几根 ⇒ 这一档是**精确的 0**(本页第 ③ 步)。// ★ 演示用 unsigned 让溢出的结果可复现(第 45 章那条:别拿 UB 派生的数字当断言)。#include <bits/stdc++.h>using namespace std;
static const int N = 100005;static unsigned h[N];static int stk[N];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; while ((cin >> n) && n) { for (int i = 1; i <= n; i++) cin >> h[i]; h[n + 1] = 0u - 1u; // 哨兵:unsigned 里当「比谁都矮」用不了, int top = 0; // 所以这一版改成扫完再单独收尾 unsigned ans = 0; for (int i = 1; i <= n; i++) { while (top && h[stk[top]] >= h[i]) { unsigned H = h[stk[top]]; top--; unsigned left = top ? (unsigned)stk[top] : 0u; unsigned w = (unsigned)i - left - 1u; if (H * w > ans) ans = H * w; // ✗ 32 位相乘,溢出就绕回去了 } stk[++top] = i; } while (top) { unsigned H = h[stk[top]]; top--; unsigned left = top ? (unsigned)stk[top] : 0u; unsigned w = (unsigned)n - left; if (H * w > ans) ans = H * w; } cout << ans << '\n'; } return 0;}点「运行 ▶」看结果
顶格答案(10⁵ 根全高 10⁹) |
100 000 000 000 000 |
int 上限 |
2 147 483 647 |
| ⇒ 差 | ★ 46 566 倍 |
| 300 轮 | 档 0 高度 0~10 | 档 1 | ★★ 档 2 高度接近 10⁹ |
档 3 高度 0~2 |
|---|---|---|---|---|
| 答案 ≥ 2³² | ★ 0 | ★ 0 | 207 | ★ 0 |
| ⇒ 真被抓 | ★ 0 | ★ 0 | ★ 207 | ★ 0 |
★★ 触发条件 ≡ 抓获数,一个不差(207 ≡ 207)——
⚠ 而三个档是精确的 0:顺手写的生成器高度只有 0~10、n 只有几根,
面积撑死几十,结构上够不着 2³²。
⇒ 又一次「溢出的触发条件是一条数值线,生成器够不够是算术题」:
要抓它,得让 h × 宽 真的越过那条线,加轮数一点用都没有。
5★ 那个「更聪明的暴力」:全部等高比单调不降正好慢一倍
正文第 4 步量过它在单调不降上退化。这一页顺手把第三种形状也量了:
n = 20000,往左右一共挪了多少步 |
|
|---|---|
| 随机高度 | 350 186(≈ 17.5 n,线性) |
| 单调不降(正文第 13 步那一档) | 199 990 000 = n²/2 |
| ★ 全部等高 | 399 980 000 = n² ⇒ ★ 正好是上一行的 2.00 倍 |
★★ 而这个「正好 2 倍」是能证的:单调递增时每根柱子只往右扩(左边都比它矮),
全部等高时两边都扩到底。⇒ 一个 n²/2,一个 n²。
A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 顶格 n = 10⁵ · 时限 409 毫秒 |
单调栈 | 往左右扩 |
|---|---|---|
全部等高 10⁹ |
★ 3 ms | 2513 ms |
| 单调不降 | ★ 3 ms | 1230 ms |
⇒ ★ 正文第 13 步说「暴力的最坏形状是单调不降」——
这一页把它改准了:单调不降是 n²/2,全部等高才是 n²。
⚠ 而顺手随机造一份顶格数据,暴力线性跑完 —— 又一次「顶格 ≠ 最坏」。
6★ 对拍这一页
| 300 轮 | 档 0 顺手 | ★ 档 1 只有一组 | ★★ 档 2 高度大 | 档 3 高度 0~2 |
|---|---|---|---|---|
| 单调栈(正解) | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 只处理第一组 | 204 | ★ 0 | 204 | 204 |
| 组间忘清空 | 76 | ★ 0 | 148 | 75 |
| 读到 EOF 才停 | 300 | 300 | 300 | 300 |
| 面积用 32 位 | ★ 0 | ★ 0 | 207 | ★ 0 |
| 错法 | 抓获率(最狠的那一档) | 官方样例挡住了吗 |
|---|---|---|
| 读到 EOF 才停 | 300 / 300 | ★ 挡住(多打一行 0) |
| 只处理第一组 | 204 / 300 | ★ 挡住(只打出 8) |
| 组间忘清空 | 148 / 300 | ★ 挡住(第二组打出 0) |
| 面积用 32 位 | 207 / 300 | ⚠ 放过 —— 样例最大的面积才 4000 |
⇒ ★ 前三个都是「多组数据」这件事派生的,而官方样例本身就有两组 —— 它对这一类坑天生就是好用的过滤器。 ⚠ 唯独溢出那条它无能为力:那不是结构问题,是一条数值线。
7度量程序和生成器
8一页纸
| 算法 | 就是本章第 5~7 步那道题的原题,一个字都不用改 |
| ★★★ 多出来的三件 | ① 多组数据 ② 那个 0 是结束标志 ③ 面积要 long long —— 一件都不在算法里 |
| ★★★ 同一个第一层,两个第二层 | 「不止一组」204 —— 只处理第一组 ≡ 204,组间忘清空只有 76(差 2.7 倍) |
| ★ 说清楚残留的是什么 | 每组跑完栈里恒剩一格(末尾哨兵)⇒ 它「永远弹不掉」的轮数 148,在高度大的档上 ≡ 148 |
| ★★ 溢出 | 顶格答案 10¹⁴,是 int 上限的 46 566 倍;触发 ≡ 抓获(207 ≡ 207),⚠ 另外三档结构性的 0 |
| ★ 暴力的最坏形状 | 正文说「单调不降」——改准了:那是 n²/2,全部等高才是 n²(正好 2.00 倍) |
| ★ 官方样例 | 挡住三个(都是「多组数据」派生的,因为样例本身就有两组),只放过溢出那个 |