Graph【XOR-MST转换]
题意
给一棵有 \(n\) 个节点的树,第 \(i\) 条边有边权 \(W_i\)。
我们可以无限次地对其增添或删除边,但要求全程始终保证:
- 图始终是联通的
- 任意把一个环(如果存在)上边的权值的异或和为 \(0\)
最后得到一颗新树(也可保持原树不变),使得边权之和最少,求边权之和的最小值。
\(2\leq N \leq 10^5,0\leq W_i \leq 2^{30}\)
题目链接:https://ac.nowcoder.com/acm/contest/5670/B
分析
如果任意两个点之间连边,那么该边的权值确定,因为要保证环上的边权值异或值为 \(0\)。
转换
异或最小生成树问题,是在只给定各点点权值、且各边边权为两端点点权的异或值的完全图上,求最小生成树。本题告知了各边的边权,但是当指定某一点的点权值时,可以通过与边权异或得到其它点的点权值(边权转点权)。对于树上不连通的两点 \(u,v\),设原树上的路径为 \(\{u,x_1,x_2,\dots ,x_k,v \}\),那么如果要将 \(u,v\) 两点连接,那么连接的边权为:\(u \bigoplus v\),即两点的点权的异或和。连通的两点同样成立。
异或最小生成树解法
已知权值(点权、边权)小于 \(2^{len}\)(本题 \(len=30\)),则将点权值表示为 \(len\) 位二进制数,构造 \(len\) 层的字典树。于是,字典树上的 \(n\) 个叶子就表示原图的 \(n\) 个节点,我们要在叶子上连 \(n-1\) 条边使它们互相连通,且生成树边权之和最小。当然,可能存在多个点的点权相同,它们在字典树同一个叶子上、连边时权值为零,当然就要连边,且连了边也不对最小生成树答案的增加做贡献(边权为零),所以直接对点权去重即可。
两叶子 \(u,v\) 连边的边权是它们点权的异或和。在字典树上,假设它们的 \(LCA\) 是 \(lca\),则 \(u,v\) 的点权在从字典树根到 \(lca\) 表示的所有位上都相同,即异或和为 \(0\)。
对于字典树每个分叉点,择其两个子树中叶子较少的那个,依次取叶子(点权值)分别在另一子树中直接链状地匹配。这样,对叶子较少的子树中的各叶子分别去另一子树内匹配,能取到各自的最小异或值,最后汇总即可取得此 \(LCA\) 处连边时需要的总的最小异或值。
实现上的优化技巧
我们可以用最多 \(n\) 个 vector
来记录子树中的值,并对字典树维护一个数组 \(\text{id[ ]}\) 表示第 \(i\) 个(字典树)节点的子树中的叶子所代表的权值们被存储在了第 \(\text{id[i]}\) 个 vector
中。建字典树(插入单词)时我们只需把(去重后的)每个权值记录在各自叶子所对应的某个 vector
中;在 \(dfs\) 回溯时再向上合并。这样,对于每个 \(LCA\),我们在从较小子树中取值去与另一子树匹配时,取值的步骤就是 \(O(1)\) 的了,更重要的是降低了代码编写难度。既然 dfs 过程不仅担负选边任务,还要担负子树叶子集的合并任务,因此各项工作当然是在回溯时进行。
参考博客:https://blog.csdn.net/henry2k888/article/details/107621257
代码
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
typedef pair<int,int>pii;
typedef long long ll;
const int N=3e6+5;
const int maxn=1e5+5;
vector<pii>pic[N];
int trie[N][2],a[maxn],cnt,id[N];
ll ans;
vector<int>value[maxn];
void dfs(int u,int p)//边权转点权
{
for(int i=0;i<pic[u].size();i++)
{
int v=pic[u][i].second;
if(v==p) continue;
a[v]=(a[u]^pic[u][i].first);
dfs(v,u);
}
}
void add(int x,int y)
{
int rt=1;
for(int i=29;i>=0;i--)
{
int t=((x>>i)&1);
if(trie[rt][t]==0)
trie[rt][t]=++cnt;
rt=trie[rt][t];
}
id[rt]=y;
value[y].pb(x);
}
int matching(int x,int rt,int d)
{
int xor1=(1<<(d-1));
for(int i=d-2;i>=0;i--)//注意-2
{
int t=((x>>i)&1);
if(trie[rt][t]>0)//能匹配就匹配
rt=trie[rt][t];
else
{//不匹配,并记录结果
rt=trie[rt][1-t];
xor1|=(1<<i);
}
}
return xor1;
}
void solve(int rt,int d)//d表示二进制的第几位
{
if(trie[rt][0]>0) solve(trie[rt][0],d-1);
if(trie[rt][1]>0) solve(trie[rt][1],d-1);
if(trie[rt][0]>0&&trie[rt][1]>0)//当前点为一个LCA
{
int min_xor=(1<<30);
if(value[id[trie[rt][0]]].size()<value[id[trie[rt][1]]].size())
{
for(int i=0;i<value[id[trie[rt][0]]].size();i++)//从小的子树中选择
{
int value1=value[id[trie[rt][0]]][i];
int xor1=matching(value1,trie[rt][1],d);
if(xor1<min_xor) min_xor=xor1;
//把点rt的子树的所有叶子节点的点权值合并在一起
value[id[trie[rt][1]]].pb(value1);
}
id[rt]=id[trie[rt][1]];//把记录转到rt点
}
else
{
for(int i=0;i<value[id[trie[rt][1]]].size();i++)
{
int value1=value[id[trie[rt][1]]][i];
int xor1=matching(value1,trie[rt][0],d);
if(xor1<min_xor) min_xor=xor1;
value[id[trie[rt][0]]].pb(value1);
}
id[rt]=id[trie[rt][0]];
}
ans+=min_xor;
}
else
{//直接转移,不用合并
if(trie[rt][0]>0||trie[rt][1]>0)
id[rt]=id[trie[rt][0]+trie[rt][1]];
}
}
int main()
{
int n,u,w,v;
scanf("%d",&n);
for(int i=1;i<n;i++)
{
scanf("%d%d%d",&u,&v,&w);
pic[v].pb(make_pair(w,u));
pic[u].pb(make_pair(w,v));
}
a[0]=0;
dfs(0,-1);
sort(a,a+n);//便于处理重复的点
//for(int i=0;i<n;i++) cout<<a[i]<<endl;
cnt=1;
add(a[0],0);
for(int i=1;i<n;i++)
{
if(a[i]!=a[i-1])
add(a[i],i);
}
ans=0;
solve(1,30);
printf("%lld\n",ans);
return 0;
}