问题:888G - 8
题目大意:
你有一个含有 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<bits/stdc++.h>
#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<<pos这么个操作
枚举左子树中经过x结点的所有数异或上以y为根节点的右子树,在query操作中并没有把y这个根给加进去
但是,事实上,这个根也是有贡献的,且是必不可少的,不加的话,那不就是少考虑了嘛
要考虑全面呢,是左子树中的所有值和右子树中的所有值合并 ,注意是所有,根当然包含在在这个"所有"之内了
*/
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类型的数据。