题单 · 习题解析

洛谷 P1886 【模板】单调队列 / 滑动窗口

★★★ 题面那半行 `aᵢ ∈ [−2³¹, 2³¹)` 把 int 的 **4 294 967 296 个值全用光了 ⇒ 一个哨兵都不剩**;⚠⚠ 而中招的不是正解,是**你的参照物** —— 顺手写的暴力 `best = 0x3f3f3f3f` 在全值域档错 60/300(触发 ≡ 抓获)⇒ **顶格不只要顶被测程序的顶,也要顶参照物的顶**;★★ 「快读忘了负号」**对拍默认档 0/300,官方样例一测就死**(顺手生成器只造正数);★★★ 一条草稿被打回:「队尾保留相等会慢」错了 —— 队列平均长度差 **68 285 倍**,秒表只差 **1.17 倍**(均摊和弹不弹相等的无关);★★ 「谁更快」的主语是 k —— multiset 自己跟自己差 43.7 倍、顶格端到端 1757 ms(TLE),而单调队列 47 ms 纹丝不动 ⇒ 它只配当参照物;★ 边界差一漏掉的 **27 轮 ≡ k=1 的 27 轮**

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

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

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

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

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

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

题目描述

有一个长为 n 的序列 a,以及一个大小为 k 的窗口。现在这个窗口从左边开始向右滑动, 每次滑动一个单位,求出每次滑动后窗口中的最小值和最大值。

例如,对于序列 [1, 3, -1, -3, 5, 3, 6, 7] 以及 k = 3,有如下过程:

窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7

输入格式

输入一共有两行,第一行有两个正整数 n, k;第二行有 n 个整数,表示序列 a

输出格式

输出共两行,第一行为每次窗口滑动的最小值;第二行为每次窗口滑动的最大值。

数据范围

对于 50% 的数据,1 ≤ n ≤ 10⁵

对于 100% 的数据,1 ≤ k ≤ n ≤ 10⁶aᵢ ∈ [−2³¹, 2³¹)

时限 1 秒,内存 500 MB。

输入输出样例

输入

8 3
1 3 -1 -3 5 3 6 7

输出

-1 -3 -3 -3 3 3
3 3 5 5 6 7

⚠ 这组样例里有负数 —— 记住这句话,第 ⑤ 步要用它。

1★ 关键一步:队列里存下标,队尾扔掉「已经没戏的」

p1886.cpp★ 这一版就能 AC(顶格 n = 10⁶ / k = n/2,本机端到端 47 毫秒)
// P1886【模板】单调队列 / 滑动窗口 —— ★ 这一版就能 AC
//
// 两问用同一套骨架,只把比较符号反过来:
// 最小值:队尾弹掉「不比 a[i] 小」的 ⇒ 队列里的值从队头到队尾递增,队头就是最小
// 最大值:队尾弹掉「不比 a[i] 大」的 ⇒ 队头就是最大
//
// ★ 队列里存的是**下标**,不是值 —— 因为判「有没有滑出窗口」要拿下标算(本章第 9 步)。
//
// ⚠ 题面写着 aᵢ ∈ [−2³¹, 2³¹) —— 那正好是 int 的**全部**范围,一点余量都没有。
// ⇒ ① 存得下,用 int 就够;② 但**任何自己造的哨兵值都是错的**(本页第 ③ 步)。
#include <bits/stdc++.h>
using namespace std;
static const int N = 1000006;
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 != '-' && (c < '0' || c > '9')) c = gc();
bool neg = false;
if (c == '-') { neg = true; c = gc(); } // ⚠ 这一行不能少,见本页第 ⑤ 步
long long x = 0;
while (c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = gc(); }
return (int)(neg ? -x : x); // ★ 用 long long 中转:−2³¹ 的绝对值放不进 int
}
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 < 0) { pc('-'); }
unsigned v = x < 0 ? (unsigned)0 - (unsigned)x : (unsigned)x; // ★ 同理,别写 -x
char t[12];
int c = 0;
if (!v) t[c++] = '0';
while (v) { t[c++] = char('0' + v % 10); v /= 10; }
while (c) pc(t[--c]);
}
int main() {
int n = rd(), k = rd();
for (int i = 1; i <= n; i++) a[i] = rd();
for (int pass = 0; pass < 2; pass++) {
int head = 1, tail = 0;
for (int i = 1; i <= n; i++) {
/* 队尾:把「轮不到了」的弹掉 —— 它比 a[i] 差,还比 a[i] 先出窗 */
while (head <= tail && (pass == 0 ? a[q[tail]] >= a[i] : a[q[tail]] <= a[i])) tail--;
q[++tail] = i;
/* 队头:滑出窗口的丢掉 —— ★ 判据是下标,不是值 */
if (q[head] <= i - k) head++;
if (i >= k) { wr(a[q[head]]); pc(i == n ? '\n' : ' '); }
}
}
flushOut();
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 两问是同一套骨架,只把比较符号反过来

新来的 a[i] 一进来,队尾那些「又不比它小、又比它先出窗」的元素就永远轮不到当答案了 —— 当场弹掉。于是队列里的值从队头到队尾递增,队头就是窗口最小值。 求最大值把两个比较符号反过来就行。

★ 队列里存的是下标不是值 —— 因为「有没有滑出窗口」只能拿下标算:

if (q[head] <= i - k) head++;      // ★ 判据是下标

本章第 9 步那句话在这道题上是逐字生效的:队列里比的是值,窗口边界是拿下标判的。 第 ④ 步就是把这句话写反的后果。

2★★★ 题面那半行 aᵢ ∈ [−2³¹, 2³¹) 是这道题最狠的一句 —— 它把 int 用光了

★★★ 「INF 该写多大」这道算术题,在这道题上无解

第 32 章第 33 章连着两章都在算同一道题: 0x3f3f3f3f 够不够大?—— 那几道题的答案分别是「不够(差 2.02 倍)」「恰好够(余量 6.1%)」 「恰好够(余量 4.0%)」。

这道题连算都不用算:

题面允许的 aᵢ 取值个数 [−2³¹, 2³¹)4 294 967 296
int 一共有几个值 4 294 967 296
⇒ 还剩几个能当哨兵 ★★★ 0 个

这道题里没有任何一个 int 值可以拿来当「不可能出现的数」。 0x3f3f3f3f1e9INT_MAX-1 —— 一个都不行。

p1886Inf.cpp✗ 暴力里用 0x3f3f3f3f 当初值(官方样例照过)
// P1886 · 错法 ①:⚠⚠ **参照物自己错了** —— 暴力里用 0x3f3f3f3f 当哨兵
//
// int best = 0x3f3f3f3f; for (...) best = min(best, a[j]);
//
// 这是写暴力时最顺手的一行,前面三十几章也一直这么写。
// ⚠ 可这道题的 aᵢ 能取到 int 的**每一个值** —— 只要窗口里的数全都大于
// 0x3f3f3f3f = 1 061 109 567,它就会把这个哨兵当成最小值打出来。
// ⇒ 本页第 ③ 步:**「INF 该写多大」这道算术题,在这道题上无解**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
if (!(cin >> n >> k)) return 0;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
for (int pass = 0; pass < 2; pass++) {
for (int i = k; i <= n; i++) {
int best = (pass == 0) ? 0x3f3f3f3f : -0x3f3f3f3f; // ✗ 自己造的哨兵
for (int j = i - k + 1; j <= i; j++)
best = (pass == 0) ? min(best, a[j]) : max(best, a[j]);
cout << best << (i == n ? '\n' : ' ');
}
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠⚠ 而中招的不是正解,是你的参照物 —— 这是本书第一次

上面那一版不是「题解里的错法」,是你写对拍时最顺手的那份暴力

int best = 0x3f3f3f3f;
for (int j = i - k + 1; j <= i; j++) best = min(best, a[j]);

前面三十几章一直这么写,从来没出过事。可只要某个窗口里的数全都大于 1 061 109 567, 它就会把这个哨兵当成最小值打出来。

300 轮 档 0 正数 1~20 档 1 含负数 ★★ 档 2 全值域 档 3 值域 1~3
「某个窗口的数全都越过哨兵」 0 0 60 0
⇒ 参照物真的算错了 0 0 60 0

★★ 触发条件 ≡ 抓获数,四档一个不差又一次)。 ⚠ 而它只在照题面全值域造数据那一档才现形 —— 另外三档是精确的 0。 ⇒ ★★★ 这一页因此多出一条一般性的提醒: 顶格不只要顶被测程序的顶,也要顶参照物的顶。 正解(单调队列)根本没有哨兵这回事 —— 只有暴力需要一个初值,于是错的是暴力。 ★ 修法是一句话:拿窗口里的第一个数当初值p1886Brute.cpp 就是这么写的)。

3★★ 同一件事在读和写两侧各来一次:−2³¹ 的绝对值放不进 int

★ 快读里那句 `return neg ? -x : x` 要用 long long 中转

aᵢ 能取到 −2 147 483 648,而 +2 147 483 648 不是一个 int。 所以快读里累加要用 long long、输出取绝对值要走 unsigned

long long x = 0;  … ;  return (int)(neg ? -x : x);              // 读
unsigned v = x < 0 ? (unsigned)0 - (unsigned)x : (unsigned)x;   // 写

⇒ 和上一步是同一件事的两个面:题面把 int 用满了,于是每一处「顺手取个负」都要重看一遍。 ★ 而正解本身不需要任何额外类型 —— 它只是搬运 a[] 里的数,从不做算术。

4★★ 拿值判出窗:本章第 9 步那句话写反了

p1886Val.cpp✗ 队列里存值,靠「队头等于刚滑走的数」判出窗(官方样例照过)
// P1886 · 错法 ③:判「有没有滑出窗口」用**值**,不用**下标**
//
// 队列里存的是下标,可有人顺手存了值,于是出窗只能靠「队头等于刚滑走的那个数」来判:
// if (val[head] == a[i - k]) head++;
//
// ⚠ 它在「窗口里没有重复元素」时看着是对的 —— 而一旦窗口里有两个相等的数,
// 刚滑走的那个和留在窗口里的那个长得一模一样,它就会**把还在窗口里的那个弹掉**。
// ★ 本章第 9 步专门强调过:**队列里比的是值,窗口边界是拿下标判的** —— 两件事。
#include <bits/stdc++.h>
using namespace std;
static const int N = 1000006;
static int a[N], val[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
if (!(cin >> n >> k)) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int pass = 0; pass < 2; pass++) {
int head = 1, tail = 0;
string out;
for (int i = 1; i <= n; i++) {
while (head <= tail && (pass == 0 ? val[tail] >= a[i] : val[tail] <= a[i])) tail--;
val[++tail] = a[i];
if (i > k && head <= tail && val[head] == a[i - k]) head++; // ✗ 拿值判出窗
if (i >= k) { out += to_string(val[head]); out += (i == n ? '\n' : ' '); }
}
cout << out;
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

队列里存了值就没法算下标,于是只能这么判出窗:if (val[head] == a[i-k]) head++;窗口里一旦有两个相等的数,刚滑走的那个和还留着的那个长得一模一样 ⇒ 它会把留着的那个弹掉。

300 轮 档 0 档 1 含负数 档 2 全值域 ★ 档 3 值域 1~3
某个窗口内有重复元素(第一层) 215 167 0 273
⇒ 真被抓(第二层) 103 60 0 258
2.1 倍 2.8 倍 1.06 倍

★ 第一层写得太粗:「窗口里有重复」还不够,得恰好是队头那个值被误判掉才会错。 ★★ 而档 2 那个 0 是结构性的:全值域随机里两个数撞上的概率是 40 亿分之一 —— ⇒ 同一份「照题面顶格」的数据,把上一步那个 bug 逼了出来,却把这一步这个藏得死死的。 ⇒ 又一次「为一个 bug 精心造的档位,正是另一个 bug 的盲区」

5★★ 快读忘了负号 —— 对拍 0 次,官方样例一测就死

p1886Neg.cpp✗ 快读跳过所有非数字字符,把负号一起吃掉了(样例打出 1 1 1 3 3 3)
// P1886 · 错法 ④:手写快读**忘了负号**
//
// 前面三十几章的题,aᵢ 清一色是正数,快读也就一直这么写:
// while (c < '0' || c > '9') c = gc(); // 跳过所有非数字 —— 连负号一起跳了
// ⇒ 这道题的 aᵢ 可以是负的,`-3` 被读成 `3`,**一声不吭**。
//
// ⚠⚠ 而顺手写的生成器**只造正数** —— 于是这个 bug 在默认那一档是**精确的 0**
// (本页第 ⑤ 步)。算法一个字没错,输入读错了。
#include <bits/stdc++.h>
using namespace std;
static const int N = 1000006;
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;
}
int main() {
int n = rd(), k = rd();
for (int i = 1; i <= n; i++) a[i] = rd();
for (int pass = 0; pass < 2; pass++) {
int head = 1, tail = 0;
string out;
for (int i = 1; i <= n; i++) {
while (head <= tail && (pass == 0 ? a[q[tail]] >= a[i] : a[q[tail]] <= a[i])) tail--;
q[++tail] = i;
if (q[head] <= i - k) head++;
if (i >= k) { out += to_string(a[q[head]]); out += (i == n ? '\n' : ' '); }
}
fputs(out.c_str(), stdout);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 顺手写的生成器只造正数 —— 这一档是精确的 0

前面三十几章的题,aᵢ 清一色是正数;快读也就一直写成 「跳过所有不是数字的字符」。这道题的 aᵢ 可以是负的,-3 就被读成了 3一声不吭

300 轮 档 0 正数 ★ 档 1 含负数 ★ 档 2 全值域 档 3 值域 1~3
输入里真的有负数 0 300 300 0
⇒ 真被抓 0 300 300 0

★★ 触发条件 ≡ 抓获数,四档一个不差 —— 这一页第二次。 ⇒ ★★★ 而这一条最值钱的地方是:官方样例挡住了它,对拍的默认档没有。 那组样例里就摆着 -1-3。 这是「样例可能比对拍还狠」的第二次现场 —— 先跑官方样例,再写生成器;顺手写的生成器带着你上一道题的习惯。

6★★★ 一个看着像 bug、其实完全等价的写法 —— 而队列长 6.8 万倍,秒表只差 1 毫秒

p1886Eq.cpp★ 队尾保留相等的那个 —— 1200 轮 0 次不一致
while (... a[q[tail]] >  a[i]) tail--;   // 这一版:相等的留着
while (... a[q[tail]] >= a[i]) tail--;   // 正解:相等的也弹掉
★ 它为什么一定对,以及我为什么以为它会慢

留下来的那个和 a[i] 值相等,而这道题只要「最小值 / 最大值」这个、不要下标 ⇒ 队头是哪一个都打出同样的数。四个档 1200 轮,0 次不一致。

草稿里写着「但队列会长很多,顶格会慢」。队列确实长得离谱,慢却几乎没慢:

顶格 n = 10⁶ / k = n/2 / 值域只有 3 正解(弹掉相等) 保留相等 倍数
队列平均长度 1.83 124 962.59 68 285 倍
耗时 6 ms 7 ms 1.17 倍

★★★ 理由一行就能说完:均摊分析和「弹不弹相等的」没有关系 —— 每个下标仍然只进队一次、出队一次,总功仍然是 O(n)。队列长只是「同时待着的多」, 而那个数组本来就得开到 n。⇒ 连内存都不多花。

⇒ ★★ 于是这一页给「两把尺子会打架」添了最极端的一点: 一把尺子差 68 285 倍,另一把差 1.17 倍。 ⚠ 而结论不是「队列长度这把尺子没用」,是它量的根本不是时间 —— 两把尺子都要报,然后说清楚各自量的是什么。

7★★ 第二条正确的路:multiset —— 它做参照物很好,交上去会 TLE

p1886Set.cpp⚠ multiset O(n log n):答案全对,顶格 1757 毫秒(时限 1 秒)
★★ 「谁更快」的主语是 k —— 而 k 是输入给的,你控制不了

同一个 n = 10⁶只拧 kp1886Count.cppkcurve):

k 1 10 1000 n/2
单调队列 4 ms 7 ms 7 ms 7 ms
multiset 17 ms 52 ms 102 ms 743 ms

★★ multiset 版自己跟自己差 43.7 倍,而单调队列从头到尾没动过 —— 它的每一步都是 O(1),和窗口多大完全无关。 ⇒ 这和第 12 章 P1923(堆的大小是 k+1k=0 最快、k=n/2 自己跟自己差 57 倍) 是同一个形状的第二次:题面写着 1 ≤ k ≤ n, ⇒ 只能按最坏的 k 选写法。

端到端 · 顶格 n = 10⁶ / k = n/2 · 3 次取最小 时限 1 秒
单调队列(p1886.cpp 47 ms
multisetp1886Set.cpp 1757 ms ⇒ TLE

★ 但它不是没用:两版输出逐字节相同,而它跑得动顶格 —— ⇒ 拿它当顶格对拍的参照物正合适(P1803 那条: 「对拍只能跑小数据」的主语从来不是对拍,是「你把暴力当成了唯一的参照物」)。

8⚠ 边界差一:而漏掉的那 27 轮能数到底

p1886Edge.cpp✗ 窗口没满就开始输出(样例每行多出 2 个数,当场挡住)
// P1886 · 错法 ②:窗口还没满就开始输出(差一)
//
// if (i >= k) 写成 if (i >= 1) —— 于是前 k−1 个「窗口」只有一部分元素也被打了出来。
// ⇒ 输出会多出 k−1 个数,两行都多。
// ★ 这是本章第 10 步那句「边界比模板本身更容易写错」的第一种形态。
#include <bits/stdc++.h>
using namespace std;
static const int N = 1000006;
static int a[N], q[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
if (!(cin >> n >> k)) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int pass = 0; pass < 2; pass++) {
int head = 1, tail = 0;
string out;
for (int i = 1; i <= n; i++) {
while (head <= tail && (pass == 0 ? a[q[tail]] >= a[i] : a[q[tail]] <= a[i])) tail--;
q[++tail] = i;
if (q[head] <= i - k) head++;
out += to_string(a[q[head]]); // ✗ 少了 if (i >= k)
out += (i == n ? '\n' : ' ');
}
cout << out;
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

if (i >= k) 写丢了,前 k−1 个「半个窗口」也被打了出来。四个档全是 273 / 300。

★ 那漏掉的 27 轮是哪些?—— k = 1 的那 27 轮,一个不差
300 轮 档 0 档 1 档 2 档 3
生成器抽到 k = 1 的轮数 27 27 27 27
「窗口没满就输出」漏掉的轮数 27 27 27 27

★ 一行就能证:k = 1i >= k 就是 i >= 1,那句判断本来就恒成立 ⇒ 两版逐字节相同,是能证的 0。 ⇒ 又一次「说清楚它算了什么」 —— 说清楚之后,抓获率是白送的推论。

9★ 对拍这一页

参照物是那份修好了初值的暴力(第 ② 步)。⚠ 而这一页的教训之一就是: 参照物本身先得过一遍题面的顶格。

300 轮(n 随机 814,k 随机 1n) 档 0 正数 1~20 档 1 含负数 ★★ 档 2 全值域 ★ 档 3 值域 1~3
单调队列(正解) 0 0 0 0
multiset 0 0 0 0
队尾保留相等 0 0 0 0
暴力用 0x3f3f3f3f 当初值 0 0 60 0
窗口没满就输出 273 273 273 273
拿值判出窗 103 60 0 258
快读忘了负号 0 300 300 0
★ 官方样例这一轮挡住两个、放过两个 —— 而放过的两个原因完全不同
错法 抓获率(最狠的那一档) 官方样例挡住了吗
窗口没满就输出 273 / 300 挡住(每行多出 2 个数)
快读忘了负号 300 / 300 挡住(样例里有 -1-3
拿值判出窗 258 / 300 放过 —— 样例的每个窗口里都没有重复元素
暴力用 0x3f3f3f3f 60 / 300 放过 —— 样例里最大的数是 7,够不着哨兵

⇒ 两个「放过」都是结构上问不出这个问题,而不是概率低。 ★ 而「快读忘了负号」是对拍的默认档抓不到、官方样例一测就死 —— 两道防线各有各的盲区,缺一不可。

10度量程序和生成器

p1886Count.cpp度量程序(本页所有数字都出自它)
p1886Gen.cpp(六个档位)数据生成器
p1886Brute.cpp参照物:每个窗口扫一遍(★ 初值取窗口里的第一个数)

11一页纸

★★ 关键的一步 队列存下标,队尾扔掉「又不更优、又更早出窗」的;两问只差比较符号
★★★ 题面把 int 用光了 [−2³¹, 2³¹) 占掉全部 4 294 967 296 个值 ⇒ 一个哨兵都不剩
★★★ 中招的是参照物 顺手写的暴力 best = 0x3f3f3f3f 在全值域档错 60 / 300(触发 ≡ 抓获)
⇒ 一条新提醒 顶格不只要顶被测程序的顶,也要顶参照物的顶
★★ 样例比对拍还狠 「快读忘了负号」对拍默认档 0 / 300,而官方样例里就摆着 -1
★★★ 草稿被打回 「保留相等会慢」错了 —— 队列长 68 285 倍,秒表只差 1.17 倍(均摊和它无关)
★★ 谁更快的主语是 k multiset 自己跟自己差 43.7 倍(17 → 743 ms),单调队列纹丝不动(4 → 7)
⇒ 而 k 是输入给的 顶格端到端 47 ms vs 1757 ms ⇒ multiset 只配当参照物
★ 边界差一 273 / 300,⚠ 漏掉的 27 轮 ≡ k = 1 的 27 轮(那句判断恒成立,能证)
★ 拿值判出窗 两层:窗口内有重复 215 / 真被抓 103;⚠ 全值域那一档是结构性的 0