Trie树:最大抑或对 - 实践

最大异或对问题详解

在算法难题中,最大异或对是一个经典问题:给定NNN 个整数 A1,A2,…,ANA_1, A_2, \ldots, A_NA1,A2,,AN,从中选择两个不同的数进行异或运算(记为a⊕ba \oplus bab),求可能的最大结果。
异或运算的定义为:对于每个二进制位,倘若两位相同则结果为 0,不同则结果为 1。例如,5⊕3=65 \oplus 3 = 653=6(二进制:101⊕011=110101 \oplus 011 = 110101011=110)。输入格式为:第一行是整数NNN,第二行是 NNN个整数;输出为一个整数,表示最大异或值。

问题分析与优化思路

直接使用双重循环枚举所有数对并计算异或值,时间复杂度为O(N2)O(N^2)O(N2)。当 NNN较大时(例如N≤105N \leq 10^5N105),这种暴力手段会超时。优化思路基于异或运算的特性:要最大化结果,应优先确保高位为 1。例如,结果10000000210000000_2100000002(128)比01111111201111111_2011111112(127)更大,尽管低位值相同。因此,大家需一种数据结构来高效查找与给定数字在高位上差异最大的数字。

Trie 树(前缀树)是理想的优化工具。它可以将数字表示为二进制字符串(从高位到低位存储),并在查询时优先选择相反位(0 或 1)以最大化异或结果。这样,内层循环的查询时间复杂度降至O(log⁡C)O(\log C)O(logC),其中 CCC是数字的位数(如 31 位),整体复杂度优化为O(Nlog⁡C)O(N \log C)O(NlogC)

算法思路

  1. Trie 树构建:每个数字被视为一个 31 位二进制串(从最高位开始)。Trie 树节点有两个子节点,分别代表 0 和 1。插入数字时,从高位向低位遍历,创建或更新路径。

  2. 查询过程:对于给定数字xxx,从高位开始,在 Trie 树中查找与xxx当前位相反的路径(以使异或结果为 1)。如果相反位不存在,则退而求其次选择相同位(异或结果为 0)。查询结束时,返回一个数字yyy,使得 x⊕yx \oplus yxy 尽可能大。

  3. 整体流程:遍历所有数字,先插入当前数字到 Trie 树,再查询与其异或最大的数字,更新全局最大值。注意,查询时 Trie 树已包含之前插入的数字,确保每个数对只计算一次。

算法的关键在于贪心策略:高位优先匹配,最大化结果的权重。设数字范围为[0,231)[0, 2^{31})[0,231),则最多需31×N31 \times N31×N 个节点(N≤105N \leq 10^5N105),空间复杂度可接受。

代码实现

以下是 C++ 实现,代码结构清晰,囊括详细注释:

#include <iostream>
  #include <algorithm>
    using namespace std;
    const int N = 100010; // 最大数字数量
    const int M = 3100010; // Trie 树节点数:31位 * 100000(约31e5,取310万)
    int son[M][2]; // Trie 树结构,每个节点有两个子节点(0 和 1)
    int idx; // Trie 树节点索引
    int n, a[N]; // 输入数据
    // 插入数字 x 到 Trie 树
    void insert(int x)
    {
    int p = 0; // 根节点
    for (int i = 30; i >= 0; i--)
    { // 从最高位(第31位)开始处理
    int t = (x >> i) & 1; // 提取第 i 位(0 或 1)
    if (!son[p][t]) son[p][t] = ++idx; // 若路径不存在,创建新节点
    p = son[p][t]; // 移动到子节点
    }
    // 注意:由于所有数字位数相同,无需额外标记结束节点
    }
    // 查询与 x 异或最大的数字
    int query(int x)
    {
    int p = 0;
    int res = 0; // 存储查询到的数字
    for (int i = 30; i >= 0; i--)
    {
    int t = (x >> i) & 1; // x 的第 i 位
    if (son[p][!t])
    { // 优先选择相反位(使异或结果为 1)
    p = son[p][!t];
    res = res * 2 + !t; // 更新 res 的二进制表示
    }
    else
    { // 相反位不存在,选择相同位
    p = son[p][t];
    res = res * 2 + t;
    }
    }
    return res;
    }
    int main()
    {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    int res = 0; // 全局最大异或值
    for (int i = 0; i < n; i++)
    {
    insert(a[i]); // 先插入当前数字
    int y = query(a[i]); // 查询与 a[i] 异或最大的数字
    res = max(res, a[i] ^ y); // 更新最大值
    }
    printf("%d\n", res);
    return 0;
    }

代码解释

  • Trie 树结构son[M][2] 存储节点,M 大小设为 31×10531 \times 10^531×105(约 310 万),最多有10510^5105个不同的数字,每个数字占31位,确保空间足够。
  • 插入函数insert(int x) 将数字按高位到低位分解,构建 Trie 路径。
  • 查询函数query(int x) 贪心选择路径:优先走相反位(!t),否则走相同位。res 在遍历中构建为二进制数字。
  • 主函数:遍历数组,插入后查询,更新最大异或值。时间复杂度O(N×31)O(N \times 31)O(N×31),高效处理大输入。

复杂度分析

  • 时间复杂度O(Nlog⁡C)O(N \log C)O(NlogC),其中 CCC是数字位数(31),插入和查询各O(log⁡C)O(\log C)O(logC)
  • 空间复杂度O(Nlog⁡C)O(N \log C)O(NlogC),Trie 树节点数约31×N31 \times N31×N

总结

使用 Trie 树优化最大异或对挑战,体现了贪心思想和数据结构的高效结合。该方法避免了O(N2)O(N^2)O(N2)关键。此算法可扩展到其他位运算问题,如最大与对或最小异或对。就是暴力枚举,适用于大规模数据。理解二进制表示和 Trie 树的匹配机制


算法内容来自AcWing算法基础课,感谢AcWing老师的详细讲解。

posted @ 2025-12-07 20:42  gccbuaa  阅读(3)  评论(0)    收藏  举报