0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2420,日期见页头。两边不一致时信原站。
题目描述
异或是一种神奇的运算,大部分人把它总结成不进位加法。
在生活中 xor 运算也很常见。比如,对于一个问题的回答,是为 1,否为 0,那么:
(A 是否是男生)xor(B 是否是男生)= A 和 B 是否能够成为情侣。
好了,现在我们来制造和处理一些复杂的情况。比如我们将给出一颗树,它很高兴自己有 N 个结点。
树的每条边上有一个权值。我们要进行 M 次询问,对于每次询问,
我们想知道某两点之间的路径上所有边权的异或值。
输入格式
输入文件第一行包含一个整数 N,表示这颗开心的树拥有的结点数,
以下有 N−1 行,描述这些边,每行有 3 个数,u, v, w,表示 u 和 v 之间有一条权值为 w 的边。
接下来一行有一个整数 M,表示询问数。之后的 M 行,每行两个数 u, v,
表示询问这两个点之间的路径上的权值异或值。
输出格式
输出 M 行,每行一个整数,表示异或值。
数据范围
- 对于 40% 的数据,有
1 ≤ N, M ≤ 3000; - 对于 100% 的数据,有
1 ≤ N, M ≤ 100000。
保证边权在 int 范围内。
时限 1 秒,内存 128000 KB(125 MiB)。
输入输出样例
输入
5 1 4 9644 2 5 15004 3 1 14635 5 3 9684 3 2 4 5 4 1 1
输出
975 14675 0
★ 注意第三个询问是 1 1 —— 题面允许问同一个点,答案是 0。
1★★★ 这一页的全部功课:先想清楚这道题要不要 LCA
先一遍 BFS 求出「根到每个点的路径异或和」d[u],那么
路径(u, v) 的异或和 = d[u] ^ d[v]道理:d[u] ^ d[v] 里,根到 LCA 那一段被算了两遍,而 x ^ x = 0 —— 自己抵消掉了。
⇒ 不用求 LCA、不用建倍增表、查询是 O(1),u == v 时天然给 0(一个特判都不用写)。
★ 而这道题被放进第 51 章的题单,恰恰是为了让你先问这一句: 题单注解自己写着「练『先想清楚要不要 LCA』」。
// P2420 让我们异或吧 —— 正解:**连 LCA 都不用**//// ★★★ 一句话:先一遍 BFS 求出「根到每个点的路径异或和」`d[u]`,那么//// 路径(u,v) 的异或和 = d[u] ^ d[v]//// 道理:`d[u] ^ d[v]` 里,**根到 LCA 那一段被算了两遍**,而 `x ^ x = 0` —— 自己抵消掉了。// ⇒ **不用求 LCA,不用建倍增表,查询是 O(1)。**//// ⚠⚠ 而这正是[第 52 章树上差分](/ch/52-tree-diff/)那条「上一章的正确写法就是这一章的 bug」// 的反面:路径**和**是 `s[u] + s[v] − 2·s[lca]`(要减两遍),// 路径**异或**是 `d[u] ^ d[v]`(**一遍都不用减**)。// ⇒ 照猫画虎写成 `d[u] ^ d[v] ^ d[lca]` 就把那一段又加了回去 —— 和 [P4551](/sol/p4551/) 同一个坑。//// ★ 这道题被放进[第 51 章](/ch/51-lca/)的题单,恰恰是为了让你先问一句:**这题到底要不要 LCA。**
#include <bits/stdc++.h>using namespace std;
const int MAXN = 100005;int head_[MAXN], nxt_[MAXN * 2], to_[MAXN * 2], wt_[MAXN * 2], ecnt;int d[MAXN];int n, m;
inline void addEdge(int u, int v, int w) { to_[++ecnt] = v; wt_[ecnt] = w; nxt_[ecnt] = head_[u]; head_[u] = ecnt; }
int main() { if (scanf("%d", &n) != 1) return 0; for (int i = 0; i < n - 1; i++) { int u, v, w; if (scanf("%d %d %d", &u, &v, &w) != 3) return 0; addEdge(u, v, w); addEdge(v, u, w); // ⚠ 无向边,两边都要存 } { // BFS 一趟求 d(⚠ 别用递归,链有 10⁵ 层) vector<char> vis(n + 1, 0); vector<int> que; que.reserve(n); d[1] = 0; vis[1] = 1; que.push_back(1); for (size_t i = 0; i < que.size(); i++) { int u = que[i]; for (int e = head_[u]; e; e = nxt_[e]) { int v = to_[e]; if (vis[v]) continue; vis[v] = 1; d[v] = d[u] ^ wt_[e]; que.push_back(v); } } } if (scanf("%d", &m) != 1) return 0; for (int i = 0; i < m; i++) { int u, v; if (scanf("%d %d", &u, &v) != 2) return 0; printf("%d\n", d[u] ^ d[v]); // ★ u == v 时天然是 0,一个特判都不用写 } return 0;}点「运行 ▶」看结果
2★ 那「老老实实求 LCA」白费了多少
| 耗时 | 峰值内存 | |
|---|---|---|
★ 正解(d[u] ^ d[v]) |
0.02 秒 | 7.4 MiB |
| ★ 倍增求 LCA 再拼 | 0.03 秒 | ⚠ 14.3 MiB |
⇒ 两版在四种顶格形状上逐字节相同(rand / chain / star / binary)。
★ 而多出来的 6.9 MiB 几乎全是那张表:up[100005][17] × 4 字节 = 6.8 MB = 6.5 MiB。
⚠ 时间上只差 1.5 倍 —— 因为这道题的 N 只有 10⁵,那一趟建表才 10⁵ × 17 步。
⇒ ★★ 所以「省了多少」这句话在这道题上要老实说:省的是一张表和一段代码,
不是分数线。 真正的分数线在下一步。
3⚠ 真正的分数线:第一版沿着树走
顶格 N = M = 10⁵ |
✗ 沿着树走 | ★ 正解 |
|---|---|---|
rand(随机树,深度 log 级) |
0.04 秒 | 0.02 |
star |
0.02 秒 | 0.02 |
binary |
0.03 秒 | 0.02 |
chain(一条链) |
⚠ 22.82 秒 | 0.02 |
⇒ 同样顶格,只换树的形状,第一版从 0.04 秒变成 22.82 秒(差 570 倍)。 ★ 这就是第 51 章第 3 步那句「随机树的深度只有 log 级」—— 这一章的题单里,它已经连着第三道题决定生死了(P3379 / P5903 / 这道题)。
★ 而题面 40% 档(N, M ≤ 3000)上第一版秒过(链上 0.00 秒)⇒ 暴力稳拿 40 分。
4★★★ 三个错法,而第一个是「上一章的正确写法」
第 52 章树上差分的路径和是
s[u] + s[v] − 2·s[lca] ← 根到 lca 那一段被加了两遍,所以要减两遍到了异或这儿,很多人顺手写成「异或一遍 lca」。可异或的抵消是自动的:
d[u] ^ d[v] 里那一段已经没了;再异或一次 d[lca],等于把它又加了回去。
⚠⚠ 而这道题的官方样例一个字都挡不住它 —— 那三个询问(2 4 / 5 4 / 1 1)的
LCA 全都是根 1,而 d[根] = 0 ⇒ 多异或的那一下正好是 0。
⇒ ★★ 这和 P4551 是同一个坑的第二次,而那道题上官方样例也是这么放过它的 (最优那一对的 LCA 正好是根)。两道题、两组样例、同一个理由。
只 addEdge(u, v, w) 不 addEdge(v, u, w),BFS 就只能顺着输入给的方向走。
可顺手写的生成器是这样造边的:
for (int i = 2; i <= n; i++) printf("%d %d %d\n", fa[i], i, w);—— 每条边都是「父亲写在前、儿子写在后」,正好就是 BFS 要走的方向
⇒ 那一档它是结构性的精确的 0。
★ 而只要在生成器里加一句「随机决定写成 (u,v) 还是 (v,u)」,同一列当场 300 / 300。
5★ 对拍:三个错法 + 一份「一律输出 0」
| 档位 | ①Sub | ②Sum | ③One | 试金石 |
|---|---|---|---|---|
0 顺手写法(边按 (fa[i], i) 给、编号不打乱) |
294 | 300 | ★ 0 | 300 |
| 1 + 每条边随机决定写成 (u,v) 还是 (v,u) | 297 | 300 | ★ 300 | 300 |
| 2 + 询问取「同一棵子树里的两个点」(LCA 不是根) | 300 | 234 | 0 | 234 |
| 3 最终档 = 1 + 2 + 编号打乱 | 300 | 229 | 225 | 229 |
★ ③One 那两个 0 → 300 的跳变,是这张表最值钱的一格:
改的不是数据规模,是「边写下来的顺序」 —— 一句 if (rng() & 1) swap(a, b);。
⚠ 而档 2 / 3 里试金石从 300 掉到 229 是有原因的:那两档的询问是「从同一棵子树里往下走三步」取两个点,
两个点很容易走成同一个 ⇒ 答案本来就是 0。
★ ②Sum 跟着一起掉到 229(u == v 时它也给 0)——
⇒ 两列一起掉,说明掉的不是抓获能力,是「这一档有多少个询问在问真问题」。
6★ 哪一版就已经能过了
| 版本 | 能过吗 | 数字(顶格最坏形状) |
|---|---|---|
| ✗ 沿着树走 | 40 分 | chain 22.82 秒 / 时限 1 秒 |
| ★ 倍增求 LCA 再拼 | ✓ | 0.03 秒、14.3 MiB |
★ 正解(d[u] ^ d[v]) |
✓ | 0.02 秒、7.4 MiB、代码短一半 |
⇒ ★★★ 这道题不是「倍增题」,是「先想清楚要不要倍增」题。 而想清楚之后的收获很具体:少一张 6.9 MiB 的表、少一段 30 行的代码、 少一个「LCA 那一下到底该异或几遍」的坑。