题单 · 习题解析

洛谷 P1115 最大子段和

★★★ 对拍的抓获数不是概率是一个计数:八个格子里「被抓轮数」和「满足触发条件的轮数」一个不差

原题:洛谷 P1115出自 第 11 章 分治 的题单出自 第 12 章 分治进阶 的题单题面本地存档:2026-08-27
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。

输入格式

第一行是一个整数,表示序列的长度 n

第二行有 n 个整数,第 i 个整数表示序列的第 i 个数字 aᵢ

输出格式

输出一行一个整数表示答案。

说明 / 提示

样例 1 解释:选取 [3, 5] 子段 {3, -1, 2},其和为 4

数据规模与约定

  • 对于 40% 的数据,保证 n ≤ 2 × 10³
  • 对于 100% 的数据,保证 1 ≤ n ≤ 2 × 10⁵-10⁴ ≤ aᵢ ≤ 10⁴

2026/01/21:增加一组 hack 数据。

输入输出样例

输入

7
2 -4 3 -1 2 -4 3

输出

4

1⚠ 这道题的全部难度,是题面里的两个字

★★★ 「连续且非空的一段」

把「非空」两个字删掉,这道题的标准答案就变成下面第 ② 步那个的写法。

  • 允许空段 ⇒ 答案下限是 0(什么都不选,和为 0)⇒ ans = 0 起手是对的;
  • 不允许空段 ⇒ 全是负数时,答案是最大的那个负数ans = 0 起手就是 WA。

这道题只有一条 bug 触发线,而它是题面里的两个字。 下面整页都在量这条线:它有多窄、随机数据能不能撞上、换个实现还认不认得出来。

p1115.cpp★ 这一版就能 AC
// P1115 最大子段和 —— 能 AC 的那一版(O(n) 扫一遍)
//
// 递推一句话:**以第 i 个数结尾**的最大子段和,要么是「前面那一段接上 a[i]」,
// 要么是「从 a[i] 重新开始」——
// cur = max(a[i], cur + a[i])
// 全程取最大值就是答案。
//
// ⚠ 起手值是这道题唯一的坑:`ans` 和 `cur` 都从 **a[0]** 开始,不是从 0 开始。
// 题面写的是「选出其中连续且**非空**的一段」——
// 那两个字就是这道题的全部难度(解析页第 ② 步把它量出来了)。
//
// ⚠ 用不用 long long?**这道题刚好不用**:n ≤ 2×10⁵、|aᵢ| ≤ 10⁴
// ⇒ 和最大 2×10⁹,而 int 的上限是 2 147 483 647 —— 只差 1.47×10⁸,余量约 7%。
// 这是一道算术题,不是「保险起见都开」。(同一章的 P1908 是反过来的:差 58 倍。)
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 200005;
static int a[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int i = 0; i < n; i++) cin >> a[i];
int ans = a[0], cur = a[0]; // ★ 从 a[0] 起手,不是 0
for (int i = 1; i < n; i++) {
cur = max(a[i], cur + a[i]);
ans = max(ans, cur);
}
cout << ans << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

顶格数据(n = 2 × 10⁵)本机实测:0.01 秒以内,时限 1 秒。

2⚠ 第 ① 版:ans = 0 起手 —— 教科书上最常见的写法

p1115Zero.cpp⚠ ans 从 0 起手
// P1115 ⚠ 第 ① 版:`ans` 从 0 起手 —— 教科书上最常见的写法,而它在这道题上是错的
//
// 「和变成负数就丢掉,从头再来」——这套写法本身没问题,
// 问题出在 **ans 的起手值是 0**:它等于偷偷允许了「一段都不选」。
//
// ⇒ 只要序列里**有一个非负数**,它就是对的;
// **所有数都是负数**时,它输出 0,而正确答案是「最大的那个负数」。
//
// ★ 触发条件是一个**开关**,不是一个概率:max(a) < 0。
// ⇒ 对拍的抓获率就等于「这一档数据里全负的比例」——
// 而那个比例是能**事先算出来**的(解析页第 ③ 步)。
//
// ★ 最小的反例只有一个数:
// 输入 `1` / `-1` ⇒ 它输出 0,正确答案 -1。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 200005;
static int a[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int i = 0; i < n; i++) cin >> a[i];
int ans = 0, cur = 0; // ⚠ 就是这里
for (int i = 0; i < n; i++) {
cur += a[i];
if (cur > ans) ans = cur;
if (cur < 0) cur = 0;
}
cout << ans << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

「和变成负数就丢掉,从头再来」——这套写法本身没问题。 问题在 ans起手值是 0:它等于偷偷允许了「一段都不选」。

样例过(4)。而最小的反例只要一个数

输入 正确答案 ⚠ 它输出
1 / -1 -1 0

3★★★ 抓获率不是概率 —— 它是一个能数出来的计数

p1115Gen.cpp生成器:五档
// 数据生成器(P1115 对拍用):`./p1115Gen <seed> [level] [n]`
//
// level 0(默认)★ **n ≤ 10**,aᵢ ∈ [-10⁴, 10⁴] —— 抓获率能**先算出来**的那一档
// level 1 n ≤ 200,同样的值域 —— 只把 n 放宽,抓获率就塌了
// level 2 ★ **全是负数**(aᵢ ∈ [-10⁴, -1])—— 触发线那一档,300/300
// level 3 n = 1(最小的反例:`1` / `-1` 就够)
// level 4 顶格 n(第三个参数,默认 2×10⁵)
//
// ★ 这道题的 bug 触发条件是一个**开关**:max(a) < 0。
// ⇒ 抓获率 = 这一档里「全负」的比例,而那个比例是算术:
// level 0 里 n 均匀取 1..10、每个数为负的概率 p = 10000/20001 ≈ 0.49998
// ⇒ P(全负) = (1/10) Σ_{n=1..10} pⁿ ≈ **9.99%** ⇒ 300 轮期望 **29.97** 次。
// level 1 里 n 均匀取 1..200 ⇒ 掉到约 **0.5%**(期望 1.5 次)。
// ⇒ 这一次是**先算后量**,不是量完再解释。
#include <bits/stdc++.h>
using namespace std;
static mt19937 rng;
static int ri(int lo, int hi) { return lo + (int)(rng() % (unsigned)(hi - lo + 1)); }
int main(int argc, char** argv) {
rng.seed((argc > 1) ? (unsigned)atoi(argv[1]) : 1);
int level = (argc > 2) ? atoi(argv[2]) : 0;
int n;
if (level == 1) n = ri(1, 200);
else if (level == 3) n = 1;
else if (level == 4) n = 200000;
else n = ri(1, 10);
if (argc > 3) n = atoi(argv[3]);
printf("%d\n", n);
for (int i = 0; i < n; i++) {
int v = (level == 2) ? ri(-10000, -1) : ri(-10000, 10000);
printf("%d%c", v, i + 1 == n ? '\n' : ' ');
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

这个 bug 的触发条件是一个开关max(a) < 0),不是一个「碰运气」。 所以在动手对拍之前,抓获率就已经能算出来了

level 0n 均匀取 1..10、每个数为负的概率 p = 10000/20001 ≈ 0.49998P(全负) = (1/10) Σ pⁿ ≈ 9.99% ⇒ 300 轮的期望29.97 次。

四档 × 300 轮,实测:

n 值域 这 300 组里「全负」的有几组 p1115Zero 被抓
level 0 1~10 [-10⁴, 10⁴] 38 38 / 300
level 1 1~200 [-10⁴, 10⁴] 1 1 / 300
level 2 ★ 全负 1~10 [-10⁴, -1] 300 300 / 300
level 3 n = 1 [-10⁴, 10⁴] 130 130 / 300
★★★ 看两列数字:它们不是「接近」,是「一模一样」

「被抓的轮数」和「满足触发条件的轮数」四格全等,一个不多一个不少。

⇒ 这不是巧合,是这类 bug 的本性:抓获数根本不是一个统计量,它是一个计数。 把生成器造的那 300 组数据拿出来,数一数有几组满足触发条件,那就是抓获数

★ 于是有一条马上能用的操作:

对拍抓不到的时候,别急着加轮数 —— 先去数一数你这 300 组里有几组满足触发条件。 如果是 0,那么加到 3000 轮、30000 轮,还是 0。

(这也是第 5 章 P1042「概率低 vs 结构上不可能」那条的度量方式: 「结构上不可能」就是这个计数恰好为 0。)

⚠ 而我先算的那个 29.97,和实测的 38 差了 27%

算术给的是期望,实测这 300 个种子恰好造出 38 组全负 —— 涨落而已(标准差约 5.2)。

⇒ 两件事都要做,但用途不同: 算术定量级(「这一档大概能抓到一成」还是「一次都抓不到」), 计数定精确值(断言里写的必须是这一个)。 ⚠ 而断言里绝不能写算出来的那个期望 —— 这本书上一次栽在这上头是 第 10 章 P1104:推了个 49,实测 30,check:viz 第一次跑就红。

4★★ 同一个 bug 换一副身体:分治版

本章题单点名要拿这道题练分治的「跨越中点」。先看写对的那版:

p1115Divide.cpp★ 分治(写对的)
// P1115 第 ③ 版:分治 —— 本章题单点名要练的那一版
//
// 「跨越中点的那部分怎么算」才是分治题的真正内容(本章正文第 ⑧ 步):
//
// 最大子段和 = max( 全在左边的, 全在右边的, **跨过中点的** )
// ↑递归 ↑递归 ↑左半边的最大后缀 + 右半边的最大前缀
//
// ⚠ 而「最大后缀 / 最大前缀」这两个量**必须非空** ——
// 它们各自至少要含一个元素,跨中点那一段才真的跨过了中点。
// 写成「允许为空(即下限 0)」就退化成 p1115DivideBad.cpp,
// 而那和第 ① 版的 `ans = 0` **是同一个 bug 的另一副身体**。
//
// 一次递归返回四个量:整段和、最大前缀、最大后缀、区间内最大子段和。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 200005;
static int a[MAXN];
struct Node { long long sum, pre, suf, best; };
static Node solve(int l, int r) {
if (l == r) return {a[l], a[l], a[l], a[l]}; // ★ 单个元素:四个量都是 a[l]
int mid = l + (r - l) / 2;
Node L = solve(l, mid), R = solve(mid + 1, r);
Node c;
c.sum = L.sum + R.sum;
c.pre = max(L.pre, L.sum + R.pre); // 前缀:要么只在左边,要么吃掉整个左边
c.suf = max(R.suf, R.sum + L.suf);
c.best = max(max(L.best, R.best), L.suf + R.pre); // ★ 跨中点:左最大后缀 + 右最大前缀
return c;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int i = 0; i < n; i++) cin >> a[i];
cout << solve(0, n - 1).best << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

一次递归返回四个量:整段和、最大前缀、最大后缀、区间内最大子段和。 跨中点那一段 = 左半边的最大后缀 + 右半边的最大前缀 —— 这就是这道题分治的全部内容。

p1115DivideBad.cpp⚠ 前缀 / 后缀允许为空
// P1115 ⚠ 第 ③' 版:分治,但「最大前缀 / 最大后缀」允许为空
//
// 和 p1115Divide.cpp 的差别只有两处 `max(0LL, …)` —— 看起来像是「顺手加个保护」。
//
// ★ 而它和第 ① 版 `ans = 0` **是同一个 bug**:
// 允许前缀 / 后缀为空 ⇒ 跨中点那一段可以是「空 + 空 = 0」⇒ 又一次偷偷允许了「不选」。
// 触发线一模一样:**所有数都是负数**。
//
// ⇒ 这一版留在这儿只为一句话:**换了实现不等于换了 bug。**
// 两处代码长得毫不相干,`p1115Zero` 是在主循环里错的、它是在合并那一步错的,
// 而它们在同一档数据上一起翻车、在别的档上一起正确 —— 逐字节相同。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 200005;
static int a[MAXN];
struct Node { long long sum, pre, suf, best; };
static Node solve(int l, int r) {
if (l == r) return {a[l], max(0LL, (long long)a[l]), max(0LL, (long long)a[l]), a[l]};
int mid = l + (r - l) / 2;
Node L = solve(l, mid), R = solve(mid + 1, r);
Node c;
c.sum = L.sum + R.sum;
c.pre = max(0LL, max(L.pre, L.sum + R.pre)); // ⚠ 就是这个 0
c.suf = max(0LL, max(R.suf, R.sum + L.suf)); // ⚠ 和这个
c.best = max(max(L.best, R.best), L.suf + R.pre);
return c;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int i = 0; i < n; i++) cin >> a[i];
cout << solve(0, n - 1).best << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

差别只有两处 max(0LL, …),看起来像是「顺手加个保护」。

★★ 它和第 ② 步那个 ans = 0 是同一个 bug —— 而两处代码毫不相干

允许前缀 / 后缀为空 ⇒ 跨中点那一段可以是「空 + 空 = 0」⇒ 又一次偷偷允许了「不选」

p1115Zero 错在主循环的起手值上,p1115DivideBad 错在合并那一步上 —— 两段代码没有一行是像的,可它们:

p1115Zero p1115DivideBad
level 0(n ≤ 10 38 / 300 17 / 300
level 2(全负) 300 / 300 266 / 300
level 3(n = 1 130 / 300 0 / 300

换了实现不等于换了 bug。

★ 而那三处差额全部来自同一件事:分治版在 n = 1 上不触发 (只有一个元素时根本没有「跨中点」这一步)。 所以它的触发线比另一版窄一个 n = 1: level 2 那 300 组里恰好有 34 组是 n = 1300 - 34 = 266; level 0 那 38 组全负里有 21 组是 n = 138 - 21 = 17又是精确对得上的计数。

5★ 换两把尺子:分治真的是 O(n log n) 吗

p1115Count.cpp两把尺子 + 部分分的账
// P1115 换两把尺子:扫描 vs 分治,以及「暴力能拿多少分」这笔账
//
// 用法:./p1115Count [n] 人话版(默认 n = 2×10⁵,题面顶格)
// ./p1115Count [n] csv 给 check:viz 用
//
// 三件事:
// ① ★ **「分治求最大子段和是 O(n log n)」这句话,对写法有要求。**
// 每次合并都现扫一遍求最大前后缀,才是 O(n log n);
// 像 p1115Divide.cpp 那样**把四个量一起返回**,合并就是 O(1) ——
// 一共 2n-1 次调用,总复杂度其实是 **O(n)**,和扫描版同一档。
// ② 同一档复杂度,**常数不一样**:递归调用 + 栈 + 访存模式,实测差几倍。
// ③ O(n²) 暴力在 40% 那一档(n ≤ 2×10³)真跑一次,再外推到顶格 ——
// 「先写个暴力拿 40 分」是一道**算术题**。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static const int MAXN = 200005;
static int a[MAXN];
static long long calls = 0, maxDepth = 0;
struct Node { long long sum, pre, suf, best; };
static Node solve(int l, int r, long long depth) {
calls++;
maxDepth = max(maxDepth, depth);
if (l == r) return {a[l], a[l], a[l], a[l]};
int mid = l + (r - l) / 2;
Node L = solve(l, mid, depth + 1), R = solve(mid + 1, r, depth + 1);
Node c;
c.sum = L.sum + R.sum;
c.pre = max(L.pre, L.sum + R.pre);
c.suf = max(R.suf, R.sum + L.suf);
c.best = max(max(L.best, R.best), L.suf + R.pre);
return c;
}
int main(int argc, char** argv) {
int n = (argc > 1) ? atoi(argv[1]) : 200000;
bool csv = (argc > 2 && string(argv[2]) == "csv");
mt19937 rng(20260827u);
for (int i = 0; i < n; i++) a[i] = (int)(rng() % 20001u) - 10000;
auto t0 = steady_clock::now();
long long ansScan = a[0], cur = a[0];
for (int i = 1; i < n; i++) { cur = max((long long)a[i], cur + a[i]); ansScan = max(ansScan, cur); }
double msScan = duration<double, milli>(steady_clock::now() - t0).count();
t0 = steady_clock::now();
long long ansDiv = solve(0, n - 1, 1).best;
double msDiv = duration<double, milli>(steady_clock::now() - t0).count();
/* ③ O(n²) 在 40% 那一档上真跑 */
const int NSMALL = 2000;
static long long pre[MAXN];
for (int i = 0; i < NSMALL; i++) pre[i + 1] = pre[i] + a[i];
t0 = steady_clock::now();
long long ansSq = LLONG_MIN;
for (int i = 0; i < NSMALL; i++)
for (int j = i; j < NSMALL; j++)
ansSq = max(ansSq, pre[j + 1] - pre[i]);
double msSq = duration<double, milli>(steady_clock::now() - t0).count();
long long ansSmall = a[0], curS = a[0]; // 同一段前缀,扫描版的答案(拿来对一下)
for (int i = 1; i < NSMALL; i++) { curS = max((long long)a[i], curS + a[i]); ansSmall = max(ansSmall, curS); }
double sqOpsSmall = (double)NSMALL * (NSMALL + 1) / 2;
double sqOpsFull = (double)n * (n + 1) / 2;
double sqSecFull = msSq / sqOpsSmall * sqOpsFull / 1000.0;
/* int 够不够:这道题的和最大 2×10⁵ × 10⁴ */
long long worstSum = (long long)n * 10000;
double headroom = (2147483647.0 - (double)worstSum) / 2147483647.0 * 100.0;
if (csv) {
printf("n,%d\nsame,%d\n", n, (ansScan == ansDiv) ? 1 : 0);
printf("calls,%lld\nexpectCalls,%d\ndepth,%lld\n", calls, 2 * n - 1, maxDepth);
printf("divSlower,%d\n", (msDiv > msScan) ? 1 : 0);
printf("sqOk,%d\nsqFitsInSubtask,%d\n", (ansSq == ansSmall) ? 1 : 0, (msSq < 100.0) ? 1 : 0);
printf("sqFullSecGE,%d\n", (int)sqSecFull);
printf("worstSum,%lld\nintEnough,%d\nheadroomPct,%d\n",
worstSum, (worstSum <= 2147483647LL) ? 1 : 0, (int)headroom);
return 0;
}
printf("n = %d(题面顶格),随机数据:\n\n", n);
printf(" 扫描 O(n) %7.2f 毫秒 n = %d 次更新\n", msScan, n);
printf(" 分治 %7.2f 毫秒 %lld 次递归调用(= 2n-1,%s),最深 %lld 层\n",
msDiv, calls, (calls == 2LL * n - 1) ? "对上了" : "对不上", maxDepth);
printf(" 两边答案 %lld %s\n\n", ansScan, (ansScan == ansDiv) ? "一致" : "居然不一样!");
printf(" ⇒ ★ 合并写成 O(1) 之后,分治其实也是 **O(n)** ——\n");
printf(" 「分治是 O(n log n)」说的是每次合并现扫一遍的那种写法。\n");
printf(" 同一档复杂度,分治慢 %.1f 倍:递归调用 + 栈 + 访存都要钱。\n\n", msDiv / max(0.001, msScan));
printf(" O(n²) 暴力:n = %d(40%% 那一档)%.2f 毫秒(答案 %lld,%s)⇒ 稳过\n",
NSMALL, msSq, ansSq, (ansSq == ansSmall) ? "和扫描版在同一段前缀上一致" : "居然不一样!");
printf(" n = %d(顶格)按同一速率外推 **约 %.0f 秒** ⇒ 时限 1 秒,没戏\n\n", n, sqSecFull);
printf(" ⇒ 「先写个暴力拿 40 分」不是安慰奖:部分分那一栏给的 n,\n");
printf(" 就是出题人替你算好的「暴力能跑到哪儿」。\n\n");
printf(" 顺带:和最大 %d × 10⁴ = %lld,int 上限 2147483647 ⇒ %s,余量只有 %.1f%%。\n",
n, worstSum, (worstSum <= 2147483647LL) ? "刚好够" : "不够", headroom);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★ 「分治求最大子段和是 O(n log n)」这句话,对写法有要求

那句话说的是每次合并都现扫一遍求最大前后缀的写法(每层 O(n),共 log n 层)。

而上面那份把四个量一起返回了,合并就是 O(1) —— n = 2×10⁵ 时一共 399 999 次递归调用(正好 2n-1),最深 19 层。 ⇒ 它其实是 O(n)和扫描版同一档

同一档复杂度,实测分治仍然慢 5 倍上下(0.19 毫秒 vs 0.88 毫秒): 递归调用、栈、访存模式都要钱。 ⇒ 第 7 章 P1873 那条的邻居:那道题是「复杂度更优的反而慢」, 这道题是「复杂度一样,常数差五倍」。

★ 顺带两笔算术,都值三十秒

① 暴力能拿多少分,是算得出来的。 O(n²)n = 2×10³(题面写的 40% 那一档)实测 1.0 毫秒,稳过; 按同一速率外推到顶格 n = 2×10⁵约 10 秒,时限 1 秒。 ⇒ 部分分那一栏给的 n,就是出题人替你算好的「暴力能跑到哪儿」。

② 这道题要不要开 long long?不用 —— 而这是算出来的,不是感觉出来的。 和最大 2×10⁵ × 10⁴ = 2 000 000 000int 上限 2 147 483 647 ⇒ 只差 1.47 × 10⁸余量约 7%。 ⚠ 和同一章的 P1908 正好两个方向:那道题差 58 倍,必须开。 ⇒ 「保险起见都开 long long」不算理由;算一下只要三十秒

6一张总表

版本 做法 样例 n=1, a=-1 全负 300 轮 顶格 结果
p1115Zero 扫描,ans = 0 起手 ✓ 4 ✗ 0 ✗ 300/300 被抓 0.01 秒 ✗ WA
p1115Sq 前缀和枚举两端 O(n²) ✓ 4 外推 10 秒 ✗ TLE(40 分)
p1115DivideBad 分治,前后缀允许空 ✓ 4 ★ ✓ -1 ✗ 266/300 被抓 0.01 秒 ✗ WA
p1115Divide 分治,前后缀非空 ✓ 4 0.01 秒 ★ AC
p1115 扫描,ans = a[0] 起手 ✓ 4 0.01 秒 AC
这一页记住三句话
  1. ★★★ 对拍的抓获数不是概率,是一个计数。 四档 × 两个 bug 共八个格子,「被抓轮数」和「满足触发条件的轮数」一个不差。 ⇒ 抓不到的时候别加轮数,去数一数你那 300 组里有几组满足触发条件 —— 是 0 就永远是 0。 ⚠ 而算出来的期望不能写进断言:我先算的是 29.97,实测是 38。
  2. ★★★ 这道题唯一的一条 bug 线,是题面里的两个字(「连续且非空」)。 删掉那两个字,错的写法就是对的。最小的反例只要一个数:n = 1, a = [-1]
  3. ★★ 换了实现不等于换了 bug。 扫描版的 ans = 0 和分治版的「前后缀允许为空」是同一件事的两副身体 —— 两段代码没有一行是像的,触发线只差一个 n = 1