2020牛客暑期多校训练营(五)B - Graph

2020牛客暑期多校训练营(五)B - Graph


链接: 2020牛客暑期多校训练营(五)B - Graph.

本题相关知识点简介

  1. 异或
    异或:0 ^ 0= 0,0 ^ 1=1,1 ^ 0=1,1 ^ 1=0.记为"同0异1".
  2. 异或和
    例如:1 ^ 2 ^ 4=7,7为异或和

  3. G=<V,E>,点与边的集合
  4. 完全图
    即图的每对点之间都有边

  5. 连通且无回路的无向图
  6. 生成树
    基于原无向图的子图(少边不少点),构成的树,为生成树.
  7. 最小生成树
    即边权值最小的生成树
  8. 前缀树(trie树)(字典树)
    用于快速检索单词,数字是否存在的树状结构.
    单词的字典树是一个26叉树.
    数字的字典树是一个10叉树.
    01字典树便是一个2叉树.
  9. 01字典树
    即只处理01串的字典树
  10. 最大异或对
    要求:1~n个数,选两个数求异或最大.
    方法:n个整数转化成为n个长度为32位的二进制字符串.建01字典树
    每次选取一个串检索的时候,我们都走与当前a[i]的二进制位的数值相反的位置走,这样就可以让异或值最大,如果说没有路可以走的话,那么就走相同的路.
  11. 分治
    分治(divide and conquer)的全称称为“分而治之”,分治即是将大问题划分为若干个规模较小、可以直接解决的子问题,然后解决这些子问题,最后将这些子问题的解合并起来,即是原问题的解。

分治是一种思想,它不涉及到具体的算法,而大多数情况下,分治都是借由递归来实现的。

  1. 最小异或对
    要求:1~n个数,选两个数求异或最小.
    方法:n个整数转化成为n个长度为32位的二进制字符串.建01字典树
    每次选取一个串检索的时候,我们都走与当前a[i]的二进制位的数值相同的位置走,这样就可以让异或值最大,如果说没有路可以走的话,那么就走相同的路.
  2. 最小异或生成树(重点知识点)
    要求:给出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 异或最小生成树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值