牛客多校5 - Graph(字典树+分治求最小生成树)

题目链接:点击查看

题目大意:给出一棵树,每条边都有一个权值,每次操作可以删除任意一条边或者增加任意权值的一条边,现在可以执行数次操作,不过任何时间都要满足以下两个条件:

  1. n 个点互相连通
  2. 所有环的权值异或和为 0

求数次操作后图上边权之和的最小值

题目分析:将题意转换一下就可以转换为经典问题:完全图上的最小生成树,给出 n 个点,每个点都有权值 a[ i ] ,每条边的权值为 a[ i ] ^ a[ j ] 现在需要求最小生成树

这个题目只需要 dfs 跑一遍就可以转换为上述问题了,这里不多赘述,那么将问题转换后,该如何求解呢

有个 boruvka 算法也是求解最小生成树问题的,只不过是克鲁斯卡尔算法和普雷姆算法的结合版本,具体就是维护图中的所有连通块,然后贪心去找每一个连通块和其余所有的连通块之间的最小边权,将其合并为一个连通块,如此往复

那么和这个题目有什么联系呢?首先抛出一个问题:把当前图的点集随意划分成两半,递归两半后选出连接两个点集的边中权值最小的一条,得到最后的最小生成树。

乍一看没什么问题,但仔细想一下就会发现这个策略其实是错误的,因为最终的最小生成树中可能有两条连接当前层两个点集的边

但是对于本题而言,我们可以借助01字典树划分点集,借一下图:https://www.cnblogs.com/qieqiemin/p/13381095.html

当我们在字典树上从最高位到最低位维护每一个数字后,显然每一个节点的左右两个儿子,就已经将所有的点划分为两个集合了

以上图中的划分方法为例,假设两个集合是从第 deep 层的高度分开,显然两个集合中 deep 往上的位置在二进制下都是相同的,不同的只是 deep - 1 及往下的位置,所以合并上图中两个绿色集合中的点集所需要的最小代价就是:(1<<deep)+找到两个点 i 和 j ,满足点 i 属于左边的点集,j 属于右边的点集,同时 a[ i ] ^ a[ j ] 是最小的

这样贪心由下往上去合并的话,上面那个错误的策略,在这个题目中的正确性得到了保证

综上所述,总结一下本题的求解方法,在计算出每个点的权值后,将其放入01字典树中,然后对于每一层深度分治即可,需要注意的几个地方就是,这个题目每个点的范围是 0 ~ n-1 ,而不是 1 ~ n,还有就是对于点权相同的点来说,我们可以想象在其之间建立一条边,因为花费为 0 ,所以不会对答案造成影响,故在处理时,对点权去重一下可以减少时间复杂度

至于实现,对于每次左右子树中的两个连通块,我是vector暴力维护的点集(向上传递完毕后不要忘记及时初始化,防止爆内存),然后遍历点集数更小的连通块,在另一个连通块中找异或值最小的答案,这个就是01字典树的基本应用了,所以字典树在本题中共有两个用途,一个是基本应用,也就是贪心去查找异或值最小的答案,另一个是结合最小生成树的贪心策略,将其分块处理

时间复杂度的话,因为字典树的高度为30,所以分治+vector暴力维护点集的时间复杂度为 nlogn,加上需要在字典树上查找异或的最小值,需要多加一个log,总的时间复杂度也就是 nlognlogn 了

代码打注释了,有不理解的地方看看代码应该就明白了

代码:
 

#include<iostream>
#include<cstdio>
#include<string>
#include<ctime>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<stack>
#include<climits>
#include<queue>
#include<map>
#include<set>
#include<sstream>
#include<cassert>
#include<bitset>
using namespace std;
 
typedef long long LL;
 
typedef unsigned long long ull;
 
const int inf=0x3f3f3f3f;

const int N=1e5+100;

struct Node
{
	int to,w;
	Node(int to,int w):to(to),w(w){}
};

vector<Node>node[N];

vector<int>p[N*32],a;

int trie[N*32][2],cnt=1;

int newnode()//动态初始化
{
	cnt++;
	trie[cnt][0]=trie[cnt][1]=0;
	return cnt;
}

void insert(int x)//字典树的插入
{
	int pos=1;
	for(int i=30;i>=0;i--)
	{
		int to=(x>>i)&1;
		if(!trie[pos][to])
			trie[pos][to]=newnode();
		pos=trie[pos][to];
	}
	p[pos].push_back(x);
}

int search(int x,int pos,int deep)//字典树的查询,x:需要查询的值,pos:当前根节点,deep:深度
{
	int ans=0;
	for(int i=deep;i>=0;i--)
	{
		int to=(x>>i)&1;
		if(trie[pos][to])
			pos=trie[pos][to];
		else
		{
			pos=trie[pos][!to];
			ans|=(1<<i);
		}
	}
	return ans;
}

void dfs(int u,int fa,int val)
{
	a.push_back(val);
	for(auto it:node[u])
	{
		int v=it.to,w=it.w;
		if(v==fa)
			continue;
		dfs(v,u,val^w);
	}
}

int query(int ls,int rs,int deep)//在ls的子树和rs的子树中找到权值最小的边
{
	int ans=INT_MAX;
	if(ls>rs)
		swap(ls,rs);
	for(int i=0;i<p[ls].size();i++)//暴力枚举大小较小的一个点集,logn去另一个点集中查询
		ans=min(ans,search(p[ls][i],rs,deep-1));
	return ans;
}

LL solve(int rt,int deep)//分治,rt:根节点,deep:深度
{
	LL ans=0;
	int ls=trie[rt][0],rs=trie[rt][1];
	if(ls)//记录左子树中连通块的答案
		ans+=solve(ls,deep-1);
	if(rs)//记录右子树中连通块的答案
		ans+=solve(rs,deep-1);
	if(ls&&rs)//如果需要合并,找到异或和最小的边进行连接
		ans+=query(ls,rs,deep)+(1<<deep);
	p[rt].insert(p[rt].end(),p[ls].begin(),p[ls].end());//暴力维护点集
	p[rt].insert(p[rt].end(),p[rs].begin(),p[rs].end());
	p[ls].resize(0),p[rs].resize(0);//别忘了重置
	return ans; 
}

int main()
{
#ifndef ONLINE_JUDGE
//  freopen("data.in.txt","r",stdin);
//  freopen("data.out.txt","w",stdout);
#endif
//  ios::sync_with_stdio(false); 
	int n;
	scanf("%d",&n);
	for(int i=1;i<n;i++)
	{
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		u++,v++;
		node[u].push_back(Node(v,w));
		node[v].push_back(Node(u,w));
	}
	dfs(1,-1,0);//将边权转换为点权
	sort(a.begin(),a.end());
	a.erase(unique(a.begin(),a.end()),a.end());//离散化去重
	for(auto v:a)//将不同的点插入到字典树中
		insert(v);
	printf("%lld\n",solve(1,30));










    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_45458915/article/details/107603404