题单 · 习题解析

洛谷 P3368 【模板】树状数组 2

★★★ 关键一步是把树状数组建在**差分数组**上 ⇒ 和 [P3374](/sol/p3374/) **同一份代码**,换的只是「喂给它谁」(选 a 得到单点改+区间查,选 d 得到区间改+单点查);★★ 中间那一版(纯差分 + 每次现算前缀和)是对的,它把问题缩成「怎么让前缀和也快」;★★★ 最值钱的错法是「以为可以照搬 [P2367](/sol/p2367/):先做完所有修改再统一回答」—— 触发 232 ≡ 抓获 232,而把所有修改排到询问之前那一档是**能证的 0** ⇒ 两道题差的就是「可修改」三个字;⚠ 和算法无关的坑:「每行 2 **或** 4 个整数」(上一题每行都是 3 个),照抄必串位;★★★ 那条线只有一个点宽:|a| ≤ 2³⁰ ⇒ |d| 最大 2³¹ 而 int 上限 2³¹−1,**正好差 1**(顺手的 ±10⁹ 差 7.4% 碰不到)—— 而顶到线上那一档**中间值真的越界 224/300、被抓 0**(模 2³² 同余 ⇒ 答案绕得回来),拿掉题面保证才 291 ≡ 291

原题:洛谷 P3368出自 第 38 章 树状数组 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

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

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

输入格式

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

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

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

  • 操作 1:格式:1 x y k 含义:将区间 [x, y] 内每个数加上 k
  • 操作 2:格式:2 x 含义:输出第 x 个数的值。

输出格式

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

数据规模与约定

对于 30% 的数据:N ≤ 8M ≤ 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 31 2 4 2[2,4] 各加 2 ⇒ 1 7 6 4 3,于是 2 361 1 5 -10 6 5 3 21 3 5 70 6 12 10 9,于是 2 410

P3368 样例 1 解释:五步操作和每一步之后的数列

1第一反应:区间加就一个一个加

p3368Brute.cpp第 ① 版:改 O(区间长度)、查 O(1) —— ★ 70 分
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 它和上一道题的暴力,慢在「相反」的地方

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 章那招:差分 —— 对,但它把慢挪到了「查」上

p3368Diff.cpp第 ② 版:纯差分(★ 答案全对,顶格仍然超时)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 这一版才是这道题真正的台阶

第 6 章那对逆运算:

d[i] = a[i] − a[i−1]            (差分)
a[x] = d[1] + d[2] + … + d[x]   (前缀和)

⇒ 「给 [x, y] 每个数加 k」在 d 上只动两格d[x] += kd[y+1] −= k),O(1); ⇒ 「问 a[x]」变成了前缀和,而这一版每问一次就从头攒一遍,O(n)。

★ 它是对的(对拍 300 轮逐字节相同),也确实比第 ① 版强了一头。 ⚠ 可顶格还是 M × n = 2.5 × 10¹¹

⇒ ★★★ 它把这道题剩下的问题缩成了一句话:「怎么让那个前缀和也变快」 —— 而「可以修改的前缀和」正是树状数组的定义。

3★★★ 正解:把树状数组建在差分数组上 —— 和上一道题是同一份代码

p3368.cpp★ 这一版就能 AC(顶格端到端 0.11 秒 / 时限 1 秒)
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 「树状数组维护谁的前缀和」是你自己选的
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

p3368Offline.cpp✗ 先把所有修改做完,再统一回答(官方样例当场打死:12 10)
★★★ 第 6 章那道题就是这么做的 —— 差的只有三个字:可修改

P2367 语文成绩 也是「区间加」,而那道题纯差分就够了 —— 因为它所有修改做完之后才问一次,最后统一还原一遍 O(n) 就完事。

这道题的 2 x 问的是「此刻x 个数是多少」,而修改和询问是穿插的。

★ 它的触发条件写得出来:某次询问后面还有一次修改盖住了被问的位置。 实测(档 0,顺手写的小数据,300 轮):

轮数
① 存在「询问后面还有盖住它的修改」 232
② 它真被抓 232 —— 一个不差

⇒ 而档 1(把所有修改排到所有询问之前,也就是照着 P2367 的形状造数据) 是能证的精确的 0:那一档它和正解逐字节相同。 ⇒ ★★ 这一对档位就是这道题和 P2367 之间的全部差别, 而它也正好是「这道题该不该上树状数组」的判据P2367 那一页第 ⑦ 步反过来又验了一次)。

5⚠ 一个和算法完全无关、却真会挂人的坑

p3368Fix4.cpp✗ 每行固定读 4 个整数(样例当场乱:6 65 10833)
⚠ 「每行包含 2 或 4 个整数」——上一道题每行都是 3 个

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³⁰」这句话把活动余地也一起收走了。

p3368Int.cpp★★ 差分数组用 int —— 中间值真的溢出了,可答案一个字都不错
★★★ 它溢出了 224 轮,被抓 0 轮 —— 而这两个数都能解释

实测(每档 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 轮

p3368Gen.cpp(七个档位)数据生成器
p3368Count.cpp度量程序(本页的数字都出自它)
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(代价是零)