题单 · 习题解析

洛谷(SPOJ 远程评测) SP1805 HISTOGRA - Largest Rectangle in a Histogram

★★ 它就是本章前半章那道题的**原题** —— 算法一个字不用改,可它多出**三件全都不在算法里**的事:① 多组数据 ② 末尾那个 `0` 是结束标志不是数据 ③ 面积要 long long;★★★ 而「多组数据」派生的两个 bug **共用同一个第一层**(不止一组,204 轮),一个是 **≡ 204**(只处理第一组)、一个只有 **76**(组间忘清空,差 2.7 倍)⇒ 又一次「能不能写成 ≡ 取决于第一层写得够不够细」;★ 说清楚残留的是什么之后连「哪一档会 ≡」都推得出来 —— 每组跑完栈里**恒剩一格**(末尾哨兵),它「永远弹不掉」148 轮,在高度大的档上 ≡ 148;★★ 溢出:顶格答案 10¹⁴ 是 int 上限的 **46 566 倍**,触发 ≡ 抓获(207 ≡ 207),⚠ 另外三档是**结构性的 0**;★ 顺带把正文第 13 步改准了:暴力的最坏形状**不是单调不降**(n²/2),**是全部等高**(n²,正好 2.00 倍)

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

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

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接,它是 SPOJ 的远程评测题)。别人的网站不归我们管, 打不开、改版、题号调整都可能发生。所以每个解析页都把题面转录一份存在本地, 跟着仓库一起进版本库。

转录自洛谷 SP1805,日期见页头。两边不一致时信原站。 ⚠ 题面里那张图也存了一份public/sol/sp1805-histogram.png)—— P1228 那次的教训:只转文字等于存了半篇。

题目背景

如遇 SPOJ 评测服务不稳定,可以在洛谷 B4273 评测本题,注意 nhᵢ 范围的小变化。

题目描述

柱状图里最大的矩形(阴影部分即所求)

如图所示,在一条水平线上有 n 个宽为 1 的矩形,求包含于这些矩形的最大子矩形面积 (图中的阴影部分的面积即所求答案)。

输入格式

有多组测试数据,每组数据占一行。输入零时读入结束。

每行开头为一个数字 n1 ≤ n ≤ 10⁵),接下来在同一行给出 n 个数字 h₁, h₂, …, h_n0 ≤ 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:最大的是 45 那两根撑出来的 高 4 × 宽 2 = 8 (对比:整排取高 1 是 1 × 7 = 7,末尾两根取高 3 是 3 × 2 = 6); 第二组四根全高 1000 ⇒ 1000 × 4 = 4000。 ⚠ 末尾那个 0结束标志,不是一组数据 —— 第 ③ 步专门讲它。

1算法本身,本章第 5~7 步已经讲完了

sp1805.cpp★ 这一版就能 AC(顶格 n = 10⁵ 全高 10⁹,本机 3 毫秒 / 时限 409)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 一句话回顾:换个问法,题目就变成「左右第一个更矮的在哪」

枚举「以第 i 根柱子的高度为高」的最大矩形 ⇒ 它能往左右伸到第一个比它矮的为止。 单调栈一遍扫完:弹栈的那一刻,右界是当前的 i、左界是弹完之后的新栈顶。

ans = max(ans, H * (i - left - 1));      // ★ 宽度 = 右界 − 左界 − 1

这道题就是本章前半章那道题的原题,正文第 5~7 步(含那个动画)讲的就是它。 ⇒ ★ 所以这一页不重讲算法,只讲它比正文那份多出来的三件事 —— 而那三件一件都不在算法里

2★★★ 多出来的第一件:有多组数据 —— 而它派生出两个不同的 bug

sp1805One.cpp✗ 只处理第一组(官方样例只打出 8,当场挡住)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
sp1805Reset.cpp✗ 组间忘了把栈清空(官方样例第二组打出 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 两个 bug 的第一层完全一样,第二层却差 2.7 倍

两个 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 共用同一个盲区 —— 顺手写生成器时只造一组数据,两个一起漏掉。

★ 那 2.7 倍差在哪:说清楚「组间残留的到底是什么」

先问一句:每组跑完,栈里到底还剩什么?

结尾那个哨兵 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 是结束标志,不是一组数据

sp1805Eof.cpp✗ 读到 EOF 才停(样例多打一行 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
while (cin >> n)          // ✗ 末尾那个 0 也被当成一组处理了
while ((cin >> n) && n)   // ★ 正解

四个档全是 300 / 300 —— 每组都错,官方样例一测就死。 ★ 又一次「样例是个『一测就死』的过滤器」:它挡住的三个全是「每组都错」型。

4⚠ 多出来的第三件:面积要开 long long,而顺手数据一次都抓不到

sp1805Int.cpp✗ 面积用 32 位(官方样例照过;顶格给 4294884352)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
顶格答案(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★ 那个「更聪明的暴力」:全部等高比单调不降正好慢一倍

sp1805Brute.cpp参照物:每根往左右扩到比它矮为止

正文第 4 步量过它在单调不降上退化。这一页顺手把第三种形状也量了:

n = 20000,往左右一共挪了多少步
随机高度 350 186(≈ 17.5 n,线性
单调不降(正文第 13 步那一档) 199 990 000 = n²/2
全部等高 399 980 000 = ⇒ ★ 正好是上一行的 2.00 倍

★★ 而这个「正好 2 倍」是能证的:单调递增时每根柱子只往右扩(左边都比它矮), 全部等高时两边都扩到底。⇒ 一个 n²/2,一个

A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 顶格 n = 10⁵ · 时限 409 毫秒 单调栈 往左右扩
全部等高 10⁹ 3 ms 2513 ms
单调不降 3 ms 1230 ms

⇒ ★ 正文第 13 步说「暴力的最坏形状是单调不降」—— 这一页把它改准了:单调不降是 n²/2,全部等高才是 ⚠ 而顺手随机造一份顶格数据,暴力线性跑完 —— 又一次「顶格 ≠ 最坏」

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度量程序和生成器

sp1805Count.cpp度量程序(本页所有数字都出自它)
sp1805Gen.cpp(六个档位)数据生成器

8一页纸

算法 就是本章第 5~7 步那道题的原题,一个字都不用改
★★★ 多出来的三件 ① 多组数据 ② 那个 0 是结束标志 ③ 面积要 long long —— 一件都不在算法里
★★★ 同一个第一层,两个第二层 「不止一组」204 —— 只处理第一组 ≡ 204,组间忘清空只有 76(差 2.7 倍)
★ 说清楚残留的是什么 每组跑完栈里恒剩一格(末尾哨兵)⇒ 它「永远弹不掉」的轮数 148,在高度大的档上 ≡ 148
★★ 溢出 顶格答案 10¹⁴,是 int 上限的 46 566 倍;触发 ≡ 抓获(207 ≡ 207),⚠ 另外三档结构性的 0
★ 暴力的最坏形状 正文说「单调不降」——改准了:那是 n²/2全部等高才是 (正好 2.00 倍)
★ 官方样例 挡住三个(都是「多组数据」派生的,因为样例本身就有两组),只放过溢出那个