题单 · 习题解析

洛谷 P3372 【模板】线段树 1

★★ 题面那两行分档直接把暴力的分数写好了(35% 档 10⁷ 次、本机 1 毫秒 ⇒ **稳拿 35 分**);★★★ 而「要不要 long long」是最后那句保证直接给的 —— 线段树里**每个 sm[o] 都是某个子区间的和** ⇒ 全被 2×10¹⁸ 罩住(余量 4.6 倍);⚠ 可 int 版**顺手写的对拍是精确的 0**,我第一版的「中等值域」档最大答案才 2.8×10⁷、离 2³¹ 差 **76 倍** ⇒ 触发条件是一条数值线、生成器够不够是算术题;★ 顶到题面保证那一档触发 **299 ≡ 抓获 299**(正数 ⇒ 和只增不减,越过就回不来);★★★ 「数组开 4n」是算术题、对拍永远查不到(越界是 UB)—— 最先撑破 2n 的是 **n = 6**,最大比值 **3.977**(3n 不够),⚠⚠ 而 **n 是 2 的幂时 17 个全过**(树是满的)、连**官方样例 n = 5 也挡不住**(草稿写「样例能戳穿」被实测打回);★★ 这道题**两个树状数组也能做**(差分再推一步,顶格 10 ms vs 线段树 65 ms,⚠ 而步数只差 1.39 倍 —— 两把尺子又打架);★★★ 但它的中间量 j·d[j] **不是任何区间的和** ⇒ 冲到 **1.0×10²³(超 long long 10842 倍)**,而 60 轮答案**逐字节相同** —— [「它溢出了」和「它算错了」是两件事](/sol/p3374/)换一道题又成立一次(那 10842 倍是这个 0 的自检);⚠ 照抄章节 fast.cpp 会多打一行末行数组 ⇒ WA,和算法无关

原题:洛谷 P3372出自 第 39 章 线段树入门 的题单题面本地存档:2026-09-01
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

如题,已知一个数列 {aᵢ},你需要进行下面两种操作:

  1. 将某区间每一个数加上 k
  2. 求出某区间每一个数的和。

输入格式

第一行包含两个整数 nm,分别表示该数列数字的个数和操作的总个数。

第二行包含 n 个用空格分隔的整数 aᵢ,其中第 i 个数字表示数列第 i 项的初始值。

接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:

  1. 1 x y k:将区间 [x, y] 内每个数加上 k
  2. 2 x y:输出区间 [x, y] 内每个数的和。

输出格式

输出包含若干行整数,即为所有操作 2 的结果。

数据范围

对于 15% 的数据:n ≤ 8m ≤ 10

对于 35% 的数据:n ≤ 10³m ≤ 10⁴

对于 100% 的数据:1 ≤ n, m ≤ 10⁵aᵢk 为正数, ★ 且任意时刻数列的和不超过 2 × 10¹⁸

时限 1 秒,内存 512 MB。

输入输出样例

输入

5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

输出

11
8
20

★ 样例说明(转录自原站那张表,见下图):初始数列 1 5 4 2 3。 ① 2 2 45+4+2 = 11;② 1 2 3 2[2,3] 各加 2 ⇒ 1 7 6 2 3; ③ 2 3 46+2 = 8;④ 1 1 5 1 整段各加 1 ⇒ 2 8 7 3 4;⑤ 2 1 42+8+7+3 = 20

P3372 样例说明:五次操作、每一步之后的数列,以及三个输出

1第 ① 版:一个一个加、一个一个累 —— ★ 而它值 35 分

p3372Brute.cpp第 ① 版:改 O(区间长)、查 O(区间长) —— ★ 35 分,而且是后面所有版本的参照物
// P3372 第 ① 版:最直白的暴力 —— 改一段就一个一个加,查一段就一个一个累
//
// ★ 它不是白写的:题面自己把它的分数写好了
// 「对于 15% 的数据:n ≤ 8,m ≤ 10」「对于 35% 的数据:n ≤ 10³,m ≤ 10⁴」
// ⇒ 35% 那一档最坏 10³ × 10⁴ = 10⁷ 次,本机毫秒级 ⇒ **稳拿 35 分**。
// 而顶格 n = m = 10⁵ 是 10¹⁰ 次 ⇒ 一秒钟绝无可能。
//
// ⚠ 它同时是后面所有版本的**参照物**(对拍拿它当标准答案)。
//
// 复杂度:每次操作 O(区间长度),总共 O(nm)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<long long> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
string out;
for (int q = 0; q < m; q++) {
int op, l, r;
cin >> op >> l >> r;
if (op == 1) {
long long k;
cin >> k;
for (int i = l; i <= r; i++) a[i] += k; // 一个一个加
} else {
long long s = 0;
for (int i = l; i <= r; i++) s += a[i]; // 一个一个累
out += to_string(s);
out += '\n';
}
}
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 题面那两行分档,是出题人直接把暴力的分数写给你了
那一档 规模 这一版最坏多少次 值多少分
15% n ≤ 8m ≤ 10 80 ★ 15 分
35% n ≤ 10³m ≤ 10⁴ 10⁷ 35 分
100% n, m ≤ 10⁵ 10¹⁰ 0 分

本机实测(A 机 · WSL2 · Linux 6.18 / 8 线程 / 7 GB,2026-09-01,独占; p3372Count.cpp 在进程内用 steady_clock 量,不含读入): 35% 那一档(n = 10³m = 10⁴,随机区间)它只要 1 毫秒(碰了 330 万个格子)—— ⇒ 考场上这 35 分是白捡的,先写它再说。

★ 而它更重要的身份是参照物:下面每一版都拿它对拍。

2★ 正解:线段树 + 懒标记

这一章的正文(第 39 章)从头到尾讲的就是这道题, 所以这里不重讲懒标记,只把这道题特有的三件事说清楚。

p3372.cpp正解:线段树 + 懒标记,O((n + m) log n)
// P3372 正解 —— 线段树 + 懒标记(区间加 / 区间和)
//
// ★ 这就是[第 39 章](/ch/39-segment-tree/)正文那份 fast.cpp,**去掉了末行那三行 flush**:
// 本章那道题在真题基础上多要了一行「最终的整个数组」,而**真题不要**。
// ⇒ 照着章节代码交上去会多打一行,`Wrong Answer`。
//
// ★★ lz[o] 的含义(背得一字不差,否则代码写不对):
// **「o 这个节点自己的 sm 已经算进去了,但 o 的两个儿子还不知道这件事。」**
//
// ★★★ 为什么这道题**不用担心溢出**(一句话,不用跑程序):
// 题面写着「任意时刻数列的和不超过 2 × 10¹⁸」,
// 而线段树里的**每一个 sm[o] 都是某个子区间的和** ⇒ 每个都 ≤ 2 × 10¹⁸ < 9.22 × 10¹⁸。
// ⇒ long long 够用,余量 4.6 倍。(和[第 38 章 P3374](/sol/p3374/) 那句是同一个形状的论证。)
// ⚠ 但 lz 的累加和 x * len 也要一起看:len ≤ n,x 是单次加的值 ——
// x * len 本身就是「这一段这次涨了多少」,同样被那句保证罩着。
//
// 复杂度:建树 O(n),每次操作 O(log n)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
long long sm[MAXN * 4]; // ⚠ 4n:n 不是 2 的幂时 2n 是不够的(见 p3372Size.cpp)
long long lz[MAXN * 4];
long long a[MAXN];
int n, m;
inline void pushup(int o) { sm[o] = sm[o * 2] + sm[o * 2 + 1]; }
inline void applyAdd(int o, int len, long long x) {
sm[o] += x * len; // ⚠ 忘了乘 len 是最常见的错
lz[o] += x; // ★ 是 +=:欠了两笔账要叠起来
}
inline void pushdown(int o, int l, int r) {
if (!lz[o]) return;
int mid = (l + r) / 2;
applyAdd(o * 2, mid - l + 1, lz[o]);
applyAdd(o * 2 + 1, r - mid, lz[o]);
lz[o] = 0;
}
void build(int o, int l, int r) {
lz[o] = 0;
if (l == r) { sm[o] = a[l]; return; }
int mid = (l + r) / 2;
build(o * 2, l, mid);
build(o * 2 + 1, mid + 1, r);
pushup(o);
}
void update(int o, int l, int r, int ql, int qr, long long x) {
if (ql <= l && r <= qr) { applyAdd(o, r - l + 1, x); return; }
pushdown(o, l, r);
int mid = (l + r) / 2;
if (ql <= mid) update(o * 2, l, mid, ql, qr, x);
if (qr > mid) update(o * 2 + 1, mid + 1, r, ql, qr, x);
pushup(o);
}
long long query(int o, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) return sm[o];
pushdown(o, l, r);
int mid = (l + r) / 2;
long long s = 0;
if (ql <= mid) s += query(o * 2, l, mid, ql, qr);
if (qr > mid) s += query(o * 2 + 1, mid + 1, r, ql, qr);
return s;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) cin >> a[i];
build(1, 1, n);
string out;
for (int q = 0; q < m; q++) {
int op, l, r;
cin >> op >> l >> r;
if (op == 1) {
long long x;
cin >> x;
update(1, 1, n, l, r, x);
} else {
out += to_string(query(1, 1, n, l, r));
out += '\n';
}
}
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 第一件:照抄章节里那份 fast.cpp 会 WA,而且和算法无关

第 39 章那道题是在真题基础上多要了一行「最终的整个数组」, 所以 fast.cpp 末尾有三行 flush真题不要那一行。

⇒ 直接把章节代码交上去,输出会多出一行 n 个数,判成 Wrong Answer —— 而你会以为是懒标记写错了。★ 这一版就是把那三行删掉。

3★★ 第二件:「要不要 long long」是题面那句话直接给的

p3372Int.cpp✗ 整棵树用 int —— 顺手写的数据上一次都不会错

题面最后那句「任意时刻数列的和不超过 2 × 10¹⁸」把两件事一次说完了:

  • long long 够用:线段树里每个 sm[o] 都是某个子区间的和 ⇒ 都 ≤ 2 × 10¹⁸ < 9.22 × 10¹⁸, 余量 4.6 倍。★ 这和第 38 章 P3374 那句是同一个形状的论证 (「答案 ≥ 任何一个被用到的中间值」),本书第六次用到它。
  • int 差得离谱2 × 10¹⁸int 上限的 9.3 亿倍
⚠⚠ 可它偏偏不是「一交就死」—— 顺手写的对拍是精确的 0

p3372Gen.cpp 的档 0 就是绝大多数人会顺手写的样子(nm 十来个,值 1~100)。 300 轮对拍下来:

档位 值域 被问到的答案越过 2³¹ 的轮数 int真被抓
0 顺手写的 a, k ≤ 100 0 精确的 0
1 「中等值域」 a, k ≤ 10⁶ 0 仍然是 0
2 顶到题面保证的边上 2×10¹⁸/(n(1+m)) 反推 299 299 —— 一个不差
3 n 取 2 的幂 同上 300 300

★★★ 第 ② 行是我第一版写的「中等值域」档,实测颗粒无收 —— n = m = 12 时最大的那个答案才 28 086 744,离 2³¹76 倍。 ⇒ 溢出的触发条件是一条数值线,而生成器够不够是一道算术题第 6 章 P3406 那条一模一样:那次的顶格档只顶了票价,差七倍够不着 2³²)。

★ 而档 2 的「触发 299 ≡ 抓获 299」是一个不差的等式 —— 因为这道题的 ak 全是正数 ⇒ 和只增不减,越过 2³¹ 就再也回不来。

4★★★ 第三件:数组开 4n —— 这是一道算术题,对拍永远查不到

p3372Size.cpp★ 不制造越界(那是 UB),只数「最大用到第几号节点」
// 「线段树的数组到底要开多大」—— 这是一道**算术题**,不用跑对拍
//
// 用法:./p3372Size 人话版
// ./p3372Size csv 给 check:viz 用
//
// ★ 章节正文里那句「⚠ 数组要开 4n,2n 是不够的」是本章最常见的 RE 来源。
// 这份程序不制造越界(那是 UB,跑出来的数不可复现),它只**数**:
// 对每个 n,递归线段树真正用到的**最大节点下标**是多少。
//
// ★★★ 而这里有一件顺手写的测试**必然看不见**的事:
// **n 是 2 的幂的时候,树是满的,最大下标正好 2n − 1 ⇒ 「只开 2n」完全够用。**
// ⇒ 拿 n = 8 / 16 / 64 / 1024 试一遍,「开 2n」这个 bug 是**精确的 0**(实测 17 个全过)。
// ⇒ 又一次「[顺手写的档位正好把 bug 喂对了](/sol/p1972/)」。
//
// ⚠⚠ 而这里还有一条**我的草稿被实测打回来**的:我本来写「官方样例 n = 5 就能戳穿它」——
// **不对**。n = 5 的最大下标是 **9**,而 2n = 10 ⇒ **样例也挡不住**。
// 最小的反例是 **n = 6**(最大下标 13 > 12)。
// ⇒ 「[样例是一测就死的过滤器](/sol/p1223/)」这条规律在这儿又拿到一个反例,
// 而这次连**下一个整数**就够了 —— 差一个 n。
#include <bits/stdc++.h>
using namespace std;
static int mx;
static void walk(int o, int l, int r) {
mx = max(mx, o);
if (l == r) return;
int mid = (l + r) / 2;
walk(o * 2, l, mid);
walk(o * 2 + 1, mid + 1, r);
}
static int maxIndex(int n) { mx = 0; walk(1, 1, n); return mx; }
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
const int LIM = 100000; // 题面顶格
int firstOver2n = -1, firstOver3n = -1, worstN = 0;
double worstRatio = 0;
int pow2Bad = 0, pow2Cnt = 0;
for (int n = 1; n <= LIM; n++) {
int mi = maxIndex(n);
if (firstOver2n < 0 && mi > 2 * n) firstOver2n = n;
if (firstOver3n < 0 && mi > 3 * n) firstOver3n = n;
double ratio = (double)mi / n;
if (ratio > worstRatio) { worstRatio = ratio; worstN = n; }
if ((n & (n - 1)) == 0) { pow2Cnt++; if (mi > 2 * n) pow2Bad++; }
}
int s5 = maxIndex(5), s8 = maxIndex(8);
if (csv) {
printf("firstOver2n,%d\n", firstOver2n);
printf("firstOver3n,%d\n", firstOver3n);
printf("worstN,%d\nworstRatio,%.3f\n", worstN, worstRatio);
printf("over4n,%d\n", (worstRatio > 4.0) ? 1 : 0);
printf("pow2Bad,%d\npow2Cnt,%d\n", pow2Bad, pow2Cnt);
printf("sample5,%d\nsample5over,%d\n", s5, (s5 > 10) ? 1 : 0);
printf("pow8,%d\npow8over,%d\n", s8, (s8 > 16) ? 1 : 0);
return 0;
}
printf("n 从 1 数到 %d,看递归线段树真正用到的最大节点下标:\n\n", LIM);
printf(" 最先撑破 2n 的 n = %d(它的最大下标 %d > %d)\n",
firstOver2n, maxIndex(firstOver2n), 2 * firstOver2n);
printf(" 最先撑破 3n 的 n = %d\n", firstOver3n);
printf(" 最大下标 / n 的最大值 = %.3f(在 n = %d 取到)⇒ 开 4n 够,开 3n 不够\n",
worstRatio, worstN);
printf(" ★ n 是 2 的幂的那 %d 个 : 撑破 2n 的有 %d 个(树是满的,下标恰好 2n−1)\n",
pow2Cnt, pow2Bad);
printf("\n 官方样例 n = 5:最大下标 %d,而 2n = 10 ⇒ %s\n",
s5, s5 > 10 ? "★ 只开 2n 当场越界" : "开 2n 够用");
printf(" 顺手测的 n = 8:最大下标 %d,而 2n = 16 ⇒ %s\n",
s8, s8 > 16 ? "越界" : "★ 开 2n 够用 —— 所以拿 2 的幂去测,这个 bug 一次都不现形");
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★ 把 n 从 1 数到 10⁵,看递归线段树真正用到的最大节点下标
问的是 实测
最先撑破 2nn 6(最大下标 13 > 12)
最先撑破 3nn 36
最大下标 / n 的最大值 3.977(在 n = 65792 取到)⇒ 开 4n 够,开 3n 不够
n 是 2 的幂的那 17 个 撑破 2n 的有 ★ 0 个(树是满的,下标恰好 2n − 1

★★★ 最后一行是这一节的重点:n 取 8 / 16 / 64 / 1024 去测,「只开 2n」这个 bug 一次都不会现形 —— 而 2 的幂正是所有人手测时最爱用的数。 ⇒ 又一次「顺手写的档位正好把 bug 喂对了」。

⚠⚠ 而这里我的草稿被实测打回来了:我本来写「官方样例 n = 5 就能戳穿它」。 不对 —— n = 5 的最大下标是 9,而 2n = 10样例也挡不住。 最小的反例是 n = 6。 ⇒ 「样例是一测就死的过滤器」这条规律又拿到一个反例, 而这次只差一个整数

★ 顺带说清楚这类 bug 的性质:它是 RE(数组越界),不是 WA —— 对拍原理上就查不到(越界是 UB,跑出来的数不可复现)。 ⇒ 只能像上面这样n 有多大、最坏用到第几号节点、数组开了多少。

5★★ 换一条路:两个树状数组也能做这道题 —— 而且更短更快

★ 为什么值得写这一节

第 39 章第 13 步把「线段树 vs 树状数组」摆在一起, 结论是「树状数组代码短、常数小,线段树能干的事多」。 这一节把那笔账反过来算一遍:连这一章的模板题,树状数组也做得下来。

差分往前多推一步就出来了(第 6 章那套):

d[i] = a[i] − a[i−1],则区间加 [l,r] += k 就是 d[l] += kd[r+1] −= k(两个单点改)。而

Σ(i=1..x) a[i] = Σ(j=1..x) (x − j + 1) · d[j]
               = (x+1) · Σ(j=1..x) d[j]  −  Σ(j=1..x) j · d[j]

⇒ 维护两个树状数组就够了:B1d[j]B2j · d[j]

p3372Bit.cpp★ 第 ③ 版:两个树状数组 —— 区间加 4 次单点改,区间和 4 次前缀查
// P3372 第 ③ 版:**两个树状数组**也能做「区间加 + 区间和」
//
// ★ 为什么要写它:[第 39 章第 13 步](/ch/39-segment-tree/)把「线段树 vs 树状数组」摆在一起,
// 结论是「树状数组代码短、常数小,线段树能干的事多」。**这一版把那笔账反过来算了一遍** ——
// 连这一章的模板题,树状数组也做得下来,而且更短更快。
//
// ============ 推导([第 6 章](/ch/06-prefix-diff/)那套差分,往前多推一步)============
//
// 记差分 d[i] = a[i] − a[i−1]。区间加 [l,r] += k 就是 d[l] += k、d[r+1] −= k(两个单点改)。
// 而 a 的前缀和:
//
// Σ_{i=1..x} a[i] = Σ_{i=1..x} Σ_{j=1..i} d[j]
// = Σ_{j=1..x} (x − j + 1) · d[j]
// = (x+1) · Σ_{j=1..x} d[j] − Σ_{j=1..x} j · d[j]
//
// ⇒ 维护**两个**树状数组就够了:B1 存 d[j],B2 存 j·d[j]。
// ⇒ 区间加是 4 次单点改,区间和是 4 次前缀查,全都 O(log n)。
//
// ============ ⚠⚠ 但它有一处线段树没有的隐患:中间值 j · d[j] ============
//
// 题面只保证「**数列的和**不超过 2 × 10¹⁸」。线段树里每个 sm[o] 都是某个子区间的和
// ⇒ 全被那句话罩住了。**而 j · d[j] 不是任何一个区间的和** ——
// 合法输入里它能到 10⁵ × 2×10¹⁸ = **2 × 10²³**,是 long long 上限的两万多倍。
// ★ 那它到底会不会算错?见 p3372Count.cpp 的 `wrap` 那一行 —— 结论和
// [第 38 章 P3374](/sol/p3374/) 那条一模一样:**「它溢出了」和「它算错了」是两件事。**
// ⚠ 但有符号溢出是 UB,考场上别赌 —— 这一版存在的意义是「短、快」,不是「安全」。
//
// 复杂度:每次操作 O(log n),常数比线段树小得多(没有递归、没有下推)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
long long c1[MAXN], c2[MAXN]; // c1: d[j];c2: j·d[j]
int n, m;
inline void add(long long* c, int i, long long v) {
for (; i <= n; i += i & -i) c[i] += v;
}
inline long long ask(long long* c, int i) {
long long s = 0;
for (; i > 0; i -= i & -i) s += c[i];
return s;
}
/** 区间 [l,r] 每个数 += k —— 差分之后是两个单点改,两个数组各改两处 */
inline void rangeAdd(int l, int r, long long k) {
add(c1, l, k); add(c2, l, (long long)l * k);
if (r + 1 <= n) { add(c1, r + 1, -k); add(c2, r + 1, -(long long)(r + 1) * k); }
}
/** a 的前缀和 Σ_{i=1..x} a[i] = (x+1)·Σd − Σ j·d[j] */
inline long long prefix(int x) {
return (long long)(x + 1) * ask(c1, x) - ask(c2, x);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
for (int i = 1; i <= n; i++) {
long long x;
cin >> x;
rangeAdd(i, i, x); // 建树就是「把每个 a[i] 当成一次长度为 1 的区间加」
}
string out;
for (int q = 0; q < m; q++) {
int op, l, r;
cin >> op >> l >> r;
if (op == 1) {
long long k;
cin >> k;
rangeAdd(l, r, k);
} else {
out += to_string(prefix(r) - prefix(l - 1));
out += '\n';
}
}
cout << out;
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

6⚠⚠ 但它有一处线段树没有的隐患,而结局出人意料

★★★ 题面那句保证罩得住线段树,罩不住树状数组

再读一遍那句话:「任意时刻数列的和不超过 2 × 10¹⁸」。它保证的是区间和

  • 线段树里每个 sm[o] 都是某个子区间的和 ⇒ 全被罩住;
  • ⚠ 而树状数组要算的 j · d[j] 不是任何一个区间的和 —— 它没有被保证。

p3372Gen.cpp 档 4 就是照这个造的:n = 10⁵,末尾放一个吃掉一半额度的巨值。 p3372Count.cpp__int128 把真值称出来:

量的是 实测
j · d[j] 的最大真值 1.0 × 10²³
long long 上限 9.22 × 10¹⁸
超出 ★★ 10 842 倍
而这一档 60 轮里,树状数组版和线段树 / 暴力 ★★★ 逐字节相同,0 轮不一致

★★★ 这正是第 38 章 P3374 那条结论换一道题又成立一次「它溢出了」和「它算错了」是两件事。 (x+1)·Σd − Σ j·d[j] 这个恒等式在模 2⁶⁴ 意义下照样成立, 而真实答案 ≤ 2 × 10¹⁸ < 2⁶³绕出去的那部分又绕了回来。

⚠ 而那 10842 倍是自检,不是花絮:报「60 轮全对」之前, 得先证明这段代码真的溢出了 —— 否则「没错」可能只是「根本没发生」 (报 0 之前先拿已知错的东西验一遍)。

但结论不是「可以放心溢出」:有符号溢出是 UB, 换个编译器 / 开 -ftrapv 就不是这个结果。 ⇒ 这一版存在的理由是短和快,不是安全。

7★★ 三种做法并排:而两把尺子又打架了

p3372Count.cpp本页所有数字的出处(含自检:每一档都守住了题面那句 2×10¹⁸)
// P3372 的度量程序:./p3372Count csv (本页的数字都出自它)
//
// cap : ★ 自检 —— 每一档里「任意时刻的总和」的最大值,对照题面那句 2×10¹⁸。
// **生成器必须亲手守住它**,否则测的是一组题目不会给的输入。
// line : ★★ int 版的触发线是**算出来的**:档 1(值域 10⁶、n=m≈12)最大和只有 1.56×10⁸,
// 离 2³¹ 差 13.8 倍 ⇒ 那一档必然是**精确的 0**([第 6 章 P3406](/sol/p3406/) 那条的复现)。
// trig : ★★★ 档 2 上两层数:① 有查询的答案越过 2³¹ 的轮数 ② int 版真被抓的轮数。
// bitMax : ★★★ 档 4(巨值)上,双树状数组的中间量 j·d[j] 的**真值**(用 __int128 算),
// 对照 long long 上限 —— 这是「BIT 版 60 轮全对」那个 0 的**自检**:
// 先证明它真的溢出了,那句「溢出 ≠ 答错」才有意义。
// ms : 35% 那一档(n=10³、m=10⁴)和顶格(n=m=10⁵)上三种做法的毫秒。
// ops : 换一把机器无关的尺子 —— 三种做法各「碰了多少个格子 / 走了多少步」。
//
// ⚠ 秒表一律用 steady_clock 在进程内量([第 35 章 P2866](/sol/p2866/) 那一跤:
// 拿 shell 里的时间戳相减,带着约 100 毫秒的固定开销)。
// ⚠ 这里**复刻**了 p3372Gen.cpp 的档位逻辑(同一个 mt19937_64、同一个种子公式)。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
struct Op { int type, l, r; long long k; };
struct Data { int n, m; vector<long long> a; vector<Op> ops; };
static const long long CAP = 2000000000000000000LL;
/** ⚠ 和 p3372Gen.cpp 一字对应 */
static Data gen(unsigned seed, int mode) {
mt19937_64 rng(seed * 1000003ull + 20260901ull);
Data d;
int n, m;
if (mode == 3) { int e = 3 + (int)(rng() % 4u); n = 1 << e; m = 8 + (int)(rng() % 8u); }
else if (mode == 4) { n = 100000; m = 20; }
else if (mode == 5) { n = 1000; m = 10000; }
else if (mode == 6 || mode == 7) { n = 100000; m = 100000; }
else { n = 6 + (int)(rng() % 7u); m = 6 + (int)(rng() % 7u); }
long long hi;
if (mode == 0) hi = 100;
else if (mode == 1) hi = 1000000;
else if (mode == 4) hi = 1;
else hi = CAP / ((long long)n * (m + 1));
if (hi < 1) hi = 1;
d.n = n; d.m = m; d.a.assign(n + 1, 0);
for (int i = 1; i <= n; i++) {
long long v;
if (mode == 4) v = (i == n) ? (CAP / 2) : 1;
else v = 1 + (long long)(rng() % (unsigned long long)hi);
d.a[i] = v;
}
for (int q = 0; q < m; q++) {
int l = 1 + (int)(rng() % (unsigned)n);
int r = 1 + (int)(rng() % (unsigned)n);
if (l > r) swap(l, r);
if (mode == 7) { l = 1; r = n; }
int op;
if (mode == 7) op = (q % 8 == 7) ? 2 : 1;
else if (mode == 4) op = (q % 3 == 0) ? 1 : 2;
else op = 1 + (int)(rng() % 2u);
long long k = 0;
if (op == 1) {
if (mode == 4) k = 1 + (long long)(rng() % 1000ull);
else k = 1 + (long long)(rng() % (unsigned long long)hi);
}
d.ops.push_back({op, l, r, k});
}
return d;
}
/** 直接模拟:返回「任意时刻总和的最大值」和「被问到的答案的最大值」 */
static void simulate(const Data& d, __int128& maxTotal, __int128& maxAns) {
vector<__int128> a(d.n + 1, 0);
__int128 total = 0;
for (int i = 1; i <= d.n; i++) { a[i] = d.a[i]; total += a[i]; }
maxTotal = total; maxAns = 0;
for (const Op& o : d.ops) {
if (o.type == 1) {
for (int i = o.l; i <= o.r; i++) a[i] += o.k;
total += (__int128)o.k * (o.r - o.l + 1);
if (total > maxTotal) maxTotal = total;
} else {
__int128 s = 0;
for (int i = o.l; i <= o.r; i++) s += a[i];
if (s > maxAns) maxAns = s;
}
}
}
/** 档 4 上,双树状数组的中间量 j·d[j] 能到多大(真值,__int128) */
static __int128 bitIntermediate(const Data& d) {
vector<__int128> diff(d.n + 2, 0);
for (int i = 1; i <= d.n; i++) diff[i] += d.a[i], diff[i + 1] -= d.a[i];
for (const Op& o : d.ops)
if (o.type == 1) { diff[o.l] += o.k; if (o.r + 1 <= d.n) diff[o.r + 1] -= o.k; }
__int128 best = 0;
for (int j = 1; j <= d.n; j++) {
__int128 v = (__int128)j * diff[j];
if (v < 0) v = -v;
if (v > best) best = v;
}
return best;
}
/* ---------------- 三种做法,都带一把「走了多少步」的尺子 ---------------- */
static long long steps;
static vector<long long> bruteRun(const Data& d) {
vector<long long> a(d.n + 1), out;
for (int i = 1; i <= d.n; i++) a[i] = d.a[i];
for (const Op& o : d.ops) {
if (o.type == 1) { for (int i = o.l; i <= o.r; i++) { a[i] += o.k; steps++; } }
else { long long s = 0; for (int i = o.l; i <= o.r; i++) { s += a[i]; steps++; } out.push_back(s); }
}
return out;
}
static int SN;
static vector<long long> sm, lz;
static void sBuild(int o, int l, int r, const vector<long long>& a) {
steps++; lz[o] = 0;
if (l == r) { sm[o] = a[l]; return; }
int mid = (l + r) / 2;
sBuild(o * 2, l, mid, a); sBuild(o * 2 + 1, mid + 1, r, a);
sm[o] = sm[o * 2] + sm[o * 2 + 1];
}
static void sApply(int o, int len, long long x) { sm[o] += x * len; lz[o] += x; steps++; }
static void sDown(int o, int l, int r) {
if (!lz[o]) return;
int mid = (l + r) / 2;
sApply(o * 2, mid - l + 1, lz[o]); sApply(o * 2 + 1, r - mid, lz[o]); lz[o] = 0;
}
static void sUpd(int o, int l, int r, int ql, int qr, long long x) {
steps++;
if (ql <= l && r <= qr) { sApply(o, r - l + 1, x); return; }
sDown(o, l, r);
int mid = (l + r) / 2;
if (ql <= mid) sUpd(o * 2, l, mid, ql, qr, x);
if (qr > mid) sUpd(o * 2 + 1, mid + 1, r, ql, qr, x);
sm[o] = sm[o * 2] + sm[o * 2 + 1];
}
static long long sQry(int o, int l, int r, int ql, int qr) {
steps++;
if (ql <= l && r <= qr) return sm[o];
sDown(o, l, r);
int mid = (l + r) / 2;
long long s = 0;
if (ql <= mid) s += sQry(o * 2, l, mid, ql, qr);
if (qr > mid) s += sQry(o * 2 + 1, mid + 1, r, ql, qr);
return s;
}
static vector<long long> segRun(const Data& d) {
SN = d.n; sm.assign((size_t)d.n * 4 + 4, 0); lz.assign((size_t)d.n * 4 + 4, 0);
sBuild(1, 1, d.n, d.a);
vector<long long> out;
for (const Op& o : d.ops) {
if (o.type == 1) sUpd(1, 1, d.n, o.l, o.r, o.k);
else out.push_back(sQry(1, 1, d.n, o.l, o.r));
}
return out;
}
static vector<long long> c1v, c2v;
static void bAdd(vector<long long>& c, int i, long long v) { for (; i <= SN; i += i & -i) { c[i] += v; steps++; } }
static long long bAsk(vector<long long>& c, int i) { long long s = 0; for (; i > 0; i -= i & -i) { s += c[i]; steps++; } return s; }
static void bRange(int l, int r, long long k) {
bAdd(c1v, l, k); bAdd(c2v, l, (long long)l * k);
if (r + 1 <= SN) { bAdd(c1v, r + 1, -k); bAdd(c2v, r + 1, -(long long)(r + 1) * k); }
}
static long long bPre(int x) { return (long long)(x + 1) * bAsk(c1v, x) - bAsk(c2v, x); }
static vector<long long> bitRun(const Data& d) {
SN = d.n; c1v.assign(d.n + 2, 0); c2v.assign(d.n + 2, 0);
for (int i = 1; i <= d.n; i++) bRange(i, i, d.a[i]);
vector<long long> out;
for (const Op& o : d.ops) {
if (o.type == 1) bRange(o.l, o.r, o.k);
else out.push_back(bPre(o.r) - bPre(o.l - 1));
}
return out;
}
template <class F> static double timeIt(F f, long long& st) { steps = 0; auto t0 = steady_clock::now();
f(); st = steps; return duration<double, milli>(steady_clock::now() - t0).count(); }
static string i128(__int128 v) {
if (v == 0) return "0";
string s; bool neg = v < 0; if (neg) v = -v;
while (v) { s += char('0' + (int)(v % 10)); v /= 10; }
if (neg) s += '-';
reverse(s.begin(), s.end());
return s;
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
/* ① 自检:每一档都守住了题面那句「任意时刻的和 ≤ 2×10¹⁸」吗 */
int capBad = 0;
__int128 capWorst = 0;
for (int mode : {0, 1, 2, 3, 5, 6, 7}) {
int rounds = (mode >= 5) ? 3 : 60;
for (int s = 1; s <= rounds; s++) {
Data d = gen(s, mode);
__int128 mt, ma; simulate(d, mt, ma);
if (mt > capWorst) capWorst = mt;
if (mt > CAP) capBad++;
}
}
/* ② int 版的触发线:档 1 够不着,档 2 够得到 */
const long long INT_MAX_LL = 2147483647LL;
int trig1 = 0, trig2 = 0;
__int128 max1 = 0;
for (int s = 1; s <= 300; s++) {
Data d1 = gen(s, 1), d2 = gen(s, 2);
__int128 mt, ma;
simulate(d1, mt, ma); if (ma > max1) max1 = ma; if (ma > INT_MAX_LL) trig1++;
simulate(d2, mt, ma); if (ma > INT_MAX_LL) trig2++;
}
/* ③ 档 4:双树状数组的中间量到底多大 */
__int128 bitWorst = 0;
for (int s = 1; s <= 20; s++) {
Data d = gen(s, 4);
__int128 v = bitIntermediate(d);
if (v > bitWorst) bitWorst = v;
}
double bitTimes = (double)bitWorst / 9.223372036854775807e18;
/* ④ 秒表 + 步数 */
long long stB = 0, stS = 0, stT = 0;
Data d35 = gen(1, 5); // 35% 那一档
double msB35 = timeIt([&] { bruteRun(d35); }, stB);
double msS35 = timeIt([&] { segRun(d35); }, stS);
double msT35 = timeIt([&] { bitRun(d35); }, stT);
long long stB35 = stB, stS35 = stS, stT35 = stT;
Data dTop = gen(1, 6); // 顶格
double msSTop = timeIt([&] { segRun(dTop); }, stS);
double msTTop = timeIt([&] { bitRun(dTop); }, stT);
long long stSTop = stS, stTTop = stT;
Data dAll = gen(1, 7); // 顶格且全整段(懒标记最占便宜)
double msSAll = timeIt([&] { segRun(dAll); }, stS);
long long stSAll = stS;
bool same35 = (bruteRun(d35) == segRun(d35)) && (segRun(d35) == bitRun(d35));
if (csv) {
printf("capBad,%d\ncapWorstOver2e18,%d\n", capBad, (capWorst > CAP) ? 1 : 0);
printf("capWorstPct,%d\n", (int)((long double)capWorst * 100 / CAP));
printf("trig1,%d\ntrig2,%d\n", trig1, trig2);
printf("max1,%s\n", i128(max1).c_str());
printf("max1Gap,%d\n", (int)((long double)INT_MAX_LL / (long double)max1));
printf("bitWorst,%s\nbitTimesLL,%d\n", i128(bitWorst).c_str(), (int)bitTimes);
printf("ms35b,%.0f\nms35s,%.0f\nms35t,%.0f\n", msB35, msS35, msT35);
printf("st35b,%lld\nst35s,%lld\nst35t,%lld\n", stB35, stS35, stT35);
printf("msTops,%.0f\nmsTopt,%.0f\n", msSTop, msTTop);
printf("stTops,%lld\nstTopt,%lld\n", stSTop, stTTop);
printf("msAll,%.0f\nstAll,%lld\n", msSAll, stSAll);
printf("same35,%d\n", same35 ? 1 : 0);
return 0;
}
printf("① 自检:七个档共 %d 组,越过题面那句 2×10¹⁸ 的有 %d 组(最大用到额度的 %d%%)\n\n",
7, capBad, (int)((long double)capWorst * 100 / CAP));
printf("② int 版的触发线(被问到的答案要越过 2³¹−1 = %lld):\n", INT_MAX_LL);
printf(" 档 1(值域 10⁶):300 轮里触发 %d 次 —— 最大答案才 %s,差 %d 倍,**够不着**\n",
trig1, i128(max1).c_str(), (int)((long double)INT_MAX_LL / (long double)max1));
printf(" 档 2(顶到题面保证):300 轮里触发 %d 次\n\n", trig2);
printf("③ 档 4:双树状数组的中间量 j·d[j] 最大 = %s\n", i128(bitWorst).c_str());
printf(" 而 long long 上限是 9223372036854775807 ⇒ **超出 %d 倍**(它真的溢出了)\n\n",
(int)bitTimes);
printf("④ 秒表 / 步数:\n");
printf(" 35%% 那一档 n=10³ m=10⁴ : 暴力 %.0f ms(%lld 步)|线段树 %.0f ms(%lld 步)|树状数组 %.0f ms(%lld 步)\n",
msB35, stB35, msS35, stS35, msT35, stT35);
printf(" 顶格 n=m=10⁵ : 线段树 %.0f ms(%lld 步)|树状数组 %.0f ms(%lld 步)\n",
msSTop, stSTop, msTTop, stTTop);
printf(" 顶格且全是整段修改 : 线段树 %.0f ms(%lld 步)\n", msSAll, stSAll);
printf(" 三种做法答案一致:%s\n", same35 ? "是" : "★ 不一致!");
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★ 秒表差 6.5 倍,而「走了多少步」只差 1.39 倍

本机实测(A 机 · WSL2 · 2026-09-01 · 独占 · 进程内 steady_clock,不含读入):

数据 暴力 线段树 树状数组
35% 档 n=10³ m=10⁴ 1 ms(330 万步) 2 ms(44.5 万步) ★ 0 ms(22.0 万步)
顶格 n=m=10⁵ n·m 外推约 10 秒 65 ms(959 万步) 10 ms(690 万步)
顶格且全是整段修改 —— 2 ms(38.7 万步) ——

★★ 第二行就是第 29 章 P2853 那条的又一次现场: 步数只差 1.39 倍,秒表差 6.5 倍。 差在哪儿说得清 —— 树状数组是一个 for 循环i += i & -i,连续内存、没有函数调用), 线段树是递归 + 每层一次 pushdown。 ⇒ 次数和秒表量的从来不是同一件事,两个都要报。

★ 第三行是懒标记最占便宜的形状:同样顶格,全整段修改只要 2 毫秒, 比混合档快 32 倍 —— 因为每次修改在根上就被「全覆盖」那一支拦下了, 一次只碰 O(1) 个节点。

8★ 那么,哪一版就已经能过了

这道题的答案很干脆:第 ② 版(线段树)和第 ③ 版(树状数组)都能过
版本 顶格耗时 交上去
① 暴力 约 10 秒 35 分
★ ② 线段树 + 懒标记 65 ms(时限 1 秒) AC,余量 15 倍
★ ③ 两个树状数组 10 ms AC,余量 100 倍

该写哪一个? 这道题上写树状数组更短更快; 但下一道P3373:区间乘 + 区间加)树状数组就做不下来了 —— 差分那一步依赖「区间加是两个单点改」,而乘法没有这个性质。 ★ 这就是第 13 步那句话的实际含义:不是线段树更快,是它能干的事多。

⚠ 最后一条和算法无关的:输入每行是 3 4 个整数(操作 2 只有三个数)—— 和第 38 章 P3368 一模一样的坑,照着「每行读四个」写必串位。