又一树上异或和 - 题解
标签与难度
标签: 树上问题, LCA, 线性基, 异或, 图论, 位运算 难度: 2200
题目大意喵~
你好呀,指挥官!这道题是关于一棵可爱的树的旅行计划哦,喵~
我们有一棵 个节点的树,每个节点 都有一个“乐趣值” 。现在呢,我们要在这棵树上从一个起点 走到一个终点 。
这个走路的方式很自由哦!我们可以随便走,可以经过一个点好几次,也可以走过一条边再原路返回。一条路径的“总乐趣值”就是把路径上经过的所有点的乐趣值(经过几次就算几次)全部异或起来。
现在有 次询问,每次会给我们一个起点 和一个终点 。我们需要找出所有从 到 的路径中,能得到的最大的“总乐趣值”是多少,呐。
解题思路分析
这道题看起来有点复杂,路径可以随便走,感觉可能性无穷无尽,让人头晕晕的,喵~ 但是不要怕,让我来帮你理清思路吧!
路径的秘密
首先我们来分析一下,从起点 到终点 的任意一条路径,它和那条唯一的简单路径(就是不走回头路、不绕圈的最短路径)之间有什么关系呢?
任何一条复杂的路径,都可以看作是在简单路径的基础上,增加了一些“绕路”或者叫“兜圈子”的行为。比如说,我们走到一个点 的时候,没有直接走向下一个目标,而是先跑去它的一个邻居 ,然后再立刻跑回 。
这个 x -> y -> x 的小 detour 会给我们的路径序列增加 y, x 两个节点。那么,总的乐趣值就会额外异或上 。
如果我们多绕几圈呢?比如 x -> y -> x -> y -> x,那总乐趣值就额外异或了 。根据异或的性质,,所以绕偶数次等于没绕,绕奇数次等于绕一次!
这意味着,对于树上的任意一条边 ,我们都可以通过在 之间来回走动,来自由地选择要不要让我们的总乐趣值异或上 。这个能力我们对树上所有的边都拥有!
核心转化
所以,任意一条从 到 的路径,它的总乐趣值都可以表示成:
这里的 是什么呢?它是由我们自己选择的、树上若干条边的端点值异或和(即 )再进行异或得到的结果。
我们的目标是让“总乐趣值”最大化。这下问题就清晰多啦!变成了一个经典的问题:给定一个初始值 base(就是简单路径的乐趣值)和一堆可以自由选择的数(就是所有边的 ),求 base 和这些数中任意子集的异或和的最大值。
这不就是线性基的拿手好戏嘛!喵~
我们的作战计划
所以,解题的步骤就出来啦:
构建线性基: 我们遍历树上的每一条边 ,计算出 ,然后把这个值插入到我们的线性基里。这样,我们就构建了一个可以生成所有可能的“绕路”异或值的“魔法空间”。
计算简单路径的乐趣值: 对于每次询问 ,我们需要计算出从 到 的简单路径上所有节点的乐趣值异或和。这是一个树上路径问题,用 LCA (最近公共祖先) 来解决最方便啦。
- 我们可以先以某个节点(比如 1)为根,做一次 DFS(深度优先搜索),预处理出每个节点 到根节点的路径乐趣值
path_xor[i]。 - 和 的简单路径,就是从 走到它们的LCA,再从LCA走到 。
- 利用预处理的
path_xor,简单路径的乐趣值可以这样算:为什么最后要异或LCA的值呢?因为在 path_xor[u] 和 path_xor[v] 中,从根到LCA的路径被计算了两次,正好抵消了。而LCA这个点本身,在 的路径上只出现一次,但它在 path_xor[u] 和 path_xor[v] 中各包含一次,也被抵消了。所以我们要把它加回来,喵~
- 我们可以先以某个节点(比如 1)为根,做一次 DFS(深度优先搜索),预处理出每个节点 到根节点的路径乐趣值
查询最大值: 我们把第二步算出来的
SimplePathXOR作为初始值,然后去问我们第一步建好的线性基:“嘿,我拿着这个值,再异或上你能产生的某个值,能得到的最大结果是多少呀?” 线性基会用一个贪心算法,从高位到低位检查,如果异或上基里的某个向量能让结果变大,就异或它。最后得到的就是答案啦!
把这些组合起来,我们就能漂亮地解决这个问题了,呐!
代码实现
下面是我根据这个思路,精心为你准备的代码哦~ 每个细节都写得很清楚,希望能帮到你!
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;
// 使用32位无符号整数来存放乐趣值和异或和
using uint = unsigned int;
const int MAXN = 100005;
const int LOGN = 18; // log2(100005) 大约是 16.6, 18 足够了
// --- 数据结构定义 ---
vector<int> adj[MAXN]; // 邻接表存树
uint node_values[MAXN]; // 节点乐趣值
// 用于LCA和路径查询的数组
int parent[MAXN][LOGN]; // parent[i][k] 是 i 的第 2^k 个祖先
int depth[MAXN]; // 节点的深度
uint path_xor_from_root[MAXN]; // 从根到当前节点的路径乐趣异或和
vector<pair<int, int>> edges; // 存储所有边,用于建线性基
// --- 线性基 ---
struct LinearBasis {
vector<uint> basis;
int max_bits;
LinearBasis(int bits = 32) : max_bits(bits) {
basis.assign(max_bits, 0);
}
// 插入一个值
void insert(uint val) {
for (int i = max_bits - 1; i >= 0; --i) {
if (!((val >> i) & 1)) continue; // 只关心为1的位
if (basis[i] == 0) {
basis[i] = val;
return;
}
val ^= basis[i];
}
}
// 查询与初始值val异或能得到的最大值
uint query_max(uint val) const {
for (int i = max_bits - 1; i >= 0; --i) {
// 贪心:如果异或上这个基向量能使结果变大,就异或
if ((val ^ basis[i]) > val) {
val ^= basis[i];
}
}
return val;
}
};
// --- 树上算法 ---
// DFS预处理深度、父节点和到根的路径异或和
void dfs(int u, int p, int d, uint current_path_xor) {
depth[u] = d;
parent[u][0] = p;
path_xor_from_root[u] = current_path_xor;
for (int v : adj[u]) {
if (v != p) {
dfs(v, u, d + 1, current_path_xor ^ node_values[v]);
}
}
}
// 预处理LCA的倍增数组
void preprocess_lca(int n) {
for (int k = 1; k < LOGN; ++k) {
for (int i = 1; i <= n; ++i) {
if (parent[i][k - 1] != 0) {
parent[i][k] = parent[parent[i][k - 1]][k - 1];
}
}
}
}
// 查询u和v的LCA
int lca(int u, int v) {
if (depth[u] < depth[v]) swap(u, v);
for (int k = LOGN - 1; k >= 0; --k) {
if (parent[u][k] != 0 && depth[parent[u][k]] >= depth[v]) {
u = parent[u][k];
}
}
if (u == v) return u;
for (int k = LOGN - 1; k >= 0; --k) {
if (parent[u][k] != 0 && parent[v][k] != 0 && parent[u][k] != parent[v][k]) {
u = parent[u][k];
v = parent[v][k];
}
}
return parent[u][0];
}
int main() {
// 加速输入输出,喵~
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, q;
cin >> n >> q;
for (int i = 1; i <= n; ++i) {
cin >> node_values[i];
}
for (int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
edges.push_back({u, v});
}
// 1. 预处理
// 从根节点1开始DFS
dfs(1, 0, 1, node_values[1]);
// 构建LCA的倍增表
preprocess_lca(n);
// 2. 构建线性基
LinearBasis lb(32);
for (const auto& edge : edges) {
uint xor_val = node_values[edge.first] ^ node_values[edge.second];
lb.insert(xor_val);
}
// 3. 处理查询
while (q--) {
int u, v;
cin >> u >> v;
// 计算LCA
int ancestor = lca(u, v);
// 计算u到v的简单路径异或和
uint simple_path_xor = path_xor_from_root[u] ^ path_xor_from_root[v] ^ node_values[ancestor];
// 查询最大值
uint max_fun_value = lb.query_max(simple_path_xor);
cout << max_fun_value << "\n";
}
return 0;
}复杂度分析
时间复杂度:
- DFS 预处理是 的说。
- LCA 的倍增预处理需要 。
- 建线性基时,我们有 条边,每次插入最多花费 的时间( 是乐趣值的最大范围,这里是 ,所以 是常数32)。总共是 。
- 每次查询,计算 LCA 需要 ,线性基查询需要 。所以 次查询总共是 。
- 合起来就是 啦!
空间复杂度:
- 邻接表占 空间。
- LCA 的
parent数组是空间大头,需要 。 depth,path_xor_from_root等数组都是 。- 线性基本身很小,只需要 。
- 所以总空间复杂度由LCA的倍增表决定,是 ,喵~
知识点总结
这道题是好几个经典算法的有趣组合,我们可以从中学到很多东西呢!
- 问题转化: 最关键的一步!将“任意路径”问题转化为“简单路径 + 自由组合的环”问题。理解到任意 detour
x -> y -> x的效果是异或上 ,是解题的突破口。 - 线性基: 解决子集异或和问题的超级神器!当你需要在一堆数中选一个子集,让它们的异或和与某个初始值再异或后最大/最小时,第一个就该想到它,喵!
- LCA (最近公共祖先): 树上路径问题的万能工具。无论是求距离、判断祖孙关系,还是像本题一样求路径上的信息(和、异或和、最大值等),LCA 都能派上大用场。倍增法是实现LCA的常用且高效的方法。
- 树上路径信息计算:
path_info[u] \oplus path_info[v] \oplus node_info[lca]这个公式要记牢哦,它是利用差分思想计算树上两点间路径信息的标准技巧。
希望这篇题解能让你彻底明白这道题的奥秘!继续加油哦,指挥官!喵~