2020牛客暑期多校训练营(第五场) B Graph
这题可以说就是cf888g改编了一下,加上有人赛中发题解,所以过了一百多人,实际上还是很难的
首先我们要先明确一个东西:这个图任意两个点u,v边权值始终不变,因为它所在的环异或和始终等于0,所以,边uv的值就是这个环其他边的异或和,现在即使它所在的环的某条边x被删去了,由于图要联通,那x肯定是被它所在的另一个环的其他边代替,而这些边的异或和等于x的值,这其实相当于没有删x,uv这条边还是在一个同样的环里,权值必然不会变
现在我们就可以将每个点的值变成根节点到他的路径异或和,uv连边,边值就是u点到v点路径异或和,现在可以将原图视为一个完全图,uv边值就是a[u]^a[v],我们最后还是要得到一颗树,所以我们就是求这个完全图的最小生成树
这其实就是cf888g,可以参考我的另一篇博客:https://blog.nowcoder.net/n/21f7f192a09f46498c8792a78064ca89
具体做法就是对每个点求下根节点到它的路径异或前缀和,然后就是求完全图最小生成树
完全图最小生成树做法:
题意:给你一张完全图,每一个点有一个点权为 a[i],边 (u,v) 的边权为 a[u] xor a[v],求最小生成树的边权和.这题思想就是boruka
![]()
具体就是我们从最高位来看,我们可以按照二进制最高位是0还是1来把数分为两块,他们内部各自进行连边形成最小生成树,然后这两个部分再连边,因为这样只会有一条最高位是1的边,如果我们让多个01对连边,那么会形成多条最高位权值是1的边,这明显会使结果更差,然后次高位及以下也如此处理。。
在操作上就是我们先把每个点的权值插入到01字典树,从最高位开始分治,在0和1这两个左右子树,它们首先连边,然后再在左右子树找一个点对,他们异或值要最小,为了让异或值最小,我们在左右子树向下找的时候,每一位尽量找相同的点,相同的话这一位异或值就会为0
时间复杂度:我们遍历了一遍字典树,这个复杂度可以看成nlogn,在字典树的每个节点又logn找了一遍异或最小值点对,所以总复杂度可以看成n(logn)^2
#include<stdio.h> #include<algorithm> #include<iostream> #include<string> #include<string.h> #include<map> #define ll long long #include <iostream> #include <math.h> using namespace std; #define maxn 3300000+5 ll tre[maxn][3],pos=0,sum[maxn]; struct node { ll to,w; }; vector<node>e[105000];ll a[maxn]; void add(ll x) { ll c=0; for(ll i=30;i>=0;i--) { ll y=(x>>i)&1ll; if(!tre[c][y]) tre[c][y]=++pos; c=tre[c][y]; sum[c]++; } } ll Find(ll x,ll y,ll step) { ll ans=1e18; if(step==-1) return 0; if(tre[x][0]) { if(tre[y][0])//也就是说,尽量保证左右两边数每位尽量一致,这样一致异或就是0 { ll w=Find(tre[x][0],tre[y][0],step-1); ans=w; } else { ll w=Find(tre[x][0],tre[y][1],step-1); ans=w+(1ll<<step); } } if(tre[x][1]) { if(tre[y][1])//也就是说,尽量保证左右两边数每位尽量一致,这样一致异或就是0 { ll w=Find(tre[x][1],tre[y][1],step-1); ans=min(ans,w); } else { ll w=Find(tre[x][1],tre[y][0],step-1); ans=min(ans,w+(1ll<<step)); } } //printf("tre[%lld][0]=%lld tre[%lld][1]=%lld tre[%lld][0]=%lld tre[%lld][1]=%lld step=%lld ans=%lld\n",x,tre[x][0],x,tre[x][1],y,tre[y][0],y,tre[y][1],step,ans); return ans; } ll dfs(ll c,ll step) { ll ans=0; ll ls=sum[tre[c][0]],rs=sum[tre[c][1]]; if(step==-1) return 0; if(tre[c][0])ans+=dfs(tre[c][0],step-1); if(tre[c][1])ans+=dfs(tre[c][1],step-1); if(min(ls,rs)>0)//只有0和1都有才会分开连边 { ans+=Find(tre[c][0],tre[c][1],step-1)+(1ll<<step);//说明0和1都有,那它们0集合就要和1集合连边,这条边贡献1<<step //然后再在左右子树找一个点对(也就是剩余部分),它们异或和最小 }//printf("step=%lld tre[%lld][0]=%lld tre[%lld][1]=%lld ls=%lld rs=%lld ans=%lld\n",step,c,tre[c][0],c,tre[c][1],ls,rs,ans); return ans; } void init() { pos=0; } void dfs0(ll u,ll fa) { for(node i:e[u]) { ll v=i.to,w=i.w; if(v==fa) continue; a[v]=a[u]^w; dfs0(v,u); } } int main() { /*ll n; scanf("%lld",&n); for(ll i=1;i<=n;i++) { ll x; scanf("%lld",&x); add(x); } ll ans=dfs(0,30); printf("%lld\n",ans);*/ ll n; scanf("%lld",&n); for(ll i=1;i<n;i++) { ll u,v,w; scanf("%lld %lld %lld",&u,&v,&w); u++,v++; e[u].push_back({v,w}); e[v].push_back({u,w}); } dfs0(1,-1); for(ll i=1;i<=n;i++) { add(a[i]); } ll ans=dfs(0,30); printf("%lld\n",ans); } /* 3 1 2 3 */