20200726005921
给一棵有 n n n 个节点的树,第 i i i 条边有边权 W i W_i Wi。
我们可以无限次地对其增添或删除边,但要求全程始终保证:
最后得到一棵新树(也可保持原树不做变动),使得边权之和最少。求边权之和的最小值。
2 ≤ N ≤ 1 0 5 ; 0 ≤ W i < 2 30 ; 2\leq N\leq 10^5; 0\leq W_i<2^{30}; 2≤N≤105;0≤Wi<230;
具体来说,
证明:
既然增删过程中,图要连通,那么
那么,即使某条边(假设为 e 1 e_1 e1,连在 u 1 , u 2 u_1,u_2 u1,u2 之间,不要求为原树上的边)被删除时,其权值 W e 1 ′ W'_{e_1} We1′(作为环上其它边的边权的异或和)仍然被保留了下来。(写作 W ′ W' W′ 是为了与输入数据变量名作区分)
这条边原来在几个简单环上,删边之后 u 1 , u 2 u_1,u_2 u1,u2 之间就会有几条简单路径(应该没错??)。每一条路径各自的边的边权异或和都分别为 W e 1 W_{e_1} We1。
如果这些路径(中的某些)上的某些边又被删去,那么就进一步 “绕远路” ,绕远路涉及的环也还要满足异或和的要求。总之, u 1 , u 2 u_1,u_2 u1,u2 之间任意一条路径上的边的边权异或和都是 W e 1 ′ W'_{e_1} We1′。而且,由于 u 1 , u 2 u_1,u_2 u1,u2 之间没有直接连边,为了使全图连通,则 u 1 , u 2 u_1,u_2 u1,u2 之间随时都至少有一条路径。
当这条边想被增加回来时,其权值只能是上述路径的边的边权异或和,因此不变。
因此,任意两点之间若连边,则边权由两点间任意一条当前已有路径的边的边权异或和决定,且任意两点之间任意时刻必然至少有一条路径、这些路径的边权异或和无法改变。
此性质赛上已想出。
异或最小生成树问题,是在只给定各点点权值、且各边边权为两端点点权的异或值的完全图上,求最小生成树。
本题可转化为异或最小生成树,或者说,本题可将各边的确定、不变的边权转化为各点点权,是因为
(前置知识:字典树)
异或得到所有边权,然后按一般最小生成树问题来做? O ( n 2 ) O(n^2) O(n2) 条边,不可。
已知权值(点权、边权)小于 2 l e n 2^{len} 2len (本题 l e n = 30 len=30 len=30),则将点权值表示为 l e n len len 位二进制数,构造 l e n len len 层的字典树。
于是,字典树上的 n n n 个叶子就表示原图的 n n n 个节点,我们要在叶子上连 ( n − 1 ) (n-1) (n−1) 条边使它们互相 “连通”,且生成树边权之和最小。当然,可能存在多个点的点权相同,它们在字典树同一个叶子上、连边时权值为零,当然就要连边,且连了边也不对最小生成树答案的增加做贡献(边权为零),所以直接对点权去重即可。
两叶子 u 1 , u 2 u_1,u_2 u1,u2 连边的边权是它们点权的异或和。在字典树上,假设它们的 LCA (最近公共祖先节点)是 l c a lca lca,则 u 1 , u 2 u_1,u_2 u1,u2 的点权在【从字典树根到 l c a lca lca】表示的所有 bit 上都相同,那么在这些 bit 上的异或值当然分别为 0 0 0。如下图,两点权值 0110000
、 0110101
的异或值为 0000101
,则异或值只有第 [ 0 , 3 ) [0,3) [0,3) bit 可能为一,高位必都为零。
这些异或和为零的 bit 都在高位上,而我们现在在求最小生成树,自然希望这些高位越低越好,于是就希望 l c a lca lca 越低越好,于是就希望连边的两点尽量在【层数更小的】子树内。然而,对于同时拥有左右(零/一)儿子的节点(以下简称分叉节点),其左子树中的叶子必然要和右子树中的叶子以它自己为 LCA 相连至少一次,因此我们对于任何一个分叉节点,只以其为LCA连恰好一条边(把左右子树的叶子连通)。
【这一段别看了】至于怎么选边,我们只需分别枚举左右子树上的节点尝试连边,选出其中边权最小者即可。因此,我们对字典树做dfs,在回溯(从低到高)时,对于每个分叉节点按上述方法枚举得到最小边权,连边即可。(这是 https://blog.csdn.net/Galaxy_yr/article/details/102323777 对另一道异或最小生成树题目的做法,似乎还是 O ( n 2 ) O(n^2) O(n2) 的?)
另一种做法是( https://blog.csdn.net/tianyizhicheng/article/details/90696847 ),对于字典树每个分叉点,择其两个子树中叶子较少的那个,依次取叶子(点权值)分别在另一子树中直接链状地匹配。例如,分叉点的左子树中叶子较少(只有 y L y_L yL 个),我们取其中每个叶子(点权) x i x_i xi 时,在右子树中也从分叉点开始向下搜索,对于每个 bit 如果 x i x_i xi 上此 bit 的值在右子树此层有对应的节点则进入此节点(于是右子树上选来与 x i x_i xi 匹配的叶子,在此 bit 上的异或值为零),否则只好进入另一个节点。如下图,当我们对叶子较少的子树中的某叶子 x i = x_i= xi= ...0110
在另一子树中匹配时,我们尽量在各 bit 匹配同一数字使此 bit 异或值为零,然而在第 2 2 2 bit 处无法匹配相同值,就只好在此 bit 匹配另一个数字,最终匹配到 ...1100
。
这样,对叶子较少的子树中的各叶子分别去另一子树内匹配,能取到各自的最小异或值,最后汇总即可取得此 LCA 处连边时需要的总的最小异或值。
这种做法避免了任意两节点之间异或值的一一比较,优化了时间复杂度。具体来说,设某 LCA 左子树中有 L L L 个叶子、右子树中有 R R R 个叶子,则只需比较 min ( L , R ) ≤ L + R 2 \min(L,R)\leq \frac{L+R}{2} min(L,R)≤2L+R 次即可从这 L ⋅ R L\cdot R L⋅R 对叶子中选出最小异或值作为此 LCA 担负的连边任务。然后,对每个 LCA,我们根据其与最高 LCA(深度最小的 LCA)之间所隔 LCA 的数量分层,最高 LCA 为 “第零层 LCA”,依此类推。如下图,红色节点为 LCA,我们对其分层。
假设点权去重后剩余 n ′ n' n′ 个值,则有 n ′ n' n′ 个叶子。对于第 i ( i ≥ 0 ) i(i\geq 0) i(i≥0) 层 LCA,设其中第 j j j 个 LCA 的左子树中有 L j L_j Lj 个叶子、右子树中有 R j R_j Rj 个叶子,则此层所有 LCA 造成的叶子比较次数为 ∑ j min ( L j , R j ) ≤ ∑ j L j + R j 2 ≤ n ′ 2 \sum_j \min(L_j,R_j)\leq \sum_j \frac{L_j+R_j}{2}\leq\frac{n'}{2} ∑jmin(Lj,Rj)≤∑j2Lj+Rj≤2n′ (第一个小于等于见上一段解释,第二个小于等于可参考上图的第三层 LCA 来理解)。总共最多有 l e n len len 层 LCA(一般来说远远达不到),因此总的比较次数为 O ( l e n 2 ⋅ n ′ ) O(\frac{len}{2}\cdot n') O(2len⋅n′),其中 l e n 2 \frac{len}{2} 2len 是非常小的常数!如果再算上每次比较过程中沿着树链向下走最多 l e n len len 步,则【异或最小生成树】问题复杂度为 O ( l e n 2 2 ⋅ n ′ ) O(\frac{len^2}{2}\cdot n') O(2len2⋅n′),其中 l e n 2 2 \frac{len^2}{2} 2len2 也不太大。
实现上还有些技巧:我们可以用最多 N N N 个 vector
(C++)来记录子树中的值,并对字典树维护一个数组 i d [ ] id[] id[] 表示第 i i i 个(字典树)节点的子树中的叶子所代表的权值们被存储在了第 i d [ i ] id[i] id[i] 个 vector
中。建字典树(插入单词)时我们只需把(去重后的)每个权值记录在各自叶子所对应的某个 vector
中;在dfs回溯时再向上合并,例如对于字典树节点 u u u,已知当前其左子节点 L u L_u Lu 和右子节点 R u R_u Ru 的子树中的叶子分别被存在了第 i d [ L u ] , i d [ R u ] id[L_u], id[R_u] id[Lu],id[Ru] 个 vector
中,则把其中一个元素较少的 vector
的元素移到另一个 vector
中(完成合并),然后把 i d [ u ] id[u] id[u] 赋值为 i d [ L u ] id[L_u] id[Lu] 或 i d [ R u ] id[R_u] id[Ru] 即可。这样,对于每个 LCA,我们在从较小子树中取值去与另一子树匹配时,取值的步骤就是 O ( 1 ) O(1) O(1) 的了,更重要的是降低了代码编写难度。既然 dfs 过程不仅担负选边任务,还要担负子树叶子集的合并任务,因此各项工作当然是在回溯时进行。
#include
#include
#include
#include
#define ll long long
// graph
const int fu=0, fv=1, fw=2, fnext=3;
int edge[200005][4];
int edges=0;
int last[100005];
void add_edge(int u , int v , int w){
edge[edges][fu]=u;
edge[edges][fv]=v;
edge[edges][fw]=w;
edge[edges][fnext] = last[u];
last[u]=edges;
++edges;
}
std::queue<int> que;
int a[100005];
// trie
int trie[3100005][2];
int nodes=1; // trie root initially exists.
int id[3100005];
std::vector<int> values[100005];
void add_word(int value , int word_id){
int now=1;
for(int i=29 ; i>=0 ; --i){
int bit = ((value>>i)&1);
if(trie[now][bit]==0) trie[now][bit]=(++nodes);
now = trie[now][bit];
}
id[now] = word_id;
values[word_id].push_back(value);
}
int matching(int value1 , int now , int depth){
int xor1 = (1<<(depth-1)); // not value2
for(int i=depth-2 ; i>=0 ; --i){
int bit = ((value1>>i)&1);
if(trie[now][bit]>0){
now = trie[now][bit];
}else{
now = trie[now][1-bit];
xor1 |= (1<<i);
}
}
return xor1;
}
ll ans;
void dfs(int now , int depth){ // here, depth of leaves are zero!
if(trie[now][0]>0) dfs(trie[now][0] , depth-1);
if(trie[now][1]>0) dfs(trie[now][1] , depth-1);
//printf("\nnow=%d , depth=%d , trie[now][0]=%d , trie[now][1]=%d\n" , now , depth , trie[now][0] , trie[now][1]);
if(trie[now][0]>0 && trie[now][1]>0){ // now is a LCA
int min_xor = (1<<30);
if( values[id[ trie[now][0] ]].size() < values[id[ trie[now][1] ]].size() ){
for(int i=0 ; i<values[id[ trie[now][0] ]].size() ; ++i){
int value1 = values[id[ trie[now][0] ]][i];
int xor1 = matching(value1 , trie[now][1] , depth);
if(xor1<min_xor) min_xor = xor1;
//printf("\tvalue1=%d , xor1=%d , min_xor=%d\n" , value1 , xor1 , min_xor);
values[id[ trie[now][1] ]].push_back(value1);
}
id[now] = id[trie[now][1]];
}else{
for(int i=0 ; i<values[id[ trie[now][1] ]].size() ; ++i){
int value1 = values[id[ trie[now][1] ]][i];
int xor1 = matching(value1 , trie[now][0] , depth);
if(xor1<min_xor) min_xor = xor1;
//printf("\tvalue1=%d , xor1=%d , min_xor=%d\n" , value1 , xor1 , min_xor);
values[id[ trie[now][0] ]].push_back(value1);
}
id[now] = id[trie[now][0]];
}
ans += min_xor; // (ll)min_xor ? // not xor
}else{
if(trie[now][0]>0 || trie[now][1]>0) id[now] = id[ trie[now][0] + trie[now][1] ];
}
}
// main
int main(){
int N;
scanf("%d" , &N);
memset(last , -1 , sizeof last);
for(int i=1 ; i<N ; ++i){
int u,v,w;
scanf("%d%d%d" , &u , &v , &w);
add_edge(u,v,w);
add_edge(v,u,w);
}
// assign node weight
memset(a , -1 , sizeof a);
a[0]=0;
que.push(0);
while(! que.empty()){
int u = que.front(); que.pop();
for(int e=last[u] ; e>=0 ; e=edge[e][fnext]){
int v=edge[e][fv];
if(a[v]>=0) continue; // not a>=0
a[v] = (a[u] ^ edge[e][fw]);
que.push(v);
}
}
std::sort(a , a+N);
// build trie
memset(trie , 0 , sizeof trie);
for(int u=0 ; u<N ; ++u){
if(u>=1 && a[u]==a[u-1]) continue; // to ignore duplicates node weight
add_word(a[u] , u);
//printf("add word %d : %d\n" , u ,a[u]);
}
//for(int u=1 ; u<=nodes ; ++u) printf("u=%d : 0->%d , 1->%d\n" , u , trie[u][0] , trie[u][1]);
// XOR minimum spanning tree dfs
ans = 0;
dfs(1 , 30);
printf("%lld\n" , ans);
return 0;
}
G. Xor-MST 异或边的最小生成树 分治
https://blog.csdn.net/qq_41955236/article/details/90715120
Codeforces Contest 888 G Xor-MST —— 求异或最小生成树
https://blog.csdn.net/tianyizhicheng/article/details/90696847
【重要】
[题解]决斗 异或最小生成树
https://blog.csdn.net/Galaxy_yr/article/details/102323777