Skip to content

又一树上异或和 - 题解

标签与难度

标签: 树上问题, LCA, 线性基, 异或, 图论, 位运算 难度: 2200

题目大意喵~

你好呀,指挥官!这道题是关于一棵可爱的树的旅行计划哦,喵~

我们有一棵 nn 个节点的树,每个节点 ii 都有一个“乐趣值” aia_i。现在呢,我们要在这棵树上从一个起点 uu 走到一个终点 vv

这个走路的方式很自由哦!我们可以随便走,可以经过一个点好几次,也可以走过一条边再原路返回。一条路径的“总乐趣值”就是把路径上经过的所有点的乐趣值(经过几次就算几次)全部异或起来。

现在有 qq 次询问,每次会给我们一个起点 uu 和一个终点 vv。我们需要找出所有从 uuvv 的路径中,能得到的最大的“总乐趣值”是多少,呐。

解题思路分析

这道题看起来有点复杂,路径可以随便走,感觉可能性无穷无尽,让人头晕晕的,喵~ 但是不要怕,让我来帮你理清思路吧!

路径的秘密

首先我们来分析一下,从起点 uu 到终点 vv 的任意一条路径,它和那条唯一的简单路径(就是不走回头路、不绕圈的最短路径)之间有什么关系呢?

任何一条复杂的路径,都可以看作是在简单路径的基础上,增加了一些“绕路”或者叫“兜圈子”的行为。比如说,我们走到一个点 xx 的时候,没有直接走向下一个目标,而是先跑去它的一个邻居 yy,然后再立刻跑回 xx

这个 x -> y -> x 的小 detour 会给我们的路径序列增加 y, x 两个节点。那么,总的乐趣值就会额外异或上 ayaxa_y \oplus a_x

如果我们多绕几圈呢?比如 x -> y -> x -> y -> x,那总乐趣值就额外异或了 (ayax)(ayax)(a_y \oplus a_x) \oplus (a_y \oplus a_x)。根据异或的性质,kk=0k \oplus k = 0,所以绕偶数次等于没绕,绕奇数次等于绕一次!

这意味着,对于树上的任意一条边 (x,y)(x, y),我们都可以通过在 x,yx, y 之间来回走动,来自由地选择要不要让我们的总乐趣值异或上 axaya_x \oplus a_y。这个能力我们对树上所有的边都拥有!

核心转化

所以,任意一条从 uuvv 的路径,它的总乐趣值都可以表示成:

总乐趣值=(u到v简单路径的乐趣值)C\text{总乐趣值} = (\text{u到v简单路径的乐趣值}) \oplus C

这里的 CC 是什么呢?它是由我们自己选择的、树上若干条边的端点值异或和(即 aiaja_i \oplus a_j)再进行异或得到的结果。

我们的目标是让“总乐趣值”最大化。这下问题就清晰多啦!变成了一个经典的问题:给定一个初始值 base(就是简单路径的乐趣值)和一堆可以自由选择的数(就是所有边的 aiaja_i \oplus a_j),求 base 和这些数中任意子集的异或和的最大值。

这不就是线性基的拿手好戏嘛!喵~

我们的作战计划

所以,解题的步骤就出来啦:

  1. 构建线性基: 我们遍历树上的每一条边 (i,j)(i, j),计算出 aiaja_i \oplus a_j,然后把这个值插入到我们的线性基里。这样,我们就构建了一个可以生成所有可能的“绕路”异或值的“魔法空间”。

  2. 计算简单路径的乐趣值: 对于每次询问 (u,v)(u, v),我们需要计算出从 uuvv 的简单路径上所有节点的乐趣值异或和。这是一个树上路径问题,用 LCA (最近公共祖先) 来解决最方便啦。

    • 我们可以先以某个节点(比如 1)为根,做一次 DFS(深度优先搜索),预处理出每个节点 ii 到根节点的路径乐趣值 path_xor[i]
    • uuvv 的简单路径,就是从 uu 走到它们的LCA,再从LCA走到 vv
    • 利用预处理的 path_xor,简单路径的乐趣值可以这样算:

      SimplePathXOR=path_xor[u]path_xor[v]alca(u,v)\text{SimplePathXOR} = \text{path\_xor}[u] \oplus \text{path\_xor}[v] \oplus a_{\text{lca}(u,v)}

      为什么最后要异或LCA的值呢?因为在 path_xor[u] 和 path_xor[v] 中,从根到LCA的路径被计算了两次,正好抵消了。而LCA这个点本身,在 uvu \to v 的路径上只出现一次,但它在 path_xor[u] 和 path_xor[v] 中各包含一次,也被抵消了。所以我们要把它加回来,喵~
  3. 查询最大值: 我们把第二步算出来的 SimplePathXOR 作为初始值,然后去问我们第一步建好的线性基:“嘿,我拿着这个值,再异或上你能产生的某个值,能得到的最大结果是多少呀?” 线性基会用一个贪心算法,从高位到低位检查,如果异或上基里的某个向量能让结果变大,就异或它。最后得到的就是答案啦!

把这些组合起来,我们就能漂亮地解决这个问题了,呐!

代码实现

下面是我根据这个思路,精心为你准备的代码哦~ 每个细节都写得很清楚,希望能帮到你!

cpp
#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;
}

复杂度分析

  • 时间复杂度: O(NlogN+NlogV+Q(logN+logV))O(N \log N + N \log V + Q(\log N + \log V))

    • DFS 预处理是 O(N)O(N) 的说。
    • LCA 的倍增预处理需要 O(NlogN)O(N \log N)
    • 建线性基时,我们有 N1N-1 条边,每次插入最多花费 O(logV)O(\log V) 的时间(VV 是乐趣值的最大范围,这里是 2322^{32},所以 logV\log V 是常数32)。总共是 O(NlogV)O(N \log V)
    • 每次查询,计算 LCA 需要 O(logN)O(\log N),线性基查询需要 O(logV)O(\log V)。所以 QQ 次查询总共是 O(Q(logN+logV))O(Q(\log N + \log V))
    • 合起来就是 O(NlogN+NlogV+Q(logN+logV))O(N \log N + N \log V + Q(\log N + \log V)) 啦!
  • 空间复杂度: O(NlogN)O(N \log N)

    • 邻接表占 O(N)O(N) 空间。
    • LCA 的 parent 数组是空间大头,需要 O(NlogN)O(N \log N)
    • depth, path_xor_from_root 等数组都是 O(N)O(N)
    • 线性基本身很小,只需要 O(logV)O(\log V)
    • 所以总空间复杂度由LCA的倍增表决定,是 O(NlogN)O(N \log N),喵~

知识点总结

这道题是好几个经典算法的有趣组合,我们可以从中学到很多东西呢!

  1. 问题转化: 最关键的一步!将“任意路径”问题转化为“简单路径 + 自由组合的环”问题。理解到任意 detour x -> y -> x 的效果是异或上 axaya_x \oplus a_y,是解题的突破口。
  2. 线性基: 解决子集异或和问题的超级神器!当你需要在一堆数中选一个子集,让它们的异或和与某个初始值再异或后最大/最小时,第一个就该想到它,喵!
  3. LCA (最近公共祖先): 树上路径问题的万能工具。无论是求距离、判断祖孙关系,还是像本题一样求路径上的信息(和、异或和、最大值等),LCA 都能派上大用场。倍增法是实现LCA的常用且高效的方法。
  4. 树上路径信息计算: path_info[u] \oplus path_info[v] \oplus node_info[lca] 这个公式要记牢哦,它是利用差分思想计算树上两点间路径信息的标准技巧。

希望这篇题解能让你彻底明白这道题的奥秘!继续加油哦,指挥官!喵~