0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P3368,日期见页头。两边不一致时信原站。
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数加上
x; - 求出某一个数的值。
输入格式
第一行包含两个整数 N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含 N 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
⚠ 接下来 M 行每行包含 2 或 4 个整数,表示一个操作,具体如下:
- 操作 1:格式:
1 x y k含义:将区间[x, y]内每个数加上k; - 操作 2:格式:
2 x含义:输出第x个数的值。
输出格式
输出包含若干行整数,即为所有操作 2 的结果。
数据规模与约定
对于 30% 的数据:N ≤ 8,M ≤ 10;
对于 70% 的数据:N ≤ 10⁴,M ≤ 10⁴;
对于 100% 的数据:1 ≤ N, M ≤ 5 × 10⁵,1 ≤ x, y ≤ n,
★ 保证任意时刻序列中任意元素的绝对值都不大于 2³⁰。
时限 1 秒,内存 125 MB。
输入输出样例
输入
5 5 1 5 4 2 3 1 2 4 2 2 3 1 1 5 -1 1 3 5 7 2 4
输出
6 10
★ 样例 1 解释(原站给的是下面这张图,见图):初始 1 5 4 2 3;
1 2 4 2 把 [2,4] 各加 2 ⇒ 1 7 6 4 3,于是 2 3 是 6;
1 1 5 -1 ⇒ 0 6 5 3 2,1 3 5 7 ⇒ 0 6 12 10 9,于是 2 4 是 10。

1第一反应:区间加就一个一个加
// P3368 第 ① 版:**题目怎么说就怎么做** —— 区间加就一个一个加,单点查就直接看//// ★ 它值多少分,题面自己写着:// · 「对于 30% 的数据:N ≤ 8,M ≤ 10」 ⇒ 80 次加法,★ 稳拿 30 分;// · 「对于 70% 的数据:N, M ≤ 10⁴」 ⇒ 最坏 10⁸ 次,本机约 0.03 秒 ⇒ ★ 也过得去;// · 顶格 N = M = 5×10⁵ ⇒ **2.5×10¹¹ 次**,时限 1 秒。//// ⚠ 注意它和上一道题的暴力**慢在相反的地方**:// [P3374](/sol/p3374/) 的暴力是「改得快、查得慢」,这一版是「**查得快、改得慢**」。
#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 + 2, 0); for (int i = 1; i <= n; i++) cin >> a[i];
string out; for (int q = 0; q < m; q++) { int op; cin >> op; if (op == 1) { int x, y; long long k; cin >> x >> y >> k; for (int i = x; i <= y; i++) a[i] += k; // ⚠ 一个一个加 } else { int x; cin >> x; out += to_string(a[x]); // ★ 查:一句话 out += '\n'; } } cout << out; return 0;}点「运行 ▶」看结果
P3374 是「单点加 + 区间和」,这道题是「区间加 + 单点查」—— 两道题的两个暴力正好换了个位置:
| 改 | 查 | 顶格 N = M = 5 × 10⁵ |
|
|---|---|---|---|
| P3374 的暴力 | ★ O(1) | ⚠ O(区间长度) | 2.5 × 10¹¹ |
| ★ 这道题的暴力 | ⚠ O(区间长度) | ★ O(1) | 2.5 × 10¹¹ |
★ 而分数是一样的:题面「70% 的数据 N, M ≤ 10⁴」那一档它跑得完 ⇒ 稳拿 70 分。
2★ 第 6 章那招:差分 —— 对,但它把慢挪到了「查」上
// P3368 第 ② 版:**纯差分数组** —— 第 6 章那招原样搬过来//// d[x] += k、d[y+1] −= k ⇒ 区间加变成两格,O(1)// a[x] = d[1] + … + d[x] ⇒ ⚠ 每问一次就要现算一遍前缀和,O(n)//// ★ 它是**对的**(对拍 300 轮逐字节相同),而且比第 ① 版强一头:// 第 ① 版慢在「改」,这一版把改压到了 O(1) —— 但它把慢挪到了「查」上。//// ⚠ 顶格 M 次询问 × O(n) = 2.5×10¹¹,还是过不去。// ⇒ ★★ 这一版才是这道题真正的台阶:**它证明了「差分」这一步是对的,// 缺的只是「怎么把那个前缀和也变快」** —— 而那正好是树状数组的定义。// ⇒ 第 ③ 版把 d 交给树状数组,两边同时变成 O(log n),就 AC 了。
#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> d(n + 2, 0); long long prev = 0; for (int i = 1; i <= n; i++) { long long x; cin >> x; d[i] = x - prev; prev = x; }
string out; for (int q = 0; q < m; q++) { int op; cin >> op; if (op == 1) { int x, y; long long k; cin >> x >> y >> k; d[x] += k; d[y + 1] -= k; // ⚠ 数组要开到 n+1 } else { int x; cin >> x; long long s = 0; for (int i = 1; i <= x; i++) s += d[i]; // ⚠ 每次都从头攒一遍 out += to_string(s); out += '\n'; } } cout << out; return 0;}点「运行 ▶」看结果
第 6 章那对逆运算:
d[i] = a[i] − a[i−1] (差分)
a[x] = d[1] + d[2] + … + d[x] (前缀和)⇒ 「给 [x, y] 每个数加 k」在 d 上只动两格(d[x] += k、d[y+1] −= k),O(1);
⇒ 「问 a[x]」变成了前缀和,而这一版每问一次就从头攒一遍,O(n)。
★ 它是对的(对拍 300 轮逐字节相同),也确实比第 ① 版强了一头。
⚠ 可顶格还是 M × n = 2.5 × 10¹¹。
⇒ ★★★ 它把这道题剩下的问题缩成了一句话:「怎么让那个前缀和也变快」 —— 而「可以修改的前缀和」正是树状数组的定义。
3★★★ 正解:把树状数组建在差分数组上 —— 和上一道题是同一份代码
// P3368【模板】树状数组 2 —— ★ 这一版就能 AC(区间加 + 单点查)//// ============ ★★★ 关键一步:把树状数组建在**差分数组**上 ============//// 上一道 [P3374](/sol/p3374/) 是「单点加 + 区间和」,这道题**正好反过来**:// 「区间加 + 单点查」。而这两件事之间隔着的,正是第 6 章那对逆运算://// d[i] = a[i] − a[i−1] (差分)// a[x] = d[1] + d[2] + … + d[x] (前缀和)//// ⇒ 「给 [x, y] 每个数加 k」在 d 上只动**两格**:d[x] += k、d[y+1] −= k;// ⇒ 「问 a[x] 是多少」在 d 上就是**前缀和 sum(x)**。//// ⇒ ★★ 于是这道题和上一道用的是**同一份树状数组代码,一个字都不用改** ——// 换掉的只是「喂给它的数组是谁」:上一道喂 a,这一道喂 d。// (这也是本章第 ⑦ 步那句「树状数组维护的是**你选的那个数组**的前缀和」的现场。)//// ============ ⚠ 为什么必须是树状数组,第 6 章那招不够 ============//// 第 6 章 [P2367](/sol/p2367/) 也是「区间加」,那里**纯差分数组就够了** ——// 因为那道题**所有修改做完之后才问一次**,最后统一还原一遍 O(n) 就完事。// 这道题的修改和询问是**穿插**的(第 ③ 步量了:随机数据里「所有修改都排在所有询问之前」// 的概率是多少),⇒ 每问一次就得现算一遍前缀和 O(n) ⇒ 顶格 2.5×10¹¹。// ⇒ **差的那三个字就是「可修改」。**//// ⚠ 为什么是 long long:题面保证「任意时刻序列中任意元素的绝对值都不大于 2³⁰」// ⇒ |d[i]| = |a[i] − a[i−1]| 最大到 **2³¹**,而 int 的上限是 2³¹ − 1 ——// **正好差 1**(第 ④ 步:这条线只有一个点宽)。
#include <bits/stdc++.h>using namespace std;
const int MAXN = 500005;long long c[MAXN];int n, m;
inline int lowbit(int i) { return i & -i; }
void add(int p, long long x) { if (p > n) return; // ⚠ y = n 时 y+1 会越界,这里挡掉 for (int i = p; i <= n; i += lowbit(i)) c[i] += x;}long long sum(int r) { long long s = 0; for (int i = r; i > 0; i -= lowbit(i)) s += c[i]; return s;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
if (!(cin >> n >> m)) return 0;
// ★ O(n) 建树,喂进去的是**差分** d[i] = a[i] − a[i−1] long long prev = 0; for (int i = 1; i <= n; i++) { long long x; cin >> x; c[i] += x - prev; prev = x; int f = i + lowbit(i); if (f <= n) c[f] += c[i]; }
string out; for (int q = 0; q < m; q++) { int op; cin >> op; // ⚠ 每行 2 个还是 4 个数,由它说了算 if (op == 1) { int x, y; long long k; cin >> x >> y >> k; add(x, k); add(y + 1, -k); } else { int x; cin >> x; out += to_string(sum(x)); out += '\n'; } } cout << out; return 0;}点「运行 ▶」看结果
| P3374 | ★ 这道题 | |
|---|---|---|
| 树状数组建在谁身上 | 原数组 a |
★ 差分数组 d |
单点加 add(p, k) 表示 |
a[p] += k |
d[p] += k |
前缀和 sum(r) 表示 |
a[1..r] 的和 |
★ a[r] 本身 |
| 于是题目的两种操作 | 单点加 / 区间和 | 区间加(两次 add)/ 单点查(一次 sum) |
⇒ 两道题的 lowbit / add / sum 一个字都不差,换掉的只是「喂给它的数组是谁」。
★ 这也是本章第 ⑦ 步那句话的现场:树状数组维护的是你选的那个数组的前缀和 ——
选 a 就得到「单点改 + 区间查」,选 d 就得到「区间改 + 单点查」。
本机实测(A 机 · WSL2 · Linux 6.18 / 8 线程 / 7 GB,2026-08-31,独占;进程内 steady_clock):
N = M |
① 一个一个加 | ② 纯差分 | ★ ③ 差分 + 树状数组 |
|---|---|---|---|
| 10⁵ | 235 ms | 586 ms | ★ 3 ms |
| 5 × 10⁵(顶格) | 按 N·M 外推约 6 秒 |
约 15 秒 | ★ 25 ms |
4✗ 最值钱的一个错法:以为可以照搬 P2367
P2367 语文成绩 也是「区间加」,而那道题纯差分就够了 —— 因为它所有修改做完之后才问一次,最后统一还原一遍 O(n) 就完事。
这道题的 2 x 问的是「此刻第 x 个数是多少」,而修改和询问是穿插的。
★ 它的触发条件写得出来:某次询问后面还有一次修改盖住了被问的位置。 实测(档 0,顺手写的小数据,300 轮):
| 轮数 | |
|---|---|
| ① 存在「询问后面还有盖住它的修改」 | 232 |
| ② 它真被抓 | ★ 232 —— 一个不差 |
⇒ 而档 1(把所有修改排到所有询问之前,也就是照着 P2367 的形状造数据) 是能证的精确的 0:那一档它和正解逐字节相同。 ⇒ ★★ 这一对档位就是这道题和 P2367 之间的全部差别, 而它也正好是「这道题该不该上树状数组」的判据(P2367 那一页第 ⑦ 步反过来又验了一次)。
5⚠ 一个和算法完全无关、却真会挂人的坑
P3374 的每一行都是 3 个数(1 x k / 2 x y),顺手就照着写了;
这道题的两种操作长度不一样:1 x y k 四个数、2 x 只有两个。
⚠ 读多了之后所有后续数字整体串位,所以它通常不是「答错一两个」而是从某行起全乱。 ★ 五个档各 300 轮:293 / 300 / 297 / 298 / 297 —— 它是这一页最好抓的一个。 ⇒ ★★ 「先读操作类型,再决定读几个数」这句话,值一整道题的分。
6★★★ 那条只有一个点宽的线:|d| 最大 2³¹,int 上限 2³¹ − 1
题面:「保证任意时刻序列中任意元素的绝对值都不大于 2³⁰」。
|a[i]| ≤ 2³⁰ = 1 073 741 824
⇒ |d[i]| = |a[i] − a[i−1]| ≤ 2³⁰ + 2³⁰ = 2³¹ = 2 147 483 648
而 int 的上限是 2 147 483 647
⇒ ★ 正好差 1★ 也就是说:只有 a[i−1] = −2³⁰ 且 a[i] = +2³⁰ 那一格才顶得到它 ——
⚠ 而顺手把值域写成 ±10⁹ 的档位一次都碰不到(2 × 10⁹ 距 int 上限还有 7.4% 的余量)。
⇒ 和第 37 章 P3378 那条一模一样:顺手写的上界正好卡在 bug 的下面。
⚠ 而这条线窄到连造它都要把别的旋钮全锁死:想让相邻两项差到 2³¹,
两项就必须钉死在 ±2³⁰ 上,于是那一档的修改只能全取 k = 0(题面允许)——
「任意时刻 |a| ≤ 2³⁰」这句话把活动余地也一起收走了。
实测(每档 300 轮):
| 档 2:值域 ±10⁹ | ★ 档 3:值域顶到 ±2³⁰ | ⚠ 档 4:故意违反题面(±10¹⁰) | |
|---|---|---|---|
| ① 树里真的出现过越出 int 的中间值 | 0 | ★ 224 | 300 |
| ② 答案本身越出 int | 0 | ★ 0 | 291 |
③ int 版真被抓 |
0 | ★ 0 | ★ 291 —— 和 ② 一个不差 |
★★★ 中间那一列是这一页最值钱的一格:224 轮真的溢出了,可它一次都没错。
道理两行:int 的加减是模 2³² 同余的(补码),
而这道题要输出的 a[x](|a[x]| ≤ 2³⁰)装得进 int
⇒ 中间怎么绕,最后都会绕回同一个数。
⇒ 于是右边那一列就是它的自检:把题面那句保证整个拿掉(答案本身也装不下了), 它当场被抓 291 / 300,而 291 恰好 ≡「答案越界」的轮数。 (「精确的 0」要配自检、「触发条件 ≡ 抓获数」,两条老规矩在同一张表里各中一次。)
⚠⚠ 但结论不是「可以写 int」:有符号溢出是 UB,
它这次绕回来是编译器按补码办事的结果,标准一个字都没保证。
⇒ 正解写 long long:代价是零,而它让上面这整段推理都不必要。
7★ 对拍:五个档位各 300 轮
| 300 轮 | ① 一个一个加 | ② 纯差分 | ✗ 以为可以离线 | ✗ 每行读 4 个 | ★ 差分用 int |
|---|---|---|---|---|---|
| 档 0:顺手写的样子 | 0 | 0 | 232 | 293 | 0 |
| ★ 档 1:所有修改排在所有询问之前 | 0 | 0 | ★ 0 | 300 | 0 |
| 档 2:值域 ±10⁹(守住题面) | 0 | 0 | 248 | 297 | 0 |
| ★ 档 3:值域顶到 ±2³⁰、修改全取 0 | 0 | 0 | ★ 0 | 298 | ★ 0 |
| ⚠ 档 4:故意违反题面(±10¹⁰) | 0 | 0 | 248 | 297 | ★ 291 |
★ 表里三个 0 各有各的理由,没有一个是「概率低」:
档 1 那个是「离线在那一档本来就是对的」;
档 3 那两个 0 一个是「那一档没有真正的修改」(k 全取 0),
另一个是「溢出了但答案绕得回来」(上一步那张表)。
8一页纸
| ★ 暴力值多少分 | 题面 70% 档 N, M ≤ 10⁴ ⇒ 稳拿 70 分;顶格 2.5 × 10¹¹ 次 |
| ★★ 中间那一版 | 纯差分:改变成 O(1)、查还是 O(n) ⇒ 它把问题缩成「怎么让前缀和也快」 |
| ★★★ 关键一步 | 把树状数组建在差分数组上 ⇒ 和 P3374 同一份代码,只换了「喂给它谁」 |
| ★★★ 和 P2367 的差别 | 那道题改完才问一次(差分够),这道题边改边问(非树状数组不可)—— 触发 232 ≡ 抓获 232 |
| ⚠ 和算法无关的坑 | 「每行 2 或 4 个整数」——上一道题每行都是 3 个,照抄必串位(293~300 / 300 被抓) |
| ★★★ 那条一个点宽的线 | |a| ≤ 2³⁰ ⇒ |d| 最大 2³¹,int 上限 2³¹ − 1,正好差 1;顺手的 ±10⁹ 差 7.4% 碰不到 |
| ★★★ 而它溢出了也不错 | 档 3:中间值越界 224 / 300,被抓 0 —— int 的加减模 2³² 同余,答案装得下就绕得回来 |
| ⚠ 别学的地方 | 有符号溢出仍是 UB ⇒ 正解写 long long(代价是零) |