P4551 最长异或路径 题解
本篇题解思路来源于《算法竞赛进阶指南》。
前置知识:
- 字典树
- 异或性质
- 最近公共祖先
有了以上知识的基础,我们才能高效地解决本道题目。若对以上知识点不熟悉,建议先做题巩固,再来尝试此题,会更有收获。下面的内容建立在熟悉以上知识点的基础上,不再对一些名词进行解释。
题目中要求我们寻找一条从树上
整个问题不好直接入手,所以我们将其划分为两个子任务:
A. 求出两个结点间的边权异或和
B. 找到两个结点,使他们路径上的的边权异或和最大
如果我们熟悉异或性质,我们自然会想到:
即,若两个相同的数异或,那么他们的值为0。 由于异或运算具有交换律与结合律的性质,那么:
对于
因此,我们令
因此,对于
此时,我们已经解决了一个子问题:如何快速求出两个结点间的边权异或和。令
使得:
现在我们来讨论,如何找到最大值,即最大异或对。我们可以扫描一遍
这个时候,我们需要使用一种数据结构——字典树。字典树是解决异或问题的有力数据结构,我们运用贪心的思想,可以很快地解决本题的第二个子任务。假设某棵树有4个结点,每个结点的
这里不给出树的结构,因为当我们已经求出
现在,我们已经在这棵线段树中插入了 Dis 数组 的值。先抛出理论,令 Sum 表示异或和。在 Dis[X] 的二进制表示中,从高位到低位扫描,若 Dis[X] 当前位的值是 1 ,那么我们在当前字典树的子树中,从根节点向左儿子走(即向值为 0 的子节点走,在我的图中是左儿子),并更新 Sum (具体在代码中);若没有左儿子,那我们只能向右走,不更新 Sum 。若当前位的值是 0 ,则相反,即每次向与当前位数值不同的节点走。这样的贪心策略是显然成立的。最后走到叶节点时,此时的 Sum 就是某个 Dis[Y] 与 Dis[X] 异或得到的最大值。
事实上,我们也可以得知
下面是一个例子:寻找与
至此,问题得到解决,剩下就是码代码了。下面是我的代码,仅供参考,时间复杂度应该近似于
#include<iostream>
#include<cstdio>
#include<memory.h>
using namespace std;
#define N 100010
int n;
int head[N],nxt[N<<1],ver[N<<1],val[N<<1],tot=0;
int Dis[N];//此处Dis含义与上文相同
int trie[N<<4][2],cnt=1;//字典树
bool end[N<<4];//结束标识
inline void add(int x,int y,int z)
{
ver[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
val[tot]=z;
}//邻接表存图
void dfs(int x,int fa)
{
for(int i=head[x];i;i=nxt[i])
{
int y=ver[i],z=val[i];
if(y == fa)
continue;
Dis[y]=(z^Dis[x]);
dfs(y,x);
}
return ;
}//求出Dis数组
inline void insert(int x)
{
int p=1;
for(int i=31;i>=0;i--)
{
int num=((x>>i)&1);
if(!trie[p][num])
trie[p][num]=++cnt;
p=trie[p][num];
}
end[p]=1;
}//字典树-插入节点
inline int find(int num)
{
int p=1;
int Sum=0;
for(int i=31;i>=0;i--)
{
int x=((num>>i)&1);
if(trie[p][x^1])
Sum+=(1<<i),p=trie[p][x^1];//更新Sum
else
p=trie[p][x]; //不更新Sum
}
return Sum;
}//字典树-求和
int main()
{
scanf("%d",&n);
for(int i=1;i<=n-1;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
Dis[1]=0;
dfs(1,0);
for(int i=1;i<=n;i++)
insert(Dis[i]);
int Sum=0;
for(int i=1;i<=n;i++)
Sum=max(Sum,find(Dis[i]));
printf("%d",Sum);
return 0;
}