【题目描述】
原题来自: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 字典树:
-
预处理 (DFS):以节点 1 为根遍历全树,求出所有的 D[i]。
-
构建字典树:将所有的 D[i] 拆解成 31 位的二进制串(从高位到低位),插入到一棵 01 字典树中。
-
贪心查询:遍历每一个 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 内存限制内绝对安全。
-
易错点总结
- 无向图建边越界(RE) :树是无向的,必须
addedge(u,v)和addedge(v,u)。因此vtex、nxt和wt数组的长度必须是节点数上限的 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;
}