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 的异或路径和,并存入数组 Di 中。那么会有如下等式:

  • Du=Path(1,LCA)⊕Path(LCA,u)

  • Dv=Path(1,LCA)⊕Path(LCA,v)

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

Du⊕Dv=Path(1,LCA)⊕Path(LCA,u)Path(1,LCA)⊕Path(LCA,v)

重新组合:

Du⊕Dv=Path(1,LCA)⊕Path(1,LCA)Path(LCA,u)⊕Path(LCA,v)

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

Du⊕Dv=0⊕Path(LCA,u)⊕Path(LCA,v)=Path(u,v)

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

算法设计

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

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

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

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

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

    • 贪心策略:想让异或值最大,高位必须尽量是 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;
}
相关推荐
Forever Nore1 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考2 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX3 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面4 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号4 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽4 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI4 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者6 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者6 小时前
【征程6】校准量化中HistogramObserver解析
算法
OuO-27 小时前
笔试强训 Day 34:ISBN 号码、kotori 和迷宫、矩阵最长递增路径
java·算法·矩阵