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 底下挂 2 和 4,2 底下挂 3 和 5;初值 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[根] = 1,要求的是 ans(x) = Σ (x 的每个祖先,含它自己)的点权。
① 1 u a(单点加):只有 u 一个点变了 ⇒ 对 ans(y) 的贡献是
「y 在 u 的子树里就 +a,否则 0」。
② 2 u a(子树加):u 的子树里每个点都 +a ⇒ 对 ans(y) 的贡献是
「y 的祖先里有几个落在 u 的子树里」× a。
而 y 在 u 的子树里时,那些祖先正好是 u … y 这一段 ⇒ 个数 = dep[y] − dep[u] + 1;
y 不在时是 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 [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;}点「运行 ▶」看结果
3从暴力爬上来:三级台阶,而中间那一级答案全对
| 形状 | 最大深度 | ★ 正解 | ✗ 第二版(爬) | 树链剖分 |
|---|---|---|---|---|
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★★ 和「题单说的那个正解」比一比
随机树、顶格 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
// 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;}点「运行 ▶」看结果
| 档位 | ①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 倍
一次 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 的一次函数,
两棵树状数组就够了。
⇒ ★★ 如果它问的是「任意两点之间」,那才轮到树链剖分出场。