题单 · 习题解析

洛谷 P3178 [HAOI2015] 树上操作

★★★ 这道题教的是一句话:**「上一个工具不够用」和「该换哪一个工具」是两个问题**。差分不够用,是因为它**只能还原一次**([第 38 章那条判据](/sol/p3368/):能不能用差分 = 所有修改是不是都在所有询问之前);⚠⚠ 而**题单原来那句「正解是树链剖分」这一页订正了** —— 这道题只问「**根到 x**」,于是一次子树加对某个后代的贡献恰好是 `dep` 的**一次函数**(`k·dep[y] + b`,子树加 `k = a, b = a(1−dep[x])`,单点加 `k = 0, b = a`)⇒ **DFS 序 + 两棵「区间加单点查」的树状数组**就全做完了,一次 `O(log n)`;★★ 实测比剖分快 **3.3 倍**、代码 56 行对 96 行 —— ⚠ 而**端到端只差 1.9 倍**,不减掉那 21.5 ms 读入会说成另一个数([P3366 那条](/sol/p3366/)的第五次);★★★ 那张对拍表里**一次出现四种性质完全不同的 0**:答案永远对(第二版「询问还在爬」/剖分)、档位到不了那条线(`int`,线精确在**深度 2148**,而对拍的树只有 20 个点 ⇒ 差 107 倍)、结构性的 0(「先全改完再全问」那一档 Offline 精确的 0 —— 那正是它的自检)、以及整整一档在验零;★★ 而「单点加当成子树加」的触发 ≡ 抓获**五档一个不差**(98/132/0/0/89);⚠ 顶格那张表是这一轮第三次撞见同一件事:「询问一步一步爬到根」那一版在 `star`/`rand`/`binary` 上**和正解一样快(0.03 秒)**,只在链上 **9.30 秒**(慢 310 倍),而 `int` 那一版也只在链上才烂

⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

有一棵点数为 N 的树,以点 1 为根,且树有点权。然后有 M 个操作,分为三种:

  • 操作 1:把某个节点 x 的点权增加 a
  • 操作 2:把某个节点 x 为根的子树中所有点的点权都增加 a
  • 操作 3:询问某个节点 x 到根的路径中所有点的点权和。

输入格式

第一行包含两个整数 N, M。表示点数和操作数。

接下来一行 N 个整数,表示树中节点的初始权值。

接下来 N−1 行每行两个正整数 from, to,表示该树中存在一条边 (from, to)

再接下来 M 行,每行分别表示一次操作。格式为 op, x, a, 分别表示操作种类、节点序号和增加的数量。

输出格式

对于每个询问操作,输出该询问的答案。答案之间用换行隔开。

数据范围

对于 100% 的数据,1 ≤ N, M ≤ 10⁵,且所有输入数据的绝对值都不会超过 10⁶

时限 1 秒,内存 128000 KB(125 MiB)。

输入输出样例

输入

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

输出

6
9
13

★ 树是 1 底下挂 242 底下挂 35;初值 1 2 3 4 5。 三次询问:3+2+1 = 6 /(2 号点 +1 之后)5+3+1 = 9 /(全树 +2 之后)5+5+3 = 13

1★★★ 这道题放在这一章,是为了让你看见「树上差分不够用」

★★ 差分的能力边界,一句话

本章那一套是离线的:把所有修改叠成标记,最后一趟子树求和还原。 ⇒ 它做不到「改一次、问一次、再改一次」——因为还原是一次性的

⇒ 这正是第 38 章 P2367 / P3368 那条判据

「这道题能不能用差分」= 「所有修改是不是都在所有询问之前」。

而这道题的 M 个操作是混着来的 ⇒ 差分当场出局。 (第 ⑤ 步那个 p3178Offline.cpp 就是「非要离线」的下场; ⚠ 而它在「先全改完再全问」那一档上是精确的 0 —— 那一档差分确实够用。)

⚠⚠ 而题单原来那句「正解是树链剖分」,这一页要订正它

第 52 章题单里这道题的注解写着「放在这儿是为了让你亲眼看到本章这套为什么不够用(正解是树链剖分)」。

前半句对,后半句是多余的。这道题用 DFS 序 + 两个树状数组就全做完了, O((N+M) log N),而且代码短一半、快 3.3 倍(第 ④ 步有实测)。

★ 分水岭在询问的形状上:树链剖分能回答「任意两点之间」的路径; 而这道题只问「根到 x」—— 这一条特殊得多,能被一句代数吃掉(下一段)。 ⇒ ★★★ 「本章这套不够用」推不出「得上最重的那个工具」。 (题单那条注解已按这一页改写。)

2★★★ 把「根到 x 的和」拆成一次代数

★★ 两种修改对一次询问的贡献,都是 dep 的一次函数

dep[根] = 1,要求的是 ans(x) = Σx 的每个祖先,含它自己)的点权。

1 u a(单点加):只有 u 一个点变了 ⇒ 对 ans(y) 的贡献是 「yu 的子树里就 +a,否则 0」。

2 u a(子树加):u 的子树里每个点都 +a ⇒ 对 ans(y) 的贡献是 「y 的祖先里有几个落在 u 的子树里」× a。 而 yu 的子树里时,那些祖先正好是 u … y 这一段 ⇒ 个数 = dep[y] − dep[u] + 1y 不在时是 0。

⇒ ★★★ 两条都能写成 k·dep[y] + b,而且都只作用在「u 的子树」那一段连续的 DFS 序上

   ① 单点加:  k = 0,   b = a
   ② 子树加:  k = a,   b = a · (1 − dep[u])

⇒ 于是只要两棵「区间加、单点查」的树状数组(一棵管 k,一棵管 b),

   ans(x) = dep[x] · K(tin[x]) + B(tin[x])

一次询问 O(log n),一行公式,没有剖分、没有链、没有第二层 log。

p3178.cpp★ 正解:DFS 序 + 两个树状数组
// P3178 [HAOI2015] 树上操作 —— 正解:**DFS 序 + 两个树状数组**
//
// ★★★ 这道题放在第 52 章题单里,是为了让你亲眼看到**树上差分不够用**:
// 差分那一套是**离线**的 —— 先把所有修改叠成标记,最后一趟子树求和还原。
// 而这道题是**边改边问**(p3178Offline.cpp 就是「非要离线」的下场)。
//
// ⚠⚠ 而题单原来那句「正解是树链剖分」**是多余的**:这道题的三种操作用
// **DFS 序 + 两个树状数组**就能全部做完,`O((N+M) log N)`,而且更短更快。
// 道理在于「查询」的形状特别老实 —— 它问的是**根到 x**,不是任意两点之间。
//
// ★★★ 推一遍就出来了。设 `dep[根] = 1`,问的是 `ans(x) = Σ_{v 是 x 的祖先或自己} val(v)`:
// ① `1 x a`(单点加):只有 x 一个点变了 ⇒ 对 ans(y) 的贡献是
// 「y 在 x 的子树里就 +a,否则 0」;
// ② `2 x a`(子树加):x 的子树里每个点都 +a ⇒ 对 ans(y) 的贡献是
// 「y 的祖先里有几个落在 x 的子树里」× a。而 y 在 x 子树里时,
// 那些祖先正好是 x…y 这一段 ⇒ 个数 = `dep[y] − dep[x] + 1`;y 不在时是 0。
//
// ⇒ **两条都能写成 `k·dep[y] + b` 的形状,而且都只作用在「x 的子树」这一段连续的 DFS 序上**:
// ① k = 0, b = a
// ② k = a, b = a·(1 − dep[x])
// ⇒ 于是只要两棵「区间加、单点查」的树状数组:一棵管 k,一棵管 b,
// 答案就是 `ans(x) = dep[x] · K(tin[x]) + B(tin[x])`。
//
// ★ 「要不要 long long」是一句乘法:单点最大 |val| ≈ 10⁶ + 10⁵ × 10⁶ ≈ 10¹¹,
// 路径最多 10⁵ 个点 ⇒ 答案能到 **10¹⁶**,而 long long 的上限是 9.2×10¹⁸ ⇒ 够,余量约 920 倍;
// ⚠ 而 int 差着五个数量级 —— 见 p3178Int.cpp。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;
int tin[MAXN], tout[MAXN], dep[MAXN], n, m, timer_;
long long bitK[MAXN], bitB[MAXN];
inline void addEdge(int u, int v) { to_[++ecnt] = v; nxt_[ecnt] = head_[u]; head_[u] = ecnt; }
inline void bitAdd(long long* c, int i, long long v) { for (; i <= n; i += i & (-i)) c[i] += v; }
inline long long bitAsk(long long* c, int i) { long long s = 0; for (; i > 0; i -= i & (-i)) s += c[i]; return s; }
/* 区间 [l, r] 加 v,单点查前缀和 */
inline void rangeAdd(long long* c, int l, int r, long long v) { bitAdd(c, l, v); bitAdd(c, r + 1, -v); }
int main() {
if (scanf("%d %d", &n, &m) != 2) return 0;
vector<long long> init(n + 1);
for (int i = 1; i <= n; i++) if (scanf("%lld", &init[i]) != 1) return 0;
for (int i = 0; i < n - 1; i++) {
int x, y;
if (scanf("%d %d", &x, &y) != 2) return 0;
addEdge(x, y); addEdge(y, x);
}
/* 迭代 DFS 求 tin / tout / dep(⚠ 顶格 10⁵ 排成一条链,递归有 10 万层)*/
{
vector<int> st(n + 5), it(n + 5);
int top = 0;
st[top] = 1; it[1] = head_[1]; dep[1] = 1; tin[1] = ++timer_;
vector<int> par(n + 1, 0);
while (top >= 0) {
int u = st[top];
int e = it[u];
if (e == 0) { tout[u] = timer_; top--; continue; }
it[u] = nxt_[e];
int v = to_[e];
if (v == par[u]) continue;
par[v] = u; dep[v] = dep[u] + 1; tin[v] = ++timer_; it[v] = head_[v];
st[++top] = v;
}
}
for (int i = 1; i <= n; i++) rangeAdd(bitB, tin[i], tout[i], init[i]); // 初值 = n 次单点加
for (int q = 0; q < m; q++) {
int op, x;
long long a;
if (scanf("%d %d", &op, &x) != 2) return 0;
if (op == 1) {
if (scanf("%lld", &a) != 1) return 0;
rangeAdd(bitB, tin[x], tout[x], a);
} else if (op == 2) {
if (scanf("%lld", &a) != 1) return 0;
rangeAdd(bitK, tin[x], tout[x], a);
rangeAdd(bitB, tin[x], tout[x], a * (1LL - dep[x]));
} else {
long long k = bitAsk(bitK, tin[x]), b = bitAsk(bitB, tin[x]);
printf("%lld\n", k * dep[x] + b);
}
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3从暴力爬上来:三级台阶,而中间那一级答案全对

p3178Brute.cpp✗ 第一版(也是对拍的标准答案):照着题面一句一句做
p3178Climb.cpp✗ 第二版:子树加压成了区间加,可询问还在一步一步爬
p3178GenBig.cpp★ 顶格生成器:rand / chain / star / binary
p3178Read.cpp★ 对照:只把 3.2 MB 输入读进来,什么都不算
★★★ 顶格 N = M = 10⁵(时限 1 秒)—— 第二版只在链上死
形状 最大深度 ★ 正解 ✗ 第二版(爬) 树链剖分
star 2 0.03 秒 0.03 0.05
rand 28 0.03 秒 0.03 0.07
binary 17 0.03 秒 0.03 0.08
chain 100 000 0.03 秒 9.30 秒 0.06

(本机 · A 机 WSL2 · nproc 12 · 2026-09-12 · 独占 · 每格 3 次取中位数;只读入 0.02 秒。)

⇒ ★★ 第二版在三种形状上和正解一样快,只在链上慢 310 倍 —— 这是这一轮的第三道题上又一次撞见同一件事: 顺手造一组「顶格随机」跑一遍,它什么都看不出来。 ★ 而它的答案一个字都不错 ⇒ 对拍那一列五档全是 0(第 ⑤ 步)。

⇒ ★ 反过来,正解的耗时和树的形状完全无关(四格都是 0.03 秒)—— 因为它一次询问只碰 log n 个格子,跟深度没有半点关系。

⚠⚠ 同一批顶格数据还把「int 够不够」也演了一遍,而结论是同一个方向star / rand / binary 三档里中间值最大只有 2.0×10⁷(离 2³¹ 差 105 倍) ⇒ 那个全程用 int 的错法在这三档上逐字节全对; 只有 chain 那一档冲到 2.4×10¹² ⇒ 当场瞎掉。 ⇒ ★★ 这一页第三次撞见同一件事:形状不对,什么都问不出来。

4★★ 和「题单说的那个正解」比一比

p3178Hld.cpp★ 对照:树链剖分 + 线段树 —— 也对,但在这道题上是多余的
★★★ 端到端只差 1.9 倍,减掉读入之后是 3.3 倍

随机树、顶格 N = M = 10⁵,每份跑 20 遍取平均(本机,独占):

端到端 减掉「只读入」的 21.5 ms
只读入 21.5 ms
正解(两个树状数组) 36.0 ms 14.5 ms 56 行有效代码
树链剖分 + 线段树 69.0 ms 47.5 ms 96 行有效代码

⇒ ★★ 不减掉那 21.5 ms 会说成「1.9 倍」,减掉之后是「3.3 倍」 —— 第 34 章 P3366第 46 章 P1469第 47 章 P1308第 50 章 P8306 那条的第五次量什么都要先问一句「这个数里有没有别人的份」。

★ 慢在哪儿也说得清:剖分那条路一次询问要跳 O(log n) 条链、每条链再走一次线段树 ⇒ O(log²n),而且线段树是递归 + 懒标记下推; 树状数组那条路是两个平的 while 循环,一次 O(log n)

5★ 对拍:一张表里四种性质完全不同的 0

p3178Offline.cpp✗ 错法①:把所有修改先做完,最后统一回答 —— 差分那一套的极限
p3178AsSub.cpp✗ 错法②:把 1 号操作(单点加)也按 2 号操作写了
p3178NoDep.cpp✗ 错法③:子树加那条常数标记少了一个 +1
p3178Int.cpp✗ 错法④:算法一个字没错,全程用 int
p3178Zero.cpp★ 试金石:什么都不算,一律输出 0
p3178Gen.cpp★ 生成器:五个档位
p3178Count.cpp★ 数三个触发条件:交错 / 单点加落在严格祖先上 / 中间值有没有越过 2³¹
// P3178 的账:这份数据长什么样、几个错法的触发条件成不成立、两条路各做多少次基本动作
//
// 用法:./p3178Count <csv|table> < 一份输入
//
// ★ 数四个决定成败的东西:
// ① **修改和询问有没有真的交错**(询问后面还有修改)—— 错法④Offline 的触发条件;
// ② **有没有一次单点加落在某个后续询问点的严格祖先上** —— 错法③AsSub 的触发条件;
// ③ **中间值的绝对值最大到多少** —— 错法①Int 的那条数值线;
// ④ **爬到根一共要走多少个点** —— 第二版(p3178Climb)那张顶格表的主角。
// ⚠ ①②是 O(m²) 数的,所以 m > 2000 时直接报 −1(顶格表用不着它们)。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
static int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], ecnt;
static int tin[MAXN], tout[MAXN], dep[MAXN], par[MAXN], order_[MAXN], n, m, timer_;
static long long val[MAXN];
int main(int argc, char** argv) {
string mode = argc > 1 ? argv[1] : "table";
if (scanf("%d %d", &n, &m) != 2) return 0;
for (int i = 1; i <= n; i++) if (scanf("%lld", &val[i]) != 1) return 0;
for (int i = 0; i < n - 1; i++) {
int x, y;
if (scanf("%d %d", &x, &y) != 2) return 0;
to_[++ecnt] = y; nxt_[ecnt] = head_[x]; head_[x] = ecnt;
to_[++ecnt] = x; nxt_[ecnt] = head_[y]; head_[y] = ecnt;
}
{
vector<int> st(n + 5), it(n + 5);
int top = 0;
st[top] = 1; it[1] = head_[1]; dep[1] = 1; tin[1] = ++timer_; order_[timer_] = 1;
while (top >= 0) {
int u = st[top], e = it[u];
if (e == 0) { tout[u] = timer_; top--; continue; }
it[u] = nxt_[e];
int v = to_[e];
if (v == par[u]) continue;
par[v] = u; dep[v] = dep[u] + 1; tin[v] = ++timer_; order_[timer_] = v; it[v] = head_[v];
st[++top] = v;
}
}
int maxDep = 0;
for (int i = 1; i <= n; i++) maxDep = max(maxDep, dep[i]);
vector<int> op(m), ox(m);
vector<long long> oa(m, 0);
for (int i = 0; i < m; i++) {
if (scanf("%d %d", &op[i], &ox[i]) != 2) return 0;
if (op[i] != 3 && scanf("%lld", &oa[i]) != 1) return 0;
}
/* ★ 一遍模拟,同时量「答案」和「中间值最大绝对值」(这一份自己用 long long) */
long long maxAbs = 0, climbSteps = 0, qCnt = 0, ansNonZero = 0;
vector<long long> lazySub(n + 1, 0);
for (int i = 1; i <= n; i++) maxAbs = max(maxAbs, llabs(val[i]));
for (int i = 0; i < m; i++) {
if (op[i] == 1) { val[ox[i]] += oa[i]; maxAbs = max(maxAbs, llabs(val[ox[i]])); }
else if (op[i] == 2) {
for (int j = tin[ox[i]]; j <= tout[ox[i]]; j++) {
val[order_[j]] += oa[i];
maxAbs = max(maxAbs, llabs(val[order_[j]]));
}
} else {
long long s = 0;
for (int u = ox[i]; u; u = par[u]) { s += val[u]; climbSteps++; }
maxAbs = max(maxAbs, llabs(s));
qCnt++;
if (s != 0) ansNonZero++;
}
}
(void)lazySub;
long long interleave = -1, asSub = -1, noDep = -1;
if (m <= 2000) {
interleave = 0; asSub = 0; noDep = 0;
for (int i = 0; i < m; i++) {
if (op[i] != 3) continue;
for (int j = i + 1; j < m; j++) if (op[j] != 3) { interleave++; break; }
}
for (int i = 0; i < m; i++) {
if (op[i] != 1 || oa[i] == 0) continue;
for (int j = i + 1; j < m; j++)
if (op[j] == 3 && tin[ox[i]] <= tin[ox[j]] && tin[ox[j]] <= tout[ox[i]] && dep[ox[j]] > dep[ox[i]]) { asSub++; break; }
}
for (int i = 0; i < m; i++) {
if (op[i] != 2 || oa[i] == 0) continue;
for (int j = i + 1; j < m; j++)
if (op[j] == 3 && tin[ox[i]] <= tin[ox[j]] && tin[ox[j]] <= tout[ox[i]]) { noDep++; break; }
}
}
vector<pair<string, string> > out;
out.push_back(make_pair("n", to_string(n)));
out.push_back(make_pair("m", to_string(m)));
out.push_back(make_pair("maxdep", to_string(maxDep)));
out.push_back(make_pair("queries", to_string(qCnt)));
out.push_back(make_pair("ans_nonzero", to_string(ansNonZero)));
out.push_back(make_pair("max_abs", to_string(maxAbs)));
out.push_back(make_pair("over_int", to_string(maxAbs > 2147483647LL ? 1 : 0)));
out.push_back(make_pair("interleave", to_string(interleave)));
out.push_back(make_pair("trig_assub", to_string(asSub)));
out.push_back(make_pair("trig_nodep", to_string(noDep)));
out.push_back(make_pair("climb_steps", to_string(climbSteps)));
if (mode == "csv") for (size_t i = 0; i < out.size(); i++) printf("%s,%s\n", out[i].first.c_str(), out[i].second.c_str());
else for (size_t i = 0; i < out.size(); i++) printf(" %-14s %s\n", out[i].first.c_str(), out[i].second.c_str());
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 300 轮 × 五档(本机实测)
档位 ①Offline ②AsSub ③NoDep ④Int 第二版(爬) 树链剖分 试金石
0 顺手写法 207 98 158 0 0 0 300
1 修改全排在询问之前 0 132 193 0 0 0 300
2 单点加只打在叶子上 166 0 158 0 0 0 300
3 初值和 a 全为 0 0 0 0 0 0 0 0
4 最终档 227 89 121 0 0 0 300

★★★ 一张表里,四种性质完全不同的 0

那个 0 是哪一种 怎么救
答案永远对 第二版(爬)、树链剖分 救不了 —— 换尺子,数次数 / 看秒表
档位到不了那条线 ④Int 换档位(顶格),见第 ⑥ 步
结构性的 0(能证) ①档 1、②档 2 那正是它们的自检
这一档在验零 档 3 整行 换生成器

★★★ ②AsSub 的「触发 ≡ 抓获」五档一个不差: 「存在一次单点加落在某个后续询问点的严格祖先上」的轮数是 98 / 132 / 0 / 0 / 89,和它被抓的轮数逐格相同 (能证:它多算 a·(dep[y] − dep[x])dep[y] > dep[x] 时这个数必不为 0)。

⚠⚠ 而 ①Offline 的触发写得不够细:「修改和询问真的交错」在四档上是 287 / 0 / 287 / 287 / 300,抓获却只有 207 / 0 / 166 / 0 / 227 —— 交错了不等于交错改变了答案。⇒ ★★ 「能不能写成 ≡,取决于你第一层写到第几格」, 这一页两种结局各演了一次。

⚠ 档 3 是在验零那一格:初值和所有 a 全为 0 ⇒ 正确答案恒等于 0 ⇒ 六个待测版本连同试金石,全部满分

6⚠ int 那条线只有 2148 —— 而对拍差了 107 倍

p3178Line.cpp★ 探针:造一条指定深度的链,只做一次 2 1 1000000,再问最深那个点
★★ 先算,再拿真程序在两侧各跑一遍

一次 2 1 1000000 就给全树每个点 +10⁶;问最深那个点,答案就是 深度 × 10⁶。 ⇒ 越过 int 的线在 2147×10⁶ = 2 147 000 000(塞得下)和 2148×10⁶(塞不下)之间, 也就是深度 2148

⚠⚠ 而这条线不能拿同一个公式再算一遍来自检 —— 拿真程序跑(第 48 章 P3435 那一课):

链长 ★ 正解 ✗ ④Int
2146 2 146 000 000 2 146 000 000
2147 2 147 000 000 2 147 000 000
2148 2 148 000 000 −2 146 967 296
2149 2 149 000 000 −2 145 967 296

⇒ ★ 而对拍的树只有 n ≤ 20 个点 ⇒ 差 107 倍,结构上永远够不着。 ⇒ 这是第 48 章 P3435「档位到不了那条线」那一类 0 的又一个现场: 加轮数没用,得换档位。

★ 而顶格到底能多大:单点值能到 10⁶ + 10⁵ × 10⁶ ≈ 10¹¹, 路径 10⁵ 个点 ⇒ 答案能到 10¹⁶ —— 是 int 上限的 460 万倍,而 long long 的余量还有 920 倍。

7★ 哪一版就已经能过了

★★ 结论
版本 能过吗 数字(顶格最坏形状 chain
✗ 照题面直做 子树加 O(n)、询问 O(n) ⇒ 10¹⁰
✗ 子树加压成区间加、询问还爬 ✗(答案全对 9.30 秒 / 时限 1 秒
✗ 全程 int 深度 ≥ 2148 就烂
★ 树链剖分 + 线段树 0.06 秒(净 47.5 ms)、96 行
正解(DFS 序 + 两个树状数组) 0.03 秒(净 14.5 ms)、9.3 MiB / 125 MiB、56 行

⇒ 这道题的三件功课: ① 看清楚差分为什么不够用(改和问是交错的); ② 看清楚询问的形状(只问「根到 x」)—— 这一条把剖分整个省掉了; ③ long long(线低到只有 2148 层深)。

★ 一句话带走

这道题真正教的是一句话:「上一个工具不够用」和「该换哪一个工具」是两个问题。 ⇒ 差分不够用,是因为它只能还原一次; 而接下来该换什么,取决于这道题问的是什么 —— 它只问「根到 x」,于是一次子树加对一个后代的贡献恰好是 dep 的一次函数, 两棵树状数组就够了。 ⇒ ★★ 如果它问的是「任意两点之间」,那才轮到树链剖分出场。