题单 · 习题解析

洛谷 P1440 求 m 区间内的最小值

★ 窗口**不含自己、在自己左边** ⇒ 「先输出、再入队」,两行的次序就是全部关卡;★★★ 而这一页最值钱的是**两个边界 bug 的盲区正好互补** —— 「前 m 项一律输出 0」死在 **m = 1**、「出窗那句差一」死在 **m = n**,两个精确的 0 都能证 ⇒ **只造一个极端档去测边界,必然漏掉另一个**(⚠ 反倒是「m 随机」那一档两个都抓得到);★★ 两个 ≡:m=1 的轮数 ≡ 前者漏掉的轮数,「窗口最小值只在最左端取到」≡ 后者的抓获数(四档全中);★★ 「300/300」未必是你想的那个原因 —— 「窗口含自己」逐行全比 300,**只比第 2 行起就掉到 280/263/223**(真正抓它的是第一行那个 0);★★ 三条正确的路:单调队列 40 ms / 12 MB、ST 表 160 ms / **171 MB**、multiset **2.96 秒 ⇒ TLE**,⚠ 而 ST 表过得去纯粹因为题面给了 512 MB;★ 哨兵是题面白送的(1 ≤ aᵢ + 没有数就输出 0)—— 和同一章的 [P1886](/sol/p1886/) 正好相反

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

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

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

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

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

转录自洛谷 P1440,日期见页头。两边不一致时信原站。

题目描述

一个含有 n 项的数列,求出每一项m 个数到它这个区间内的最小值。 若前面的数不足 m 项则从第 1 个数开始,若前面没有数则输出 0。

输入格式

第一行两个整数,分别表示 nm

第二行,n 个正整数,为所给定的数列 aᵢ

输出格式

n 行,每行一个整数,第 i 个数为序列中 aᵢ 之前 m 个数的最小值。

数据规模与约定

对于 100% 的数据,保证 1 ≤ m ≤ n ≤ 2 × 10⁶1 ≤ aᵢ ≤ 3 × 10⁷

时限 1.5 秒,内存 512 MB。

输入输出样例

输入

6 2
7 8 1 4 3 2

输出

0
7
7
1
1
3

m = 2:第 1 项前面没有数 ⇒ 0;第 2 项前面只有 77; 第 3 项前面是 7 87;第 4 项前面是 8 11; 第 5 项前面是 1 41;第 6 项前面是 4 33。 ⚠ 注意第 2 项不是 0 —— 「前面不足 m 项」照样要算,只是窗口短一点。

1★ 和本章那道模板题只差一件事:窗口不含自己,而且在自己左边

p1440.cpp★ 这一版就能 AC(顶格 n = 2×10⁶,本机 40 毫秒 / 12 MB)
// P1440 求 m 区间内的最小值 —— ★ 这一版就能 AC
//
// 求每一项**之前** m 个数的最小值:第 i 行输出 min(a[max(1, i−m)] … a[i−1]),
// 前面一个数都没有(i = 1)就输出 0。
//
// ★★ 和本章第 9 步那道模板题只差一件事:**窗口不含自己,而且落在自己左边**。
// ⇒ 顺序变成「先输出,再把 a[i] 放进队列」——
// 这两行的次序就是这道题唯一的关卡(本页第 ② 步)。
//
// ★ 题面那句「若前面没有数则输出 0」+「1 ≤ aᵢ」,等于**直接把哨兵送给你了**
// —— 和同一章的 [P1886](/sol/p1886/)(题面把 int 用光、一个哨兵都不剩)正好相反。
#include <bits/stdc++.h>
using namespace std;
static const int N = 2000006;
static int a[N], q[N];
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(), m = rd();
for (int i = 1; i <= n; i++) a[i] = rd();
int head = 1, tail = 0;
for (int i = 1; i <= n; i++) {
while (head <= tail && q[head] < i - m) head++; // 窗口是 [i−m, i−1]
wr(head <= tail ? a[q[head]] : 0); // ★ 先输出
pc('\n');
while (head <= tail && a[q[tail]] >= a[i]) tail--;
q[++tail] = i; // ★ 再入队
}
flushOut();
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 于是那两行的次序就是这道题唯一的关卡
while (head <= tail && q[head] < i - m) head++;   // 窗口是 [i−m, i−1]
wr(head <= tail ? a[q[head]] : 0);                // ★ 先输出
while (head <= tail && a[q[tail]] >= a[i]) tail--;
q[++tail] = i;                                    // ★ 再入队

本章第 9 步那道模板题的窗口当前元素 ⇒ 先入队再输出; 这道题的窗口不含 ⇒ 反过来。代码长得几乎一样,两行一换就全错。

★ 顺带一句:这道题的哨兵是题面白送的 —— 和同一章的 P1886 正好相反

题面写着「1 ≤ aᵢ」,又写着「若前面没有数则输出 0」 ⇒ 0 是一个永远不会和真实答案撞车的值,直接拿来当「队列空」的标志就行。

⚠ 而同一张题单上的 P1886另一个极端aᵢ ∈ [−2³¹, 2³¹)int 的每一个值都用光了,一个哨兵都不剩。 ⇒ ★★ 「能不能用哨兵」不是写法的习惯,是每道题读一遍数据范围的事。

2★★★ 两个边界 bug,盲区正好互补 —— 只造一个极端档,必漏一个

p1440First.cpp✗ 把「前面不足 m 项」读成「前 m 项一律输出 0」(样例打出 0 0 7 1 1 3)
// P1440 · 错法 ②:把「前面不足 m 项」读成了「前面不足 m 项就输出 0」
//
// 题面原话是两句:
// 「若前面的数不足 m 项**则从第 1 个数开始**」—— 不足也要算,只是窗口短一点;
// 「若前面**没有数**则输出 0」 —— 只有第 1 项才输出 0。
// ⇒ 把两句读成一句,就成了「前 m 项全输出 0」。
// ★ m = 1 时这两种读法**完全一样**(只有第 1 项没有前驱)⇒ 那一档是**能证的精确 0**。
#include <bits/stdc++.h>
using namespace std;
static const int N = 2000006;
static int a[N], q[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
string out;
int head = 1, tail = 0;
for (int i = 1; i <= n; i++) {
while (head <= tail && q[head] < i - m) head++;
out += to_string(i <= m ? 0 : a[q[head]]); // ✗ 前 m 项一律 0
out += '\n';
while (head <= tail && a[q[tail]] >= a[i]) tail--;
q[++tail] = i;
}
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1440Off.cpp✗ 出窗那句多了个等号,窗口短了一个(样例打出 0 7 8 1 4 3)
// P1440 · 错法 ③:出窗那句差一 —— 窗口短了一个
//
// while (head <= tail && q[head] < i − m) head++; ← 正解(窗口 [i−m, i−1],共 m 个)
// while (head <= tail && q[head] <= i − m) head++; ← 这一版(窗口 [i−m+1, i−1],共 m−1 个)
//
// ★ 这是本章第 10 步那句「边界比模板本身更容易写错」在这道题上的样子:
// 窗口的两端都被题面挪了一格,于是那个 `<` 到底该不该带等号,**只能重新推一遍**。
#include <bits/stdc++.h>
using namespace std;
static const int N = 2000006;
static int a[N], q[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
string out;
int head = 1, tail = 0;
for (int i = 1; i <= n; i++) {
while (head <= tail && q[head] <= i - m) head++; // ✗ 多了个等号
out += to_string(head <= tail ? a[q[head]] : 0);
out += '\n';
while (head <= tail && a[q[tail]] >= a[i]) tail--;
q[++tail] = i;
}
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 一个死在 m = 1,一个死在 m = n
300 轮 档 0 m 随机 ★ 档 1 m = 1 ★ 档 2 m = n 档 3 值域 1~3
「前 m 项一律 0」被抓 259 0 300 259
「出窗差一」被抓 170 300 0 128

两个 0 都是能证的

m = 1,「前面不足 m 项」和「前面没有数」是同一句话(都只有 i = 1) ⇒ 那个误读和正解逐字节相同。

m = ni − m ≤ 0 而下标最小是 1 ⇒ 出窗那句判断从来没成立过,多个等号少个等号都一样。

⇒ ★★★ 于是顺手挑一个极端档去测边界,必然漏掉另一个 —— 这是「为一个 bug 精心造的档位正是另一个 bug 的盲区」对称版: 本书前面那几次是「一个档护着一个 bug」,这一页是两个档互为对方的盲区。 ★ 而档 0(m 随机 1~n)两个都抓得到 —— 随机那一档在这里反而是最稳的。

★★ 而这两个 bug 的抓获数都能写成「≡」
300 轮 档 0 档 1 档 2 档 3
m = 1 的轮数 41 300 0 41
⇒ 「前 m 项一律 0」漏掉的轮数 41 300 0 41
★ 「窗口最小值只在最左端取到」 170 300 0 128
⇒ 「出窗差一」被抓 170 300 0 128

★★ 下面那一对是四档一个不差的 ≡ —— 而它的第一层不是「m 是几」, 是「少看最左边那一个,答案会不会变」。 ⇒ 又一次「能不能写成 ≡ 取决于你第一层写得够不够细」: 拿「m = n 的轮数」当第一层,档 0 是 26 而抓获是 170,差了 6.5 倍; 换成上面那句,四档全中。

3★★ 第三个错法 300 / 300 —— 但抓到它的不是你以为的那件事

p1440Self.cpp✗ 先入队再输出 ⇒ 窗口含自己(样例打出 7 7 1 1 1 2)
// P1440 · 错法 ①:把自己也算进了窗口
//
// 两行的次序反了:先把 a[i] 放进队列,再输出 ⇒ 窗口成了 [i−m, i]。
// ⚠ 这就是把本章第 9 步那道模板题的代码**原样搬过来**的结果 ——
// 那道题的窗口**含**当前元素,这道题**不含**。
// ⇒ 触发条件一句话:**a[i] 比它前面 m 个数里最小的还小**。
#include <bits/stdc++.h>
using namespace std;
static const int N = 2000006;
static int a[N], q[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
string out;
int head = 1, tail = 0;
for (int i = 1; i <= n; i++) {
while (head <= tail && q[head] < i - m) head++;
while (head <= tail && a[q[tail]] >= a[i]) tail--;
q[++tail] = i; // ✗ 先入队
out += to_string(a[q[head]]); // ✗ 再输出
out += '\n';
}
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

这是把本章第 9 步那道模板题的代码原样搬过来的结果。四个档全是 300 / 300

★★ 把「逐行全比」和「只比第二行起」分开量,结论就变了
300 轮 档 0 档 1 档 2 档 3
逐行全比 ⇒ 不同 300 300 300 300
只从第 2 行起比 ⇒ 不同 280 300 263 223

★★ 差出来的那 20 / 0 / 37 / 77 轮,唯一的分歧就在第一行: 正解那儿是 0,而这一版把 a[1] 打了出来(题面保证 aᵢ ≥ 1必然不等)。 ⇒ ★★★ 所以「300 / 300」这个数看着像是窗口错了被抓,其实相当一部分是第一行那个 0 抓的 —— ⇒ 又一次「说清楚它算了什么」: 真正因为「窗口含了自己」而错的,是 a[i] 比它前面 m 个数都小的那些轮。

4★★ 三条都正确的路:一条卡时间、一条卡内存、一条都不卡

p1440Set.cpp⚠ multiset:答案全对,顶格 2.96 秒(时限 1.5 秒)⇒ TLE
p1440St.cpp⚠ ST 表:0.16 秒,但吃掉 171 MB
A 机 · WSL2 · 8 核 / 7 G · 2026-08-31 · 独占 · 顶格 n = 2×10⁶ / m = n/2 耗时(限 1.5 秒) 峰值内存(限 512 MB)
★ 单调队列 0.04 秒 11.7 MB
ST 表(稀疏表) 0.16 秒 171 MB(33%)
multiset 2.96 秒 ⇒ TLE 64.6 MB
O(nm) 暴力 两分钟没跑完2 × 10¹² 次)
★★ 三条路的账各不相同,而这道题恰好三条都「能算出来」
顶格 n = 2×10⁶ 时,光数组就要
单调队列(a[]q[] 2 × 2×10⁶ × 4B = 15 MB
ST 表(21 层 int 21 × 2×10⁶ × 4B = 160 MB
multiset(窗口 n/2 个结点,每个约 40 B) 38 MB

★ 三个估算和实测(11.7 / 171 / 64.6 MB)都对得上量级。 ⚠ 而 ST 表这一条过得去纯粹是因为这道题给了 512 MB —— 换成常见的 128 MB(P2704P1853 都是),它当场 MLE。 ⇒ ★★ 「这条路行不行」的主语里,除了 n,还有题面那行内存限制。

5⚠ 顺带一笔 I/O 的账:输入 16.5 MB,输出 5.5 MB

顶格输入 17 256 638 字节2 × 10⁶ 个八位数)、输出 5 723 789 字节2 × 10⁶ 行)。 时限 1.5 秒 ⇒ 这一档比 P5788(输入 28 MB + 输出 22 MB / 时限 1 秒)宽松不少, 但默认 cin 仍然是不必要的冒险 —— 这道题的正解全部工作只有 40 毫秒,而光 I/O 就能吃掉几百毫秒。 ⇒ 又一次「四种读法的倍数跨题几乎不变,变的是绝对时间」

6★ 对拍这一页

300 轮(n 随机 6~12) 档 0 m 随机 ★ 档 1 m = 1 ★ 档 2 m = n 档 3 值域 1~3
单调队列(正解) 0 0 0 0
multiset 0 0 0 0
ST 表 0 0 0 0
窗口含了自己 300 300 300 300
m 项一律 0 259 0 300 259
出窗那句差一 170 300 0 128
★ 官方样例三个错法全挡住了 —— 而这不是运气
错法 官方样例打出 挡住了吗
窗口含了自己 7 7 1 1 1 2 挡住
m 项一律 0 0 0 7 1 1 3 挡住
出窗那句差一 0 7 8 1 4 3 挡住

★ 三个都是「每组都错 / 几乎每组都错」型 —— 「样例是个『一测就死』的过滤器」又一次成立。 ★★ 而这组样例挡得住是有结构原因的:它的 m = 2 既不是 1 也不是 n ⇒ 上面那两个互补的盲区,它一个都没踩进去。 ⇒ 官方样例往往比顺手写的生成器更懂得避开退化情形。

7度量程序和生成器

p1440Count.cpp度量程序(本页所有数字都出自它)
p1440Gen.cpp(六个档位)数据生成器
p1440Brute.cpp参照物:每一项往左看 m 个

8一页纸

★ 关键的一步 窗口不含自己、在自己左边 ⇒ 「先输出、再入队」——两行的次序就是全部
★★★ 两个盲区互补 「前 m 项一律 0」死在 m = 1、「出窗差一」死在 m = n,两个 0 都能证
⇒ 直接的推论 只造一个极端档去测边界,必然漏掉另一个;⚠ 而 m 随机那一档两个都抓
★★ 两个 ≡ m = 1 的轮数 ≡ 前者漏掉的轮数;「最小值只在最左端」≡ 后者的抓获数(四档全中)
★★ 300 / 300 未必是你想的那个原因 「窗口含自己」逐行全比 300,只比第 2 行起就掉到 280 / 263 / 223
★★ 三条正确的路 单调队列 40 ms / 12 MB;ST 表 160 ms / 171 MB;multiset 2.96 s ⇒ TLE
⚠ 而 ST 表能过是因为题面给了 512 MB 换成常见的 128 MB 当场 MLE ⇒ 主语里有那行内存限制
★ 哨兵 题面「1 ≤ aᵢ」+「没有数就输出 0」白送一个哨兵 —— 和 P1886 正好相反