题单 · 习题解析

洛谷 P2367 语文成绩

★★★ 算法是三行差分,真正的门槛是读入:n ≤ 5×10⁶,连 scanf 都不够(1.06 秒,只超 6%),只有快读过得去;★★ 第 38 章回头补了第 ⑦ 步:**这道题该不该上树状数组** —— 能,答案逐字节相同,⚠⚠ 而 2026-09-05 重测出「它会不会 TLE」的主语是**数据的形状**(覆盖 [1,n] 是 0.23 秒过、随机区间才 1.17 秒挂,而两档摸的节点数只差 2.4%、碰过的缓存行差 29762 倍);判据仍是一句「所有修改是不是都在所有询问之前」(是 ⇒ 差分够;否 ⇒ 才是 [P3368](/sol/p3368/))

原题:洛谷 P2367出自 第 6 章 前缀和与差分 的题单出自 第 38 章 树状数组 的题单题面本地存档:2026-08-26
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景:语文考试结束了,成绩还是一如既往地有问题。

题目描述

语文老师总是写错成绩,所以当她修改成绩的时候,总是累得不行。 她总是要一遍遍地给某些同学增加分数,又要注意最低分是多少。你能帮帮她吗?

输入格式

第一行有两个整数 n、p,代表学生数与增加分数的次数。

第二行有 n 个数,a₁ ~ aₙ,代表各个学生的初始成绩。

接下来 p 行,每行有三个数 x、y、z,代表给第 x 个到第 y 个学生每人增加 z 分。

输出格式:输出仅一行,代表更改分数后,全班的最低分。

数据范围

  • 对于 40% 的数据,有 n ≤ 10³;
  • 对于 60% 的数据,有 n ≤ 10⁴;
  • 对于 80% 的数据,有 n ≤ 10⁵;
  • 对于 100% 的数据,有 n ≤ 5 × 10⁶,p ≤ n,学生初始成绩 ≤ 100,z ≤ 100。

输入输出样例

输入

3 2
1 1 1
1 2 1
2 3 1

输出

2

3 个学生初始都是 1 分;1 2 1 给第 12 个各加 1 分,2 3 1 给第 23 个各加 1 分。 最后是 2 3 2,最低分 2。上面那段输出是仓库里的 p2367.cpp 真跑出来的。

学生版:一页看懂这道题

这道题考什么:差分。区间加、最后问最低分,只记变化量,最后还原一次。算法三行,真正决定分数的是读入。

最容易错的三处

  1. 读入是主项。满数据有两千万个整数,cin 默认要 3 秒多,超时三倍。关同步和 scanf 也不够,要手写快读。
  2. d[y+1] 要开到 n+1。y 可以等于 n,这一行会写到数组外面。样例里 y 从没碰到 n,所以对拍要专门造这一档。
  3. 先读数据范围再动手。n ≤ 5×10⁶ 同时决定了用差分、读入预算和数组大小。

能 AC 的:第 ③ 版(差分 + 手写快读)。它的读入约 0.36 秒,算法部分只有几分之一秒。

下面每一步里灰色的「深入」块是折叠的:那是这些结论怎么实测出来的,第一遍可以跳过。

1先盯住数据范围里那个数:n ≤ 5 × 10⁶

这道题的算法是第 6 章后半场的模板(差分,三行), 真正决定分数的是另一件事:

n ≤ 5 000 000,p ≤ n
⇒ 最坏要读 5×10⁶ + 3×5×10⁶ = 两千万个整数
⇒ 输入文件本身就有 100 MB 左右
而时限是 1 秒

★ 「读入」在这道题上不是常数,是主项。 第 ④ 步会把四种读法量给你看 —— 结论会有点吓人:连 scanf 都不够。

2第 ① 版:题目怎么说就怎么做(40 分)

p2367Brute.cpp第 ① 版:一个一个加
样例当然对。它挂的是规模。
// P2367 的第 ① 版:题目怎么说就怎么做 —— 每次操作,把 [x, y] 里每个人的成绩加上 z
//
// ★ 它在「对于 40% 的数据,n ≤ 10³」那一档上稳过(最多 10⁶ 次加法),
// ⇒ 考场上的 40 分。⚠ 满数据 n = p = 5×10⁶、每次都覆盖全班 ⇒ 2.5×10¹³ 次加法。
//
// 慢在哪一眼可见:**同一个人被反复加了无数遍**,而最后只问一次最低分。
// 第 6 章后半场的「关键的一步」就是从这句话里长出来的:**只记变化量**。
#include <bits/stdc++.h>
using namespace std;
static int a[5000005];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, p;
cin >> n >> p;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 0; i < p; i++) {
int x, y, z;
cin >> x >> y >> z;
for (int j = x; j <= y; j++) a[j] += z; // ← 一个一个加
}
int ans = INT_MAX;
for (int i = 1; i <= n; i++) ans = min(ans, a[i]);
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
深入:一个一个加的秒表与外推(进阶)

本机实测(A 机 · WSL2 · Linux 6.18 / 12 线程 / 7 GB,2026-09-05 重测,独占, 各跑三次取中位数;最坏形状:每次操作都覆盖全班):

n = p ① 一个一个加 ② 差分
1 000(40% 那一档) ★ 0.00 秒 0.00 秒
10 000 0.00 秒 0.00 秒
50 000 0.20 秒 0.00 秒
200 000 3.22 秒 0.00 秒
5 000 000(满数据) 约 2000 秒(按 n² 外推) ★ 0.10 秒

★ 那个外推值有两个互相独立的基准,而它们对得上:0.20 × 100² = 2000、3.22 × 25² = 2013。 ⚠⚠ 这张表 2026-09-05 整个重测过(旧的一版是 0.02 / 0.44 / 7.75 / 约 4400, 2026-08-26 在 B 机上量的)——「秒数跨机器一律作废」是老规矩, ⚠ 而第 ⑤ 步下面那个对照把更要命的一层量了出来:旧数字是在有负载的时候量的。

★ 它在「40% 的数据 n ≤ 10³」那一档上稳过 —— 考场上是实打实的 40 分, 而写它只要五行。⚠ 但注意:这道题的部分分是按 n 分档的, n ≤ 10⁵ 那一档(80%)它也过不去(10¹⁰ 次加法)。

3第 ② 版:差分 —— 只记变化量

d[x] += z;
d[y + 1] -= z;         // ⚠ 数组要开到 n+1:y = n 时这一行会写 d[n+1]

最后对 d 求一遍前缀和,就还原出「第 i 个人一共加了多少」。

p2367.cpp第 ② 版:差分 + 快读(能 AC)
// P2367 语文成绩 —— 能 AC 的那一版:差分 + 快读
//
// 算法就是第 6 章后半场那三行:
// 区间 [x, y] 每人加 z ⇒ d[x] += z; d[y+1] -= z;
// 最后对 d 求一遍前缀和,就还原出每个人加了多少。
// O(n + p) 代替 O(np)。
//
// ★★ 但这道题真正卡人的**不是算法,是读入**:n ≤ 5×10⁶、p ≤ n ⇒
// 最坏要读 5×10⁶ + 3×5×10⁶ = **两千万个整数**,而时限只有 1 秒。
// 本机实测(正文第 ④ 步):同一个差分算法,用默认的 cin 读要 3.5 秒,用快读 0.35 秒。
// ⇒ 第 45 章那一节(四种读法差多少)在这道题上是**分数线**,不是趣味知识。
//
// 快读的原理:一次 fread 把一大块字节拿进来,自己拼数字,
// 不经过任何格式解析(scanf 那套 `%d` 的解析器)。
#include <bits/stdc++.h>
using namespace std;
static char ibuf[1 << 22];
static size_t ipos = 0, ilen = 0;
static inline int gc() {
if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return EOF; }
return ibuf[ipos++];
}
static inline int readInt() {
int c = gc(), s = 1, x = 0;
while (c != EOF && (c < '0' || c > '9') && c != '-') c = gc();
if (c == '-') { s = -1; c = gc(); }
for (; c >= '0' && c <= '9'; c = gc()) x = x * 10 + (c - '0');
return x * s;
}
static int a[5000005];
static int d[5000006]; // ⚠ 要开到 n+1:y = n 时会写 d[n+1]
int main() {
int n = readInt(), p = readInt();
for (int i = 1; i <= n; i++) a[i] = readInt();
for (int i = 0; i < p; i++) {
int x = readInt(), y = readInt(), z = readInt();
d[x] += z;
d[y + 1] -= z; // ← 差分的全部内容就这两行
}
int cur = 0, ans = INT_MAX;
for (int i = 1; i <= n; i++) {
cur += d[i]; // 前缀和还原「第 i 个人一共加了多少」
ans = min(ans, a[i] + cur);
}
printf("%d\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 两个下标,和一个不用担心的账
  • d[y+1]:y 可以等于 n,所以数组必须开到 n + 1。 这是差分唯一能写错的地方,对拍档位 1 专门盯它。
  • ans 不用开 long long:最低分最大是 100 + 100 × p ≤ 100 + 5×10⁸, 离 int 的 2.1×10⁹ 还差四倍。★ 这一笔仍然要算 —— 算完确认不用,和没算过是两回事。

4★★★ 这道题的第二关:四种读法,只有一种过得去

深入:四种读法的实测(进阶)

同一个差分算法,只把读入换掉:

p2367Read.cpp四种读法跑同一个差分
参数是 n(默认 100 万,满数据是 500 万;页面上跑 100 万就够看出形状了)。它自己造数据,不用喂输入。
// P2367 的第二关:同一个差分算法,四种读法差多少
//
// 用法:./p2367Read [n] [csv] 默认 n = 1000000(满数据是 5000000)
// ★ 它自己造一份 P2367 形状的输入写进临时文件,再 freopen 回 stdin 读四遍 ——
// 不需要喂输入。四遍的**答案必须一模一样**(csv 里的 same 就是钉这件事的)。
//
// 四种读法(第 45 章第 11 步解释过它们的区别,这里量的是**这道题上的**代价):
// ① cin(默认,同步开着) ② cin + sync_with_stdio(false)
// ③ scanf ④ 手写快读(fread 整块读进来自己拼数字)
//
// ⚠ 顺序不能换:关掉同步之后 cin 会自己预读一大块,之后再 freopen 换文件,
// cin 缓冲里剩的就是上一份文件的残渣 ⇒ 「同步开着」那一趟必须排在最前面
// (这条坑是第 45 章 read.cpp 里踩过的,照抄它的顺序)。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static char path[64];
static int a[5000005];
static int d[5000006];
static void makeData(int n) {
snprintf(path, sizeof(path), "/tmp/p2367-bench-%d.txt", (int)getpid());
FILE* f = fopen(path, "w");
if (!f) { fprintf(stderr, "写不出临时文件 %s\n", path); exit(1); }
mt19937 rng(20260826u);
fprintf(f, "%d %d\n", n, n);
for (int i = 0; i < n; i++) fprintf(f, "%d%c", (int)(rng() % 101), i + 1 == n ? '\n' : ' ');
for (int i = 0; i < n; i++) {
int x = 1 + (int)(rng() % (unsigned)n);
int y = x + (int)(rng() % (unsigned)(n - x + 1));
fprintf(f, "%d %d %d\n", x, y, (int)(rng() % 101));
}
fclose(f);
}
static void reopen() {
if (!freopen(path, "r", stdin)) { fprintf(stderr, "读不回临时文件\n"); exit(1); }
}
/* 差分那三行 —— 四趟共用,所以四趟之间的差别只可能来自读入 */
static int finish(int n) {
int cur = 0, ans = INT_MAX;
for (int i = 1; i <= n; i++) { cur += d[i]; ans = min(ans, a[i] + cur); }
return ans;
}
static void reset(int n) { memset(d, 0, sizeof(int) * (size_t)(n + 2)); }
static char ibuf[1 << 22];
static size_t ipos = 0, ilen = 0;
static inline int gc() {
if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return EOF; }
return ibuf[ipos++];
}
static inline int readIntFast() {
int c = gc(), x = 0;
while (c != EOF && (c < '0' || c > '9')) c = gc();
for (; c >= '0' && c <= '9'; c = gc()) x = x * 10 + (c - '0');
return x;
}
int main(int argc, char** argv) {
int n = (argc > 1) ? atoi(argv[1]) : 1000000;
bool csv = (argc > 2 && string(argv[2]) == "csv");
makeData(n);
double ms[4];
int ans[4];
/* ① cin(默认,同步开着)—— 必须排第一趟 */
{ reopen(); reset(n);
auto t0 = steady_clock::now();
int m, p; cin >> m >> p;
for (int i = 1; i <= m; i++) cin >> a[i];
for (int i = 0; i < p; i++) { int x, y, z; cin >> x >> y >> z; d[x] += z; d[y + 1] -= z; }
ans[0] = finish(m);
ms[0] = duration<double, milli>(steady_clock::now() - t0).count(); }
/* ② cin + sync_with_stdio(false) */
{ reopen(); reset(n);
auto t0 = steady_clock::now();
ios::sync_with_stdio(false); cin.tie(nullptr);
int m, p; cin >> m >> p;
for (int i = 1; i <= m; i++) cin >> a[i];
for (int i = 0; i < p; i++) { int x, y, z; cin >> x >> y >> z; d[x] += z; d[y + 1] -= z; }
ans[1] = finish(m);
ms[1] = duration<double, milli>(steady_clock::now() - t0).count(); }
/* ③ scanf */
{ reopen(); reset(n);
auto t0 = steady_clock::now();
int m, p; if (scanf("%d %d", &m, &p) != 2) { m = p = 0; }
for (int i = 1; i <= m; i++) { if (scanf("%d", &a[i]) != 1) break; }
for (int i = 0; i < p; i++) {
int x, y, z;
if (scanf("%d %d %d", &x, &y, &z) != 3) break;
d[x] += z; d[y + 1] -= z;
}
ans[2] = finish(m);
ms[2] = duration<double, milli>(steady_clock::now() - t0).count(); }
/* ④ 手写快读 */
{ reopen(); reset(n); ipos = ilen = 0;
auto t0 = steady_clock::now();
int m = readIntFast(), p = readIntFast();
for (int i = 1; i <= m; i++) a[i] = readIntFast();
for (int i = 0; i < p; i++) {
int x = readIntFast(), y = readIntFast(), z = readIntFast();
d[x] += z; d[y + 1] -= z;
}
ans[3] = finish(m);
ms[3] = duration<double, milli>(steady_clock::now() - t0).count(); }
remove(path);
bool same = (ans[0] == ans[1] && ans[1] == ans[2] && ans[2] == ans[3]);
if (csv) {
const char* key[4] = { "cin", "nosync", "scanf", "fast" };
for (int k = 0; k < 4; k++) printf("%s,%.1f\n", key[k], ms[k]);
printf("same,%d\nanswer,%d\n", same ? 1 : 0, ans[0]);
return 0;
}
const char* name[4] = { "cin(默认,同步开着)", "cin + sync_with_stdio(false)",
"scanf", "手写快读(fread)" };
/* ⚠ 中文是双宽的,%-30s 按字节数补空格会补歪 —— 照第 45 章 read.cpp 那套按显示宽度补 */
auto disp = [](const string& t) {
int w = 0;
for (unsigned char c : t) { if ((c & 0xC0) == 0x80) continue; w += (c < 0x80) ? 1 : 2; }
return w;
};
auto padR = [&](const string& t, int w) { return t + string(max(0, w - disp(t)), ' '); };
double best = *min_element(ms, ms + 4);
printf("n = p = %d(和满数据比是 %.0f%%),四种读法跑同一个差分:\n\n", n, 100.0 * n / 5000000);
for (int k = 0; k < 4; k++)
printf(" %s %8.1f 毫秒 慢 %4.1f 倍\n", padR(name[k], 30).c_str(), ms[k], ms[k] / best);
printf("\n四趟的答案%s(都是 %d)\n", same ? "完全一致" : "居然不一致!", ans[0]);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

本机实测(A 机 · WSL2 · 12 线程 · 2026-09-05 重测 · 独占 · 三次取中位数; n = p = 5 × 10⁶ 满数据、随机区间。⚠ 这一栏量的是只读入,不含后面那三行差分):

读法 毫秒 比最快的慢 1 秒时限
cin(默认,同步开着) 3 087.3 8.6 倍 ✗
cin + sync_with_stdio(false) 925.9 2.6 倍 ⚠ 光读入就吃掉 93% 的时限
scanf 1 064.4 3.0 倍 ✗(⚠ 只超 6%)
手写快读(fread) ★ 358.7 1.0 倍 ★ ✓
⚠⚠ 这张表 2026-09-05 重测过 —— 而旧的那一版是在有负载的时候量的

原来这四行写的是 8 139.7 / 2 676.6 / 3 029.1 / 682.3,旁边标着「独占」。 独占重测,四行一起快了 2.6 ~ 2.9 倍。⇒ 追下去只用了一个对照: 把 12 个核全占满再量一次 —— 快读那一行当场变成 690.7 毫秒, 而页面上那个旧值是 682.3,几乎一模一样。

⇒ ★★★ 所以那张表不是「机器变了」,是「量的时候机器不空」 —— 和第 51 章那一轮(「倍增查询 701.5 ms」独占重测是 366)是同一件事的第二次。 ⚠ 而这一次的代价说得出具体数:四个判决里有两个的余量被吃掉了一个数量级 —— scanf 从「超 3 倍」变成「只超 6%」, 关同步从「快了三倍还是超」变成「光读入就吃掉 93% 的时限」。

⇒ ★★ 而倍数这一列几乎没动(最慢比最快:独占 8.6 倍、满载 7.5 倍, 而绝对秒数差 2.6 倍)—— 这就是硬规矩第 5 条那句「优先写倍数,秒数只当量级」的全部理由: 倍数在空和不空两种情况下都活着,绝对秒数不是。

★★★ 连 scanf 都不够 —— 这是「读入优化」真正成为分数线的那种题

第 45 章第 11 步量过这四种读法的差别,那里的结论是 「cin 默认最慢、快读最快」。这道题把那张表变成了及格线:

  • 大多数人的第一反应 cin(不关同步):3.1 秒,三倍超时。
  • 加一句 ios::sync_with_stdio(false):0.93 秒 —— 快了 3.3 倍, 可光读入就吃掉 93% 的时限,后面那三行差分再一加就压在线上了。
  • 换成 scanf:1.06 秒 —— ⚠ 只超 6%,可它还是超。
  • 手写 fread 快读:0.36 秒,过。

⇒ 判断要不要写快读,看的不是「题目难不难」,而是要读多少个数。 这道题两千万个 —— 而 scanf 一个数在这台机器上约 53 纳秒,光解析就 1.06 秒。 ★ 经验值:读入量到 10⁶ 就该关同步,到 10⁷ 就该上快读。

p2367Cin.cpp对照:算法一样,读入用默认 cin

5★ 对拍:600 轮,两档

p2367Gen.cpp生成器:两个档位
参数是「种子 档位」。档位 1 专挑 x = 1 / y = n / x = y / z = 0。
// 数据生成器(P2367 对拍用):`./p2367Gen <seed> [level]`
//
// level 0(默认)随机 n <= 12、p <= 8,成绩 0..100,z 0..100,区间随机
// level 1 专挑边界:x = 1、y = n、x = y,以及 z = 0
//
// ★ 为什么要有 level 1:差分唯一能写错的地方是那两个下标 ——
// `d[y+1] -= z` 在 y = n 时会写到 d[n+1](数组必须开到 n+1,不然就是越界),
// 而 `d[x] += z` 在 x = 1 时是数组的第一个位置。
// 顺手随机的话,「y 正好等于 n」的概率是 1/n。
#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) {
unsigned seed = (argc > 1) ? (unsigned)atoi(argv[1]) : 1;
int level = (argc > 2) ? atoi(argv[2]) : 0;
rng.seed(seed);
int n = ri(1, 12), p = ri(1, 8);
printf("%d %d\n", n, p);
for (int i = 1; i <= n; i++) printf("%d%c", ri(0, 100), i == n ? '\n' : ' ');
for (int i = 0; i < p; i++) {
int x, y;
if (level == 1) {
int kind = ri(0, 3);
if (kind == 0) { x = 1; y = n; } // 整段
else if (kind == 1) { x = ri(1, n); y = n; } // 贴着右端 —— 会写 d[n+1]
else if (kind == 2) { x = 1; y = ri(1, n); } // 贴着左端
else { x = y = ri(1, n); } // 只有一个人
} else { x = ri(1, n); y = ri(x, n); }
printf("%d %d %d\n", x, y, level == 1 && ri(0, 3) == 0 ? 0 : ri(0, 100));
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

check:viz 每档 300 轮,暴力当标准答案 —— 两档都是 300 / 300 逐字节相同。

★ 档位 1 盯的是 d[y+1]:y 正好等于 n 的时候,那一行会写到数组的最后一格之外。 顺手随机的话它的概率是 1/n,专门造就是每四次一次。

6三个版本并排

版本 算法 读入 满数据 分数
① p2367Brute 一个一个加 O(np) cin 关同步 约 2000 秒 ★ 40 分
② p2367Cin 差分 O(n+p) cin 默认 3.13 秒 ✗ 超时
③ p2367 差分 O(n+p) 快读 ★ 0.37 秒 ★ 100 分

★ 中间那一行是这一页的全部意思:算法对了,仍然可能因为读入而挂掉。

⚠ 这张表的秒数是两份真程序端到端跑满数据(p2367Cin / p2367,输入从文件喂进去); 上一步那张表是 p2367Read 在一个进程里连跑四趟量的(每趟 freopen 重读同一份数据)。 ★ 两组数几乎相等(3.13 ↔ 3.09、0.37 ↔ 0.36),而这件事本身有话说: 端到端减去只读入,剩下的就是「算法那一份」—— 这道题上它只有 0.01 ~ 0.04 秒。 ⇒ 也就是说这一页从头到尾在量的那个东西,九成以上是读入 (第 34 章 P3366 那条「量什么都要先问一句:这个数里有没有别人的份」 在这儿是反过来用的 —— 减完之后发现别人的份就是全部)。

7★★ 第 38 章回头看:这道题该不该上树状数组?

深入:树状数组版的秒表与形状(进阶)
这一节是 2026-08-31 补的

写第 38 章的题单时这道题又出现了一次 —— 那一章的角度是 「第 6 章用纯差分做过,而树状数组多出来的正是「可修改」三个字」。 这一页原来一个字都没提树状数组,所以补上这一节。 (★ 这是本书第三次「已经写过、但角度没覆盖」:前两次是 第 35 章的 P1776 和第 37 章的 P1177。)

p2367Bit.cpp★ 树状数组版:答案全对(600 轮逐字节相同)—— 而它会不会 TLE,取决于区间端点散不散:覆盖 [1,n] 是 0.23 秒(过),随机区间是 1.17 秒(挂)
★★ 能做,但一分钱不值 —— 而且它可能会挂,取决于形状

把差分数组交给树状数组,这道题照样做得出来(和正解逐字节相同,两档 600 轮验过)。 可代价是白付的:

区间加 最后问最低分
★ 差分 + 一次还原(第 ③ 版) O(1) × p O(n) 一遍扫完
树状数组 O(log n) × p ⚠ 每个位置都要问一次 O(n log n)

⚠⚠ 可「它会不会 TLE」这句话,2026-09-05 重测之后要加一个主语:形状。

顶格 n = p = 5 × 10⁶ 端到端 ★ 差分 树状数组 比
每次操作都覆盖 [1, n](生成器默认档,对暴力最坏的那一种) 0.10 秒 0.23 秒 2.3 倍 ⇒ ★ 两版都过得去
随机区间(生成器档位 1) 0.37 秒 ⚠ 1.17 秒 3.2 倍 ⇒ ⚠ TLE

本机实测(A 机 · WSL2 · Linux 6.18 / 12 线程 / 7 GB,2026-09-05 重测,独占; /usr/bin/time 各跑三次取中位数;两版都用同一份快读,时限 1 秒)。 ⚠ 旧的一版写的是「0.61 ~ 0.74 秒 vs 1.39 ~ 1.80 秒 ⇒ TLE」,没有点名形状 (而那两个数也是有负载时量的,见第 ⑤ 步那个对照)。

★★★ 而这两行的差别不是「做的活变多了」—— 换把尺子数一数,两档做的活几乎一样多 (p2367Shape.cpp,把读入那一份摘掉,只量树状数组自己那两层循环):

update 摸的节点 query 摸的节点 update 碰过的 64 字节缓存行 这两层的耗时
覆盖 [1, n] 115 000 000 54 717 320 ★ 21 195 ms
随机区间 112 291 523(少 2.4%) 54 717 320(★ 一个不差) 625 001 ⚠ 1606 ms

⇒ 摸的节点数差 2.4%、query 那一层一个不差,秒表却差 8.2 倍 —— 差的全在「摸的地方散不散」上: [1, n] 的区间加,永远从下标 1 往上爬 —— 1 → 2 → 4 → 8 → …, 五百万次操作全程只碰 21 个缓存行(1.3 KB,一直待在 L1 里); 换成随机端点就散在 625 001 行上(40 MB,比这台机器任何一级缓存都大), 差 29 762 倍。 ★ 而差分自己也被同一件事推着走(0.10 → 0.37 秒,3.7 倍),主语一模一样。

⚠⚠ 顺带一条:这里的 8.2 倍比上面端到端那张表的 5.1 倍还大 —— 因为端到端里九成是读入,那一份两档都要付,把差别稀释掉了。 ⇒ 又是第 34 章 P3366 那条「量什么都要先问一句:这个数里有没有别人的份」, ★ 而这一次不减掉它是低估,不是高估。

⇒ ★★★ 于是「顶格 ≠ 最坏」在这一页多了一层: 同一份数据,对暴力是最坏的(每次加满 n 个),对树状数组反而是最舒服的。 ⇒ 「⇒ TLE」这种判决必须连形状一起说,不点名就是错的。 ⇒ 而这也是两把尺子打架的又一次 (那次是「次数一样、秒表差 9.5 倍」,这次是「次数差 2.4%、秒表差 8.2 倍」)。

★ 闸门里钉的是哪几个数:check:viz 钉的是摸的节点数(115 000 000 / 112 291 523、 query 两档都是 54 717 320)和碰过的缓存行(21 / 625 001)—— 这两样精确、跨机器不变; 秒表只钉一条很松的比值带(≥ 3 倍,而独占实测是 8.2 倍)。 ⚠ 上面那些端到端的秒数只当量级看 —— 这一页刚刚吃过一次亏。

★★★ 而判据只有一句话,不用跑程序:

所有修改是不是都发生在所有询问之前? · 是 ⇒ 差分 + 最后统一还原一次就够,树状数组「能改」的本事一次都用不上(这道题); · 否(边改边问)⇒ 每问一次就得现算一遍前缀和 O(n) ⇒ 非树状数组不可 (隔壁那道 P3368 就是它,两道题的区间加写法一模一样)。

⇒ ★ 这也是第 34 章 P1546、第 35 章 P1776 那句话的第三次: 「这个技巧在这道题上成立」和「这道题该用它」是两句话。

这一页记住三句话
  1. ★ 先读数据范围再动手。 n ≤ 5×10⁶ 这个数同时决定了三件事: 要用差分、要开 2000 万个数的读入预算、以及数组开多大。
  2. ★★★ 「读入优化」不是玄学,是这道题的及格线。 只读入这一段:cin 3.09 秒 / 关同步 0.93 秒 / scanf 1.06 秒 / 快读 0.36 秒,时限 1 秒。 ⇒ 读入量 10⁶ 关同步,10⁷ 上快读。 ⚠ 而这四个数 2026-09-05 重测过 —— 旧的一版大了 2.6 倍, 因为它是在有负载的时候量的(第 ⑤ 步那个对照)。
  3. ⚠ d[y+1] 要开到 n+1。 差分唯一能写错的地方, 而它只在 y = n 时露头 —— 对拍要专门造这一档。