链接: 2020牛客暑期多校训练营(五)B - Graph.
本题相关知识点简介
- 异或
异或:0 ^ 0= 0,0 ^ 1=1,1 ^ 0=1,1 ^ 1=0.记为"同0异1". - 异或和
例如:1 ^ 2 ^ 4=7,7为异或和 - 图
G=<V,E>,点与边的集合 - 完全图
即图的每对点之间都有边 - 树
连通且无回路的无向图 - 生成树
基于原无向图的子图(少边不少点),构成的树,为生成树. - 最小生成树
即边权值最小的生成树 - 前缀树(trie树)(字典树)
用于快速检索单词,数字是否存在的树状结构.
单词的字典树是一个26叉树.
数字的字典树是一个10叉树.
01字典树便是一个2叉树. - 01字典树
即只处理01串的字典树 - 最大异或对
要求:1~n个数,选两个数求异或最大.
方法:n个整数转化成为n个长度为32位的二进制字符串.建01字典树
每次选取一个串检索的时候,我们都走与当前a[i]的二进制位的数值相反的位置走,这样就可以让异或值最大,如果说没有路可以走的话,那么就走相同的路. - 分治
分治(divide and conquer)的全称称为“分而治之”,分治即是将大问题划分为若干个规模较小、可以直接解决的子问题,然后解决这些子问题,最后将这些子问题的解合并起来,即是原问题的解。
分治是一种思想,它不涉及到具体的算法,而大多数情况下,分治都是借由递归来实现的。
- 最小异或对
要求:1~n个数,选两个数求异或最小.
方法:n个整数转化成为n个长度为32位的二进制字符串.建01字典树
每次选取一个串检索的时候,我们都走与当前a[i]的二进制位的数值相同的位置走,这样就可以让异或值最大,如果说没有路可以走的话,那么就走相同的路. - 最小异或生成树(重点知识点)
要求:给出n个点的权值,若连接点x,y,则两点之间边的权值为a[x]^a[y]让你求出最小生成树.
方法:本题跟普通的最小生成树区别就是边权的设置,那怎么求边权呢?,暴力复杂度太高,我们用最小异或对的思路,选两个数求异或最小,也就是边最短.
这里我们举个 boruvka 算法求解最小生成树问题的例子,它是克鲁斯卡尔算法和普雷姆算法的结合版本,具体就是维护图中的所有连通块,然后贪心去找每一个连通块和其余所有的连通块之间的最小边权,将其合并为一个连通块,如此往复.
那么和这个题目有什么联系呢?这里先回答:通过01字典树的分块处理,我们便可以结合最小生成树类似 boruvka 算法的贪心策略,分治递归求解.
01字典树在本题中共有两个用途,一个是基本应用,也就是查找异或值最小的答案,另一个是结合最小生成树的贪心策略,将其分块处理
首先抛出一个问题:能否把当前图的点集随意划分成两半,递归两半后选出连接两个点集的边中权值最小的一条,然后得到最后的最小生成树。
乍一看没什么问题,但仔细想一下就会发现这个策略其实是错误的,因为最终的最小生成树中可能有两条连接当前层两个点集的边(形成环)
但是对于本题而言,我们可以借助01字典树划分点集从而分治求解,当我们在字典树上从最高位到最低位维护每一个数字后,显然每一个节点的左右两个儿子,就已经将所有的点划分为两个集合了(0和1的不同).
贪心思想中,一个节点的最小异或生成树是: 左子树的最小异或生成树+右子树的最小异或生成树+左右子树节点合并时的最小代价。对于前两个贡献可以分治递归计算,对于第三个贡献,总结一下就是求就是两个子树的点之间的最小异或对了。
在这里,我们可以枚举左子树或右子树点,借助Trie树求异或的另一颗子树的值,最后取最小值作为合并的边即可。
那么如何确定枚举的子树里有哪些点值呢?我们可以在插入前就排序,记录每个节点下辖节点序号的的左右区间,枚举的时候直接枚举区间里的数即可。
综上所述,总结一下本题的求解方法:
1.在接收每个点的权值后,将其放入01字典树中,方便求最短边,也划分了分块.
2.运用分治思想将大问题递归到最小子问题.左子树的生成树+右子树的生成树+通过枚举左或右子树点求异或的另一颗子树的最小值.
3.重复这个过程,不断由小子树构成大子树,最后形成完整的最小异或生成树.
本代码主要引用了King_Zhang的代码片,并稍作修饰简化,在此感谢King_Zhang(>w<).
最小异或生成树
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
const int maxn=1e7+1;
const ll inf=1e17;
#define sf scanf
#define pf printf
ll a[maxn],n;
struct node
{
int cnt = 0;//标记结点 就是记录第几个结点。这个就是字典树 中的结点那个
int trie[maxn][2];//01字典树
int l[maxn],r[maxn];//l 是最小用到这个结点的第几条边 r[]最大用到这个结点的第几条边
void inint()//初始化 cnt=0.就是返回根节点从头开始
{
cnt=0;
}
//构建01字典树
void insert(ll x,int id)// x代表插入这个数 ,id代表第几个数
{
int gen=0;
for(int i=32;i>=0;i--)///从2进制最高位开始依次往下构建01字典树
{
int op=((x>>i)%2)?1:0;//判断这个x在该二进制位上是0还是1这样形成他的分叉 相当于 (x>>i)%2 也就是 判断他是左子树还是右子树 就是把x转化成2进制然后利用字典树从上往下排下来
if(!trie[gen][op])
{
trie[gen][op]=++cnt;//如果这个几点没用过就创建出来,并且标记出来他的节点号,也就是第几个节点
}
gen=trie[gen][op];//然后走他的下一位 换句话说 就是走 这个根节点的 下一个节点。
if(!l[gen])
{
l[gen]=id;//如果在此之前(x)没有任何数 有过该位的经历 说简单点就是这些数中没有比他小的且该位是op的。
}
r[gen]=id;//一直更新最大使用这个的,
}
}
ll answer(int gen,int pos,ll x)//gen代表第几位也就是 二进制的从头开始第几位,pos是从第几位开始计算 ,x是这个数
{
ll ans=0;//初始值
for(int i=pos;i>=0;i--)//从pos开始往后遍历每一位 看每一位是否有对应的左右结点
{
int op=((x>>i)%2)?1:0;//分出x的i位2进制的01;
if(trie[gen][op])
{
gen=trie[gen][op];//如果右端点有对应的 子树(对应的0或1)就直接看下一层(找最小异或)
}
else//没有对应的
{
gen=trie[gen][!op];//跟有对应的节点只能用另一节点来代替这时就会产生代价(1<<i),
ans+=(1ll<<i);//第i为产生代价 就加上该代价,之所以 是从pos 开始到0 是因为这样的话都可以选择最优 因为 i越大产生的代价就越大 所以尽量的让其前面的一致这样就会缩小代价
}
}
return ans;
}
ll div(int gen,int pos)//gen 和pos 跟上面解释一样
{
if(trie[gen][0]&&trie[gen][1])//如果既有左又有右,这一步就是左+右+合并
{
int x=trie[gen][0],y=trie[gen][1];///x代表 左端点的结点号,y代表右端点的节点号,x<y这是一定的 因为我们原本排序了
ll minn=inf;
for(int i=l[x];i<=r[x];i++)//遍历一下用到左半树结点从小到大的数(输入的数)
{
minn=min(minn,answer(y,pos-1,a[i])+(1<<pos));
//解释一下 answer(y,pos-1,a[i])+(1<<pos)
//前面pos-1是看pos之后那些代价。而(1<<pos) 则是该位产生的代价
//因为合并 你判断出这位是一个0和一个1 就一定会产生这个代价
}
return minn+div(trie[gen][0],pos-1)+div(trie[gen][1],pos-1); //合并+ 左+右
}
else if(trie[gen][0])
{
return div(trie[gen][0],pos-1);//如果只有左就直接是 左
}
else if(trie[gen][1])
{
return div(trie[gen][1],pos-1);//如果只有右就直接是 右
}
return 0;//如果都没有 肯定是0
}
}trie;
int main()
{
trie.inint();//初始化
sf("%lld",&n);
for(int i=1;i<=n;i++)
{
sf("%lld",&a[i]);
}
sort(a+1,a+1+n);
for(int i=1;i<=n;i++)
{
trie.insert(a[i],i);
}
printf("%lld\n",trie.div(0,32));
return 0;
}
本题解析
题意:
给一棵树,每条边都有边权,可以任意加边和删边,但要保证整个图联通并且任何一个环的边权异或和为0.求最小的权值和。
解析:
我们从题干下手,有一棵树,可以加边减边,要求图联通环边异或和为0.
因为题干给了个树,怎么加边都会有环形成,所以我们分析一下加边后形成环的情况:
现有点A,点B,点C,已知AB边,AC边,在BC间添加一边,使之形成一环,要求环的边权异或和为0,求BC.
边权异或和为0就是AB^ AC ^BC=0,我们知道AB,AC,能不能求BC?,答案是能,加括号后(AB ^ AC) ^ BC=0,什么情况下异或为零呢?完全相同的时候,所以(AB ^ AC) =BC,这样就知道未知边了,
题干要求我们求最小权值和,明显是让我们求最小生成树,又因为用到了异或,第一想到是最小异或生成树.但是,题干中给的是边权值,我们会的是点权值,该怎么办呢?
这时候就要化边为点了,一条边需要两个点,因此,我们引入一个根节点.所有边的某一点都是根节点,这样,原本a[i]定义为:i点的权值.就改为:根节点到i点的边的权值.这样一个点就代表一条边了,我们便把确定的边权转化为点权,问题就转化成了求最小异或生成树,
为了求最小异或生成树,我们需要所有点的点权,所以要先求出所有边来,形成完全图,完全图可以通过dfs的方式求出,之后便于最小异或生成树一样了.
本段代码来自hzh2019的相关博客,详情可移至文章末尾的相关链接查看.
Graph
#include<cstdio>
#include<string.h>
#include<queue>
#include<algorithm>
#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;
}
鸣谢:
分支与递归
【算法总结】最小异或生成树
牛客多校5 - Graph(字典树+分治求最小生成树)
一只酷酷光儿【Codeforces 888G】Xor-MST | 最小异或生成树、字典树、分治
Vison.R【Codeforces 888G】Xor-MST | 最小异或生成树、字典树、分治
最小异或生成树
这里有证明更加详细的博客:
2020牛客暑期多校05 B - Graph 异或最小生成树