【Trie树&分治&异或】Codeforce- 888G - 8

问题:888G - 8

问题传送门
【Trie树&分治&异或】Codeforce- 888G - 8_第1张图片

题目大意:

你有一个含有 n n n 个点的完全图,每一个顶点有一个权值,在这张图中,连接两个顶点的边权值是这两个顶点的点权值的异或和,接着在这张完全图中求最小生成树,输出最小生成树的数值。

解题思路:

(1)暴力法:每一条边的边权已知,利用最小生成树算法( p r i m prim prim k r u s k a l kruskal kruskal都可)求最小生成树。
分析数据范围, n n n 的范围是 2 e 5 2e5 2e5,该图为完全图,任意两个点之间都有连线,所以总的边数有 4 e 10 4e10 4e10。上述两个算法都需要枚举所有的边,因此必然会得到 T L E TLE TLE
(2) T r i e Trie Trie树优化法: i n s e r t insert insert 函数和 q u e r y query query 函数都是 t r i e trie trie的模板,在此就不细说了,关键是这个分治是绝对妙啊,开两个数组L和R数组来维护每一层上的涉及到的数值,排序保证高层上的区间包含低层上的区间;
(1)对于既有左子树又有右子树的情况,
贡献值 = = = 左子树贡献值 + + + 右子树贡献值 + + + 左右子树合并产生的贡献。
其中左右子树的贡献分别递归求出,左右子树合并产生的贡献可看作,左子树中的某个点连接在右子树下的某个点上,要保证异或和最小,因此需要暴力枚举左子树的所有点异或上右子树取最小值,这里需要注意一点就是:是左子树中的某一点与右子树下的某一点相连,这个过程并没有包含右子树的那个根,因此需要加上那个根的贡献。
(2)对于只有左子树或只有右子树的情况,递归求得即可;
(3)递归边界不能忘,当只有一个点的时候,也就是说没有左右子树,无需连边,贡献为 0 0 0 即可。
接下来放上代码,在代码中有很多注释,根据注释会更容易理解一点。

上代码:

#include
#pragma GCC optimize(3)
using namespace std;
typedef long long ll;
const int N = 6000010;
/*对于N的范围,考虑最大的情况,
当n取200000时,对于每个数有30位,每一位有两个子节点
总的结点个数200000*30*2=6e6个,因此开6e6+10就很保险 
*/
int son[N][2],idx,L[N],R[N];
/*son[x][0]表示x结点的左子节点,son[x][1]表示x结点的右子结点 
L[x]和R[x]存储经过x结点的所有数值的下标  
*/ 
ll a[N];
inline ll read()//快读  
{
    ll  x=0;
    bool f=0;
    char ch=getchar();
    while (ch<'0'||'9'<ch)    f|=ch=='-', ch=getchar();
    while ('0'<=ch && ch<='9')
        x=x*10+ch-'0',ch=getchar();
    return f?-x:x;
}
void insert(ll x,int id)//在字典树中插入新的数值  
{
	int p = 0;
	for(int i = 31;i >= 0;i--)
	{
		if(!son[p][x >> i & 1]) son[p][x >> i & 1] = ++idx;
		p = son[p][x >> i & 1];
		if(!L[p]) L[p] = id;
		R[p] = id;
	}
}
ll query(int p,int pos,ll x)//查询当前数值x与以p为根的字典树结合的最小值  
{
	ll res = 0;
	for(int i = pos;i >= 0;i--)
	{
		int s = x >> i & 1ll;
		if(son[p][s]) p = son[p][s];
		//字典树中存在与x的第位相同的话就走相同的这一位,否则走另外一侧 
		//因为相同的数值异或之后为0,要保证最小,此贪心方案最佳 
		else{
			p = son[p][!s];
			res += (1ll << i);
		}
	}
	return res;
}
ll divide(int p,int pos)//分治求最小生成树的值  
{
	if(son[p][0] && son[p][1])
	{
		int x = son[p][0],y = son[p][1];
		ll min1=0x3f3f3f3f;
		for(int i = L[x];i <= R[x];i++) min1 = min(min1,query(y,pos - 1,a[i]) + (1ll << pos));
		/*注意优先级,'+'优先级高于'<<'所以需要加上括号
		对了,还有就是为什么要加上1< 
		return min1 + divide(son[p][0],pos - 1) + divide(son[p][1],pos - 1);
		//左右子树都存在的情况下,总的贡献值 = (左子树内部贡献值)+(右子树内部贡献值)+(左右子树合并所产生的贡献值)
	}
	else if(son[p][0]) return divide(son[p][0],pos - 1);
	//递归求左子树的贡献 
	else if(son[p][1]) return divide(son[p][1],pos - 1);
	//递归求右子树的贡献 
	return 0;//递归边界一定不能少,也就是说既没有左子树也没有右子树,当然也无需连边,贡献为0即可 
}

int main()
{
	ll n;
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	sort(a+1,a+n+1);
	/*排序这一操作就很妙,它可以保证L和R数组内存储的值具有包含性,方便递归的时候层层推进
	怎么说呢,在这个代码中,对于每个数字,我都是枚举31位;
	从下往上看,层数越高,位越高;
	从上往下看也就是 从高位到低位 
	L[高位的指针]~R[高位的指针]一定包含L[低位的指针]~R[低位的指针]
	可类比与线段树的那个区间划分来理解,但不完全相同 
	*/ 
	for(int i=1;i<=n;i++) insert(a[i],i);
	printf("%lld\n",divide(0,31));
	return 0;
}

代码放上后,再来补充一句,算是个需要注意的点吧,因为枚举的是 31 31 31 位,所以可能会爆 i n t int int 因此要记得开 l o n g long long l o n g long long类型的数据。

你可能感兴趣的:(【Trie树&分治&异或】Codeforce- 888G - 8)