树结构中的异或路径问题是一个经典的图论算法挑战。给定一棵具有N个节点的树,每条边上都有一个权值。我们需要找到两个节点,使得它们之间路径上所有边权的异或结果最大。
这个问题的暴力解法是枚举所有节点对,计算每对节点路径上的异或和,然后取最大值。对于N=10^5的规模,这种O(N^2)的解法显然不可行。我们需要更高效的算法。
核心思路分为三个关键步骤:
这种转化将原问题的复杂度从O(N^2)降低到O(N*32),其中32是整数的二进制位数。
树结构有一个重要特性:任意两个节点u和v之间的路径异或和,等于它们到根节点路径异或和的异或值,即D[u] XOR D[v]。这是因为:
这个性质让我们可以将问题转化为在D数组中找到最大异或对。
Trie树(前缀树)在这里的作用是高效存储和查询二进制数。我们将每个D[x]值表示为32位二进制数,从最高位开始逐位插入Trie树。
查询最大异或对的策略是:
这种贪心策略能在O(32)时间内找到一个数与Trie树中已有数的最大异或值。
cpp复制const int N=1e5+10;
struct Node{
int to,w; // 邻接表节点,to表示目标节点,w表示边权
};
vector<Node> g[N]; // 图的邻接表表示
int cnt=1,maxn=0; // Trie树节点计数和最大异或值
int trie[N*31][2]; // Trie树,每个节点最多有2个子节点(0/1)
int d[N]; // 存储每个节点到根节点的异或值
cpp复制void dfs(int u,int fa,int val)
{
d[u]=val; // 记录当前节点到根节点的异或值
insert(val); // 将当前值插入Trie树
for(auto i:g[u]) // 遍历所有邻接节点
{
int v=i.to;
int w=i.w;
if(v!=fa) // 避免回父节点
{
dfs(v,u,d[u]^w); // 递归计算子节点
}
}
}
DFS从根节点开始,沿着树结构向下遍历。对于每个节点u,计算其到根节点的异或值d[u],并立即将其插入Trie树。然后递归处理所有子节点。
插入操作:
cpp复制void insert(int x)
{
int p=0; // 从根节点开始
for(int i=31;i>=0;i--) // 从最高位到最低位
{
int b=(x>>i)&1; // 获取第i位的值
if(!trie[p][b]) // 如果该路径不存在
{
trie[p][b]=++cnt; // 创建新节点
}
p=trie[p][b]; // 移动到子节点
}
}
查询操作:
cpp复制int query(int x)
{
int p=0;
int res=0;
for(int i=31;i>=0;i--)
{
int b=(x>>i) &1; // 获取当前位
if(trie[p][!b]) // 优先选择相反的位
{
res=res| (1<<i); // 该位异或结果为1
p=trie[p][!b];
}
else p=trie[p][b]; // 只能选择相同位
}
return res;
}
查询时,我们总是尝试走与当前位相反的路径,这样可以最大化异或结果。如果相反路径不存在,才走相同路径。
cpp复制int main()
{
int n;
cin>>n;
// 构建树结构
for(int i=1;i<n;i++)
{
int u,v,w;
cin>>u>>v>>w;
g[u].push_back({v,w});
g[v].push_back({u,w});
}
// 1. DFS计算所有d[u]
dfs(1,0,0);
// 2. 查询最大异或对
for(int i=1;i<=n;i++)
{
maxn=max(maxn,query(d[i]));
}
cout<<maxn;
}
主函数首先读取输入构建树结构,然后执行DFS计算所有d[u]值并构建Trie树,最后查询每个d[i]在Trie树中的最大异或值,记录全局最大值。
Trie树需要存储最多N*32个节点,每个节点有2个子指针。对于N=1e5,大约需要:
1e5 * 32 * 2 * 4(bytes) ≈ 25MB,这在大多数编程竞赛中是可接受的。
提示:在实际编程竞赛中,可以将Trie树数组大小设为N*32+10,预留一些缓冲空间避免越界。
在实际编码中,我发现一个有用的技巧:在构建Trie树时,可以预先计算并缓存每个节点的掩码值,这样在查询时可以减少位运算次数。此外,对于C++实现,使用数组而非指针形式的Trie树通常会有更好的缓存性能。