0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2590,日期见页头。两边不一致时信原站。
题目描述
一棵树上有 n 个节点,编号分别为 1 到 n,每个节点都有一个权值 w。
我们将以下面的形式来要求你对这棵树完成一些操作:
- I.
CHANGE u t:把结点u的权值改为t。 - II.
QMAX u v:询问从点u到点v的路径上的节点的最大权值。 - III.
QSUM u v:询问从点u到点v的路径上的节点的权值和。
注意:从点 u 到点 v 的路径上的节点包括 u 和 v 本身。
输入格式
输入文件的第一行为一个整数 n,表示节点的个数。
接下来 n−1 行,每行 2 个整数 a 和 b,表示节点 a 和节点 b 之间有一条边相连。
接下来一行 n 个整数,第 i 个整数 wᵢ 表示节点 i 的权值。
接下来 1 行,为一个整数 q,表示操作的总数。
接下来 q 行,每行一个操作,以 CHANGE u t 或者 QMAX u v 或者 QSUM u v 的形式给出。
输出格式
对于每个 QMAX 或者 QSUM 的操作,每行输出一个整数表示要求输出的结果。
数据范围
对于 100% 的数据,保证 1 ≤ n ≤ 3×10⁴,0 ≤ q ≤ 2×10⁵。
中途操作中保证每个节点的权值 w 在 −3×10⁴ 到 3×10⁴ 之间。
时限 1 秒,内存 128000 KB(125 MiB)。
输入输出样例
输入
4 1 2 2 3 4 1 4 2 1 3 12 QMAX 3 4 QMAX 3 3 QMAX 3 2 QMAX 2 3 QSUM 3 4 QSUM 2 1 CHANGE 1 5 QMAX 3 4 CHANGE 3 6 QMAX 3 4 QMAX 2 4 QSUM 3 4
输出
4 1 2 2 10 6 5 6 5 16
★ 树是 1—2、2—3、4—1,初始权值 4 2 1 3。
⚠ 注意读入顺序:先 n−1 条边,再 n 个权值 —— 和上一道 P3384 正好相反。
1★★★ 剖分那一半一个字不用改 —— 换的是「线段树里放什么」
| P3384 | 这道题 | |
|---|---|---|
| 线段树维护 | 一个 sum |
一个 sum + 一个 mx |
| 修改 | 区间加(要懒标记) | 单点赋值(连懒标记都不用) |
| 幺元 | 0 |
0 和 ★★ −∞ |
| 剖分那一半 | 两趟遍历、跳链、top / dfn / siz |
一个字都不用改 |
⇒ ★★★ 而这一「换」里唯一会咬人的就是那个 −∞:
题面写着「每个节点的权值
w在 −3×10⁴ 到 3×10⁴ 之间」—— 权值可以是负的。 把max的幺元写成0,等于凭空往每条路径上塞了一个权值为 0 的点。
★ sum 那一半没有这个问题:加法的幺元本来就是 0。
⇒ ★★ 换一种线段树,真正要重新想的不是「怎么合并」,是「什么都没有的时候该返回什么」。
① CHANGE u t 是赋值,不是加(P3384 四种操作里三种是「加」,这道题唯一的修改是「改为」);
② 输入顺序:先 n−1 条边,再 n 个权值 —— 和 P3384 反过来;
③ 要不要 long long:一句乘法就问完了 ——
路径最多 n = 3×10⁴ 个点、|w| ≤ 3×10⁴ ⇒ |和| ≤ 9×10⁸,
而 int 上限 2 147 483 647 ⇒ int 就够,余量 2.39 倍(第 ⑥ 步有自检)。
2第一版:照题面爬
| 顶格形状 | ✗ 暴力 | ★ 正解 |
|---|---|---|
star(菊花) |
★ 0.02 秒 | 0.06 秒 |
rand(随机树) |
★ 0.03 秒 | 0.11 秒 |
binary(完全二叉树) |
★ 0.03 秒 | 0.15 秒 |
chain(链,端点随机) |
2.06 秒 | 0.05 秒 |
chain + 每次都问两端 |
⚠ 6.01 秒 | 0.02 秒 |
(本机 · A 机 WSL2 · nproc 12 · 2026-09-13 · 独占 · 每格 3 次取中位数。)
⇒ ★★ 这是这一章题单里第二次撞见同一件事:顶格随机那三档, 暴力比正解快 3.7 倍 —— 顺手造一组顶格随机数据,你会得出「这题不用学剖分」。
⇒ ★★★ 而这一页把「顶格 ≠ 最坏」又推深了一层:
同样是一条链、同样 q = 2×10⁵,只换「询问的两个端点怎么挑」就差 2.9 倍
(2.06 → 6.01 秒)。随机两个端点在链上平均只隔 n/3;顶死两端才是整整 n。
⇒ ★ 顺带一条别把话说满的:链上随机端点是 2.06 秒 / 时限 1 秒 ——
它不是差得离谱,是差 2 倍。
3正解:剖分照搬,线段树换一棵
// ★ 正解:树链剖分 + 一棵**同时维护「和」与「最大值」**的线段树//// ============ 这道题和本章原题(P3384)差在哪儿 ============//// 剖分那一半 —— 两趟遍历、跳链、`top`/`dfn`/`siz` —— **一个字都不用改**。// 换掉的只有线段树里放什么://// P3384:一个 `sum`,支持「区间加」 幺元 0// 这道题:一个 `sum` + 一个 `mx`,支持「单点赋值」 幺元 0 和 **−∞**//// ⚠⚠ 而这个「换」里唯一会咬人的就是那个 **−∞**:// 题面写着「权值 `w` 在 **−3×10⁴ 到 3×10⁴** 之间」—— **可以是负的**。// `mx` 的初值(以及跳链时那个累加器的初值)写成 0,负权路径上就会答出一个 0 来。// ⇒ 这是本页第一个错法,也是这一页的主线。//// ★ 另外两条不在剖分里的:// ① `CHANGE u t` 是**赋值**不是加;// ② 输入顺序是「先 n−1 条边,再 n 个权值」—— 和 P3384 **正好相反**。//// ★ 要不要 `long long`:路径最多 `n = 3×10⁴` 个点,`|w| ≤ 3×10⁴`// ⇒ `|和| ≤ 9×10⁸ < 2³¹−1 = 2.147×10⁹`,**`int` 就够,余量 2.39 倍**。// 这里仍写 `long long`(代价是零),而 `p2590Int.cpp` 是那份 `int` 的 ——// 它**一次都不会错**,见解析页第 ⑥ 步。#include <bits/stdc++.h>using namespace std;
static const int NEG = INT_MIN / 2;static int n, q;static vector<int> g[30005];static int w[30005];static int fa[30005], dep[30005], siz[30005], son[30005];static int top_[30005], dfn[30005], rnk[30005], bfsOrder[30005], cnt;
/* ---------- 线段树:单点赋值 + 区间求和 / 区间最大值(没有懒标记) ---------- */static long long sum_[120005];static int mx_[120005];static void pull(int p) { sum_[p] = sum_[p << 1] + sum_[p << 1 | 1]; mx_[p] = max(mx_[p << 1], mx_[p << 1 | 1]);}static void build(int p, int l, int r) { if (l == r) { sum_[p] = w[rnk[l]]; mx_[p] = w[rnk[l]]; return; } int mid = (l + r) >> 1; build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r); pull(p);}static void setAt(int p, int l, int r, int pos, int v) { if (l == r) { sum_[p] = v; mx_[p] = v; return; } int mid = (l + r) >> 1; if (pos <= mid) setAt(p << 1, l, mid, pos, v); else setAt(p << 1 | 1, mid + 1, r, pos, v); pull(p);}static long long qSum(int p, int l, int r, int L, int R) { if (L <= l && r <= R) return sum_[p]; int mid = (l + r) >> 1; long long s = 0; if (L <= mid) s += qSum(p << 1, l, mid, L, R); if (R > mid) s += qSum(p << 1 | 1, mid + 1, r, L, R); return s;}static int qMax(int p, int l, int r, int L, int R) { if (L <= l && r <= R) return mx_[p]; int mid = (l + r) >> 1; int s = NEG; if (L <= mid) s = max(s, qMax(p << 1, l, mid, L, R)); if (R > mid) s = max(s, qMax(p << 1 | 1, mid + 1, r, L, R)); return s;}
/* ---------- 两趟遍历(都不递归;顶格 n = 3×10⁴ 的链递归也危险,习惯别改) ---------- */static void pass1() { int head = 0, tail = 0; bfsOrder[tail++] = 1; fa[1] = 0; dep[1] = 1; while (head < tail) { int u = bfsOrder[head++]; for (int v : g[u]) if (v != fa[u]) { fa[v] = u; dep[v] = dep[u] + 1; bfsOrder[tail++] = v; } } for (int i = 1; i <= n; i++) { siz[i] = 1; son[i] = 0; } for (int i = n - 1; i >= 0; i--) { int u = bfsOrder[i], p = fa[u]; if (!p) continue; siz[p] += siz[u]; if (!son[p] || siz[u] > siz[son[p]]) son[p] = u; }}static void pass2() { static int st[30005]; int sp = 0; cnt = 0; top_[1] = 1; st[sp++] = 1; while (sp) { int u = st[--sp]; dfn[u] = ++cnt; rnk[cnt] = u; for (int v : g[u]) if (v != fa[u] && v != son[u]) { top_[v] = v; st[sp++] = v; } if (son[u]) { top_[son[u]] = top_[u]; st[sp++] = son[u]; } }}
/* ---------- 路径:和「区间加」那道题一模一样的跳法 ---------- */static long long pathSum(int x, int y) { long long s = 0; while (top_[x] != top_[y]) { if (dep[top_[x]] < dep[top_[y]]) swap(x, y); s += qSum(1, 1, n, dfn[top_[x]], dfn[x]); x = fa[top_[x]]; } if (dep[x] > dep[y]) swap(x, y); return s + qSum(1, 1, n, dfn[x], dfn[y]); // ★ 端点也算:没有 +1}static int pathMax(int x, int y) { int s = NEG; // ★★ 幺元是 −∞,不是 0 while (top_[x] != top_[y]) { if (dep[top_[x]] < dep[top_[y]]) swap(x, y); s = max(s, qMax(1, 1, n, dfn[top_[x]], dfn[x])); x = fa[top_[x]]; } if (dep[x] > dep[y]) swap(x, y); return max(s, qMax(1, 1, n, dfn[x], dfn[y]));}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); if (!(cin >> n)) return 0; for (int i = 1; i < n; i++) { int a, b; cin >> a >> b; g[a].push_back(b); g[b].push_back(a); } for (int i = 1; i <= n; i++) cin >> w[i]; // ⚠ 权值在边之后,和 P3384 相反 pass1(); pass2(); build(1, 1, n);
cin >> q; string op; for (int i = 0; i < q; i++) { int a, b; cin >> op >> a >> b; if (op == "CHANGE") setAt(1, 1, n, dfn[a], b); // ★ 赋值,不是加 else if (op == "QMAX") cout << pathMax(a, b) << '\n'; else cout << pathSum(a, b) << '\n'; } return 0;}点「运行 ▶」看结果
static const int NEG = INT_MIN / 2; <- ① 幺元。不是 0
int s = NEG; <- ② 跳链时那个累加器的初值
return max(s, qMax(1, 1, n, dfn[x], dfn[y]));
^^^^^^ <- ③ 没有 +1:点权版要算 LCA 那一格★ 第 ③ 行是本章第 12 步那个边权版的反面 ——
那儿写的是 dfn[x] + 1(跳过 LCA),因为边比点少一个。
⇒ 而这道题问的是点权,题面还专门写了一句「路径上的节点包括 u 和 v 本身」。
⚠ 同一张题单里,P4315 非 +1 不可、这道题 +1 就死。
4★ 三个错法 × 六个档位 —— 两条「触发 ≡ 抓获」逐格相同
// P2590 的「触发条件计数器」:读一组数据,打一行 CSV//// negmax = 1 ⟺ 存在一次 QMAX,它那条路径上的最大值 **< 0**(①MaxZero 的触发条件)// chgnz = 1 ⟺ 存在一次 `CHANGE u t` 且**当时** `w[u] ≠ 0`(②ChangeAdd 的第一层)// chgseen = 1 ⟺ 上面那种 CHANGE 之后,还有一次询问真的碰到了那个 u(第二层)// lcadiff = 1 ⟺ 存在一次询问,「漏掉浅的那个端点」会让答案变(③SkipLca 的触发条件)// maxabs = 所有 QSUM 答案绝对值的最大值(④Int 的那条线:≥ 2³¹ 才可能烂)//// ★ 全部拿**暴力那条路**算,和被测的剖分代码一行都不共享。#include <bits/stdc++.h>using namespace std;static int n, q;static vector<int> g[30005];static int w[30005], fa[30005], dep[30005], bfsOrder[30005];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); if (!(cin >> n)) return 0; for (int i = 1; i < n; i++) { int a, b; cin >> a >> b; g[a].push_back(b); g[b].push_back(a); } for (int i = 1; i <= n; i++) cin >> w[i]; int head = 0, tail = 0; bfsOrder[tail++] = 1; fa[1] = 0; dep[1] = 1; while (head < tail) { int u = bfsOrder[head++]; for (int v : g[u]) if (v != fa[u]) { fa[v] = u; dep[v] = dep[u] + 1; bfsOrder[tail++] = v; } } cin >> q; int negmax = 0, chgnz = 0, chgseen = 0, lcadiff = 0; long long maxabs = 0; vector<int> dirty(n + 1, 0); string op; for (int i = 0; i < q; i++) { int a, b; cin >> op >> a >> b; if (op == "CHANGE") { if (w[a] != 0) { chgnz = 1; dirty[a] = 1; } w[a] = b; continue; } /* 走一遍路径,顺手把「路径上有哪些点」「谁是 LCA」都收下来 */ vector<int> pts; int x = a, y = b; while (dep[x] > dep[y]) { pts.push_back(x); x = fa[x]; } while (dep[y] > dep[x]) { pts.push_back(y); y = fa[y]; } while (x != y) { pts.push_back(x); pts.push_back(y); x = fa[x]; y = fa[y]; } int lca = x; long long s = 0; int mx = INT_MIN / 2; for (int p : pts) { s += w[p]; mx = max(mx, w[p]); } long long sNo = s; int mxNo = mx; // 不含 LCA 的那一版 s += w[lca]; mx = max(mx, w[lca]); for (int p : pts) if (dirty[p]) chgseen = 1; if (dirty[lca]) chgseen = 1; if (op == "QMAX") { if (mx < 0) negmax = 1; if (mx != (pts.empty() ? INT_MIN / 2 : mxNo)) lcadiff = 1; } else { maxabs = max(maxabs, s < 0 ? -s : s); if (s != sNo) lcadiff = 1; } } printf("negmax,%d\nchgnz,%d\nchgseen,%d\nlcadiff,%d\nmaxabs,%lld\n", negmax, chgnz, chgseen, lcadiff, maxabs); return 0;}点「运行 ▶」看结果
| 档位 | ①MaxZero | ②ChangeAdd | ③SkipLca | ④Int | 试金石 |
|---|---|---|---|---|---|
| 0 ★ 顺手写法:权值一律非负 | ★ 0 | 159 | 269 | 0 | 283 |
| 1 照题面(含负数) | 95 | 147 | 271 | 0 | 283 |
| 2 权值全为负 | 245 | 146 | 276 | 0 | 283 |
| 3 一次 CHANGE 都不出 | 123 | ★ 0 | 294 | 0 | 300 |
| 4 最终档 | 131 | 278 | 300 | 0 | 300 |
| 5 权值全为 0 | ⚠ 0 | 0 | 109 | 0 | ⚠ 0 |
★★★ ①MaxZero 那一档 0 就是这一页最该记的一格:
档 0 是所有人都会顺手写的那个生成器 —— 权值取 [0, 100]。
而题面第一句数据范围就写着权值可以到 −3×10⁴。
⇒ 一个顺手的默认值,把这一页的主线错法整个测没了。
⇒ 「生成器该照抄题面的比值 / 范围,而不是你觉得顺眼的那个」的又一次。
⚠ 而档 5 那一行有意思:正确答案恒等于 0 ⇒ 试金石满分、三个错法全 0 ——
只有 ③SkipLca 还活着(109)。因为它错的方式不是「算小了」,
是在 u = v 的 QMAX 上把区间跳成了空的,于是打出那个 −∞ 哨兵。
⇒ ★★ 「一整档在验零」也不保证所有错法都瞎掉,主语是「它错的方式」。
| 第一层(触发条件) | 抓获 | ||
|---|---|---|---|
| ①MaxZero | 存在一次 QMAX,路径上的最大值 < 0:0 / 95 / 245 / 123 / 131 / 0 |
0 / 95 / 245 / 123 / 131 / 0 | ★★★ 一个不差 |
| ③SkipLca | 存在一次询问,「漏掉浅的那个端点」会让答案变:269 / 271 / 276 / 294 / 300 / 109 | 269 / 271 / 276 / 294 / 300 / 109 | ★★★ 一个不差 |
| ②ChangeAdd | 存在一次 CHANGE u t 且当时 w[u] ≠ 0,而且之后有询问碰到 u:163 / 164 / 164 / 0 / 282 / 0 |
159 / 147 / 146 / 0 / 278 / 0 | ⚠ 差 1.03~1.12 倍 |
★★ 前两条能写成 ≡,是因为它们的触发条件是一句能证的等价:
① max 的幺元多塞了一个 0 ⟺ 真实最大值 < 0 时答案变成 0;
③ 漏掉浅端点 ⟺ 那个点的权值影响到了答案。
⚠ 而 ②ChangeAdd 写不成 —— 第三层没写出来:
w[u] += t 和 w[u] = t 巧合相等(w[u] 原来是 0),
或者那次 CHANGE 之后 u 又被另一次 CHANGE 重新写过。
⇒ 「能不能写成 ≡,取决于你第一层写到第几格」,这一页三条各演了一次。
| 样例上的输出 | ||
|---|---|---|
| ★ 正解 | 4 1 2 2 10 6 5 6 5 16 |
|
| ①MaxZero | 4 1 2 2 10 6 5 6 5 16 |
✗ 放过 —— 样例权值 4 2 1 3 全是正的 |
| ②ChangeAdd | 4 1 2 2 10 6 9 9 9 21 |
✓ 挡住了 |
| ③SkipLca | 3 −1073741824 1 1 6 2 3 6 3 11 |
✓ 挡住了(而且第一个数就不对) |
⇒ ★★ 而 ①MaxZero 被放过的原因和顺手写的生成器放过它的原因一模一样: 两边都只造了非负的权值。 ⇒ 这一次不是「样例和对拍互补」(第 50 章 P8306 那种), 是样例和对拍一起瞎。只有把题面那半行数据范围读进去才救得回来。
5★ 「全程 int 一次都不会错」—— 而这个精确的 0 配了自检
六档 1800 轮,int 那一版和正解逐字节全同。而它凭什么对,是一句乘法:
路径最多 n = 3×10⁴ 个点,|w| ≤ 3×10⁴
⇒ |路径和| ≤ 9×10⁸ ,而 int 上限是 2 147 483 647
⇒ 余量 2.39 倍⚠⚠ 而这个「精确的 0」配了自检 —— 把权值上界抬到题面之外,拿真程序两侧各跑一遍:
全树权值 w |
30000 × w | ★ 正解 | ✗ int 版 |
|---|---|---|---|
| 71581 | 2 147 430 000 | 2147430000 | 2147430000 |
| 71582 | 2 147 460 000 | 2147460000 | ★ 2147460000 |
| 71583 | 2 147 490 000 | 2147490000 | ⚠ −2147477296 |
| 71584 | 2 147 520 000 | 2147520000 | −2147447296 |
⇒ ★★ 那条线精确在 w = 71583,而题面给的上限是 30000 —— 差 2.386 倍。
⇒ 结论不是「可以写 int」,是「这道题的余量算得出来,而且只有 2.4 倍」;
正解仍然写 long long,因为代价是零。
(⚠ 对拍六档里最大的那个 |QSUM| 只有 142 682 —— 离 2³¹ 差 15 051 倍,对拍原理上够不着。)
6★ 哪一版就已经能过了
| 版本 | 能过吗 | 数字(顶格最坏形状) |
|---|---|---|
| ✗ 照题面爬 | ✗ | 6.01 秒 / 时限 1 秒(⚠ 但顶格随机只要 0.03 秒) |
✗ max 的幺元写成 0 |
✗ | 只要路径最大值是负的就错 |
✗ CHANGE 当成加 |
✗ | — |
✗ 照抄边权版那个 +1 |
✗ | — |
★ 全程 int |
✓ | 一次都不会错(余量 2.39 倍) |
| ★ 正解 | ✓ | 0.15 秒(最慢那一档)、峰值 7.5 MiB / 125 MiB |
⇒ 这道题的三件功课:
① 幺元是 −∞,不是 0;② CHANGE 是赋值;③ 点权版不跳 LCA。
「换一种线段树」听着像换零件,其实要重新想的只有一件事:什么都没有的时候该返回什么。
⇒ 求和的幺元是 0,所以 P3384 上这件事根本不用想; 求最大值的幺元是 −∞,而这道题的权值真的会是负的。 ⇒ ★★ 而顺手写的生成器、和出题人给的那组样例,在这一格上一起瞎了 —— 救得回来的只有题面那半行数据范围。