The xor-longest Path(信息学奥赛一本通- P1478)

【题目描述】

原题来自:POJ 3764

给定一棵 n 个点的带权树,求树上最长的异或和路径。

【输入】

第一行一个整数 n,接下来 n−1 行每行三个整数 u,v,w,表示 u,v 之间有一条长度为 w 的边。

【输出】

输出一行一个整数,表示答案。

【输入样例】

复制代码
4
1 2 3
2 3 4
2 4 6

【输出样例】

复制代码
7

【提示】

样例解释

最长的异或和路径是 1→2→3 ,它的长度是 3⨁4=7。

注意:结点下标从 1 开始到 N。

注:x⨁y 表示 x 与 y 按位异或。

题目分析

本题要求在一棵 N 个节点(N≤100000)的带权树上,找到一条简单路径,使得路径上所有边权的异或和(XOR)最大。

常规的树上路径问题往往会让人联想到树形 DP 或点分治,但"异或"这个特殊的位运算有着自己得天独厚的数学性质。这道题的本质,其实是一场利用"异或抵消"原理,将复杂的"图论树上问题"强行降维成"纯数组匹配问题"的魔法秀。

思考过程与解题思路

这道题最核心的突破口,在于异或运算的天然属性:两个相同的数异或,结果永远为 0(即 x⊕x=0)

在一棵树中,任意两点 u 和 v 之间有且仅有一条简单路径。设 LCA 是 u 和 v 的最近公共祖先。 从几何上看,真实的物理路径可以拼接为: Path(u,v)=Path(u,LCA)+Path(LCA,v)

由于异或运算满足交换律和结合律,路径的方向根本无所谓,所以: Path(u,v)=Path(LCA,u)⊕Path(LCA,v)

如果我们在程序一开始,就随便指定一个根节点(比如节点 1),用一次 DFS 求出从根节点 1 到每一个节点 i 的异或路径和,并存入数组 D[i] 中。那么会有如下等式:

  • D[u]=Path(1,LCA)⊕Path(LCA,u)

  • D[v]=Path(1,LCA)⊕Path(LCA,v)

当我们把这两个预处理好的值异或起来:

D[u]⊕D[v]=[Path(1,LCA)⊕Path(LCA,u)]⊕[Path(1,LCA)⊕Path(LCA,v)]

重新组合:

D[u]⊕D[v]=[Path(1,LCA)⊕Path(1,LCA)]⊕[Path(LCA,u)⊕Path(LCA,v)]

因为 Path(1,LCA)⊕Path(1,LCA)=0(相同值异或为 0,公共部分在异或中被完美抵消了),所以:

D[u]⊕D[v]=0⊕Path(LCA,u)⊕Path(LCA,v)=Path(u,v)

结论诞生了:树上任意两点 u 和 v 之间的真实异或路径,完全等价于 D[u]⊕D[v]。

算法设计

经过上面的核心推导,原题被彻底降维成了一个经典问题:在包含 N 个数字的数组 D 中,选出两个数字,使它们的异或值最大。

针对这个问题,暴力枚举是 O(N^2),必定超时。最佳解法是引入 01 字典树

  1. 预处理 (DFS):以节点 1 为根遍历全树,求出所有的 D[i]。

  2. 构建字典树:将所有的 D[i] 拆解成 31 位的二进制串(从高位到低位),插入到一棵 01 字典树中。

  3. 贪心查询:遍历每一个 D[i],去字典树中寻找它的"最强异或伴侣"。

    • 贪心策略:想让异或值最大,高位必须尽量是 1。所以在字典树中往下走时,永远优先选择与当前位相反的树枝。只有当相反的树枝不存在时,才委曲求全走相同的树枝。

时空复杂度分析

  • 时间复杂度

    • DFS 遍历树:O(N)。

    • 构建字典树与查询:每个数字需要进行 31 次位运算和指针跳转,整体为 O(N×31)。

    • 总时间复杂度为 O(NlogW)(W 为值域范围),面对 10^5 的数据规模,总操作不过几百万次,毫无压力。

  • 空间复杂度

    • 链式前向星:无向图需开双倍边数组,占用极小。

    • 01 字典树:100000 个数字,每个数字 31 位,极端情况下最多开辟 3.1×10^6 个节点,每个节点 2 个 int 指针。内存约为 3100000×8 Byte≈25 MB。在 POJ 典型的 65MB 内存限制内绝对安全。

易错点总结

  1. 无向图建边越界(RE) :树是无向的,必须 addedge(u,v)addedge(v,u)。因此 vtexnxtwt 数组的长度必须是节点数上限的 2 倍

满分标程与详细注释

cpp 复制代码
//这道题思路非常清晰
//首先我们要求path(u,v)的最大值
//由lca(最近公共祖先)可以知道u到v的路径一定等于u到最近公共祖先的距离+
//最近公共祖先到v的距离
//所以path(u,v)=path(u,lca)+path(lca,v)
//但是异或运算满足交换律,顺序根本无所谓
//所以path(u,v)=path(lca,u)+path(lca,v)
//带入这道题 我们可以得到真实路径为
//path(u,v)=path(lca,u)^path(lca,v)
//带入我们前面预处理好的d数组
//d[u]=path(1,lca)^path(lca,u)
//d[v]=path(1,lca)^path(lca,v)
//d[u]^d[v]=path(1,lca)^path(lca,u)^path(1,lca)^path(lca,v)
//d[u]^d[v]=(path(1,lca)^path(1,lca))^(path(lca,u)^path(lca,v))
//因为path(1,lca)^path(1,lca)=0(两个相等的值异或肯定是0)
//所以d[u]^d[v]=0^(path(lca,u)^path(lca,v))
//所以d[u]^d[v]=path(lca,u)^path(lca,v)
//所以d[u]^d[v]=path(u,v)
//那不就好写了吗?d[u]^d[v]想取最大值,一定是尽可能高位不同才能取最大值
//遍历d然后去字典树从最高位开始尽可能找位不同的异或就好了

#include <iostream>
#include <cstring>//对应memset
#include <algorithm>//对应max函数
using namespace std;
int n;
int h[200000];//头指针数组 h[u]记录以u为起点的最后一条插入边的编号
int vtex[400000];//边数组(目标点) 记录每条边的终点树是无向图
int nxt[400000];//边数组(下一条边) 记录与当前边同起点的上一条边的编号
int wt[400000];//记录每一条有向边的权值(距离)
int idx;//每次加边分配一个唯一的新编号
int sum;//存储最长异或路径的值
int pc;//记录字典树开辟了多少个节点
int d[200000];//d[i]存储从虚拟根节点1到节点i的异或路径总和

//字典树节点
struct treenode{
    int nxt[2];//记录通往0/1两个不同方向的下一个节点的编号
}tree[50000000];

//链式前向星存图
void addedge(int u,int v,int w){
    vtex[idx]=v;//记录当前分配的第idx条边的终点是v
    wt[idx]=w;//记录当前分配的第idx条边的权重是w
    nxt[idx]=h[u];//头插法 让当前新边的下一条边 指向u节点之前的最后一条边
    h[u]=idx++;//更新u节点的头指针为当前新边idx 并将idx加1
}

//start为当前访问节点  x为到目前的异或和 fa为父亲节点(防止重复访问)
void dfs(int start,int x,int fa){
    d[start]=x;
    //取出从start节点出发的第一条边的编号
    int p=h[start];
    //遍历当前节点所连接的所有节点
    while(p!=-1){
        int v=vtex[p];
        //避免重复访问节点
        if(v==fa){
             p=nxt[p];
             continue;
        }
        int w=wt[p];
        dfs(v,x^w,start);
        p=nxt[p];
    }
}

//建立字典树 将整数x按位插入到字典树中
void insert(int x){
    int p=0;//每一轮从根节点开始插入
    //从高位到低位把x每一位的二进制表示插入字典树
    for(int i=30;i>=0;i--){
    //将x向右平移i位并与1做按位与操作 提取出二进制下第i位的精确值 (0或1)
        int y=(x>>i)&1;
        //如果当前节点向下通往位y的路径尚未打通
        if(tree[p].nxt[y]==0){
            //就给它分配一个位置
            tree[p].nxt[y]=++pc;
        }
        //指针向下移动 进入下一层子节点
        p=tree[p].nxt[y];
    }
}

//字典树查询 在树中寻找与x异或结果最大的值 返回这个最大异或和
int query(int x){
    int p=0;//每一轮从根节点开始查询
    int ans=0;//用于在向下走的过程中拼凑出最终的最大异或结果
    for(int i=30;i>=0;i--){
        //提取出待查询数字x在第i位的二进制值y(0或1)
        int y=(x>>i)&1;
        //z为我们想在字典树里找到的与y异或可以得到1的值(y是0则z是1,y是1则z是0)
        int z=y^1;
        //如果存在
        if(tree[p].nxt[z]!=0){
            //走相反位必定能异或出1 
            //把1左移i位还原其实际权重 累加到答案中
            ans+=1<<i;
            //并移动到这个节点
            p=tree[p].nxt[z];
        }
        //如果不存在就移动到相反节点 继续往下走 该位异或出0 不加值
        else p=tree[p].nxt[y];
    }
    return ans;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    //初始化头指针数组为空
    memset(h,-1,sizeof(h));
    //建图
    for(int i=1;i<n;i++){
        int u,v,w;
        cin>>u>>v>>w;
        addedge(u,v,w);//无向图双向加边
        addedge(v,u,w);
    }
    //令一号节点为根节点,先求出根节点到所有节点的异或距离
    //从1开始往下走 一个数异或0 值永远不变 所以把0设置为起点的值
    dfs(1,0,0);
    //接下来建立字典树
    for(int i=1;i<=n;i++){
        insert(d[i]);
    }
    //然后遍历d数组,用d数组每一个数去字典树找尽可能高位不同的二进制位异或
    //最后得到最大异或路径
    for(int i=1;i<=n;i++){
        //计算d[i]在字典树中能配对出的最大值
        //并与当前全局记录sum取较大者 更新全局答案
        sum=max(sum,query(d[i]));
    }
    cout<<sum;
    return 0;
}
相关推荐
whuhewei7 小时前
React diff算法为什么是DFS,不是BFS
算法·react.js·深度优先
EdmundXjs8 小时前
大模型核心概念解读
人工智能·算法
lookaroundd8 小时前
llm-compressor 普通量化调用链分析
python·算法
小羊在睡觉8 小时前
力扣239. 滑动窗口最大值
数据结构·后端·算法·leetcode·go
兰令水8 小时前
topcode【随机算法题】【2026.5.20打卡-java版本】
java·开发语言·算法
此生决int8 小时前
算法从入门到精通——前缀和
c++·算法·蓝桥杯
大大杰哥9 小时前
leetcode hot100(4)矩阵
算法·leetcode·矩阵
小白|9 小时前
cmake:昇腾CANN构建系统完全指南
java·c++·算法
nebula-AI9 小时前
人工智能导论:模型与算法(未来发展与趋势)
人工智能·神经网络·算法·机器学习·量子计算·automl·类脑计算