0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1440,日期见页头。两边不一致时信原站。
题目描述
一个含有 n 项的数列,求出每一项前的 m 个数到它这个区间内的最小值。
若前面的数不足 m 项则从第 1 个数开始,若前面没有数则输出 0。
输入格式
第一行两个整数,分别表示 n,m。
第二行,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 项前面只有 7 ⇒ 7;
第 3 项前面是 7 8 ⇒ 7;第 4 项前面是 8 1 ⇒ 1;
第 5 项前面是 1 4 ⇒ 1;第 6 项前面是 4 3 ⇒ 3。
⚠ 注意第 2 项不是 0 —— 「前面不足 m 项」照样要算,只是窗口短一点。
1★ 和本章那道模板题只差一件事:窗口不含自己,而且在自己左边
// 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;}点「运行 ▶」看结果
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 步那道模板题的窗口含当前元素 ⇒ 先入队再输出; 这道题的窗口不含 ⇒ 反过来。代码长得几乎一样,两行一换就全错。
题面写着「1 ≤ aᵢ」,又写着「若前面没有数则输出 0」
⇒ 0 是一个永远不会和真实答案撞车的值,直接拿来当「队列空」的标志就行。
⚠ 而同一张题单上的 P1886 是另一个极端:
aᵢ ∈ [−2³¹, 2³¹) 把 int 的每一个值都用光了,一个哨兵都不剩。
⇒ ★★ 「能不能用哨兵」不是写法的习惯,是每道题读一遍数据范围的事。
2★★★ 两个边界 bug,盲区正好互补 —— 只造一个极端档,必漏一个
// 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;}点「运行 ▶」看结果
// 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;}点「运行 ▶」看结果
| 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 = n时,i − m ≤ 0而下标最小是 1 ⇒ 出窗那句判断从来没成立过,多个等号少个等号都一样。
⇒ ★★★ 于是顺手挑一个极端档去测边界,必然漏掉另一个 ——
这是「为一个 bug 精心造的档位正是另一个 bug 的盲区」的对称版:
本书前面那几次是「一个档护着一个 bug」,这一页是两个档互为对方的盲区。
★ 而档 0(m 随机 1~n)两个都抓得到 —— 随机那一档在这里反而是最稳的。
| 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 —— 但抓到它的不是你以为的那件事
// 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;}点「运行 ▶」看结果
这是把本章第 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★★ 三条都正确的路:一条卡时间、一条卡内存、一条都不卡
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(P2704、P1853 都是),它当场 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度量程序和生成器
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 正好相反 |