数位树
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数 N。
第二行输入 N 个整数 A1~AN。输出格式
输出一个整数表示答案。数据范围
1≤N≤105,
0≤Ai<231
输入样例:
3
1 2 3
输出样例:
3 -
题目来源:https://www.acwing.com/problem/content/145/
题目分析:
- 暴力:
双层循环
第一层循环遍历所有数i
第二层循环遍历其他数j
用变量maxx记录每次两数异或结果
时间复杂度:O(n2),最差达到1010
cpp1s内最多运算107 - 108,即千万到亿次
严重超时 - 数位树:
我们已经学习过了字典树,将一个字符串通过遍历存储进入字典树,通过遍历查询一个字符串
现在我们可以将一个32位的int存储进入相似的数位树,同样通过遍历插入和查询 - 下面我们来讲解数位树,将时间复杂度降低到O(231N)
算法原理:
模板算法:
- 传送门:字典树Trie
- 数位树的讲解依托于字典树的基础,还未看字典树的同学一定要先去看字典树
数位树:
1. 存储形式:
- 将int的每一位视作字典树的一个节点
一个int有32位,
可以在数位树中将其视为是一个长度为32的字符串,
且每个字符只有0 & 1两种取值 - 数位库:
有N个int,每个int有32位,每一位有两种取值,共计32*N个节点
建立二维数组:tree[32*N][2]; - 节点序号:
继续idx,由于所有数都是正数,最高位都是0
可以将idx=0的树根视作这个最高位的0
每个数剩余部分只有31位,需要31个节点 - 结束数组:
不需要了,每个int加符号位必然是32位 - 公式:
tree[第i位][1] = 第i-1位
2. 最少比较次数:
- 对一堆数进行两两比较
两层for()循环造成冗余
其实只需要每个新来者和每个已到者进行比较,最终两两之间就都比较过了
3. 最大异或值:
- 每次给定一个数,求其最大异或对
这个最大异或对只需要满足优先较高位和该数较高位不同,异或结果为1即可
4. 时间复杂度分析:
- 对于每一个新到的数
插入需要O(31)
查询需要O(31) - 对于全体数
共计O(2*31*N),百万量级,1s可以通过
写作步骤:
1. 初始化:
- tree[N*32][2] = 0;
- idx = 0;
2. 插入数据:
- 从高位到低位读取新到者的每一位,插入数位树
- 每个新到的数插入31次即可,毕竟树根已经代表符号位
3. 遍历比较数据:
- 从高位的树根向下遍历已经建立好的数位树
- 满足最大异或对的遍历顺序:
当前节点为第i位,则最好当前节点逻辑值和新到数的第i位不同;若只能相同也没有办法
4. 记录当前异或的最大结果
- 每次新到一个数,插入&遍历比较结束后,记录最大值
- 当所有数都已到达时,最大值确定
代码实现:
#include<iostream>
#include<algorithm>
using namespace std;
int const N = 31*100010;
int tree[N][2],idx;
int maxx = 0;
void insert(int x){
int p=0;
for(int i=30;i>=0;i--){
int u=x>>i&1;
if(!tree[p][u]) tree[p][u]=++idx;
p=tree[p][u];
}
}
int search(int x){
int p=0;
int res=0;
for(int i=30;i>=0;i--){
int u=x>>i&1;
if(tree[p][!u]) {
//此处也可res += 1<<i; 则异或为0时不必看res了
res=res*2+1;
p=tree[p][!u];
}else{
res=res*2+0;
p=tree[p][u];
}
}
return res;
}
int main()
{
int n = 0;
cin.tie(0);
cin>>n;
while(n--){
int x = 0;
cin>>x;
insert(x);
maxx = max(maxx, search(x));
}
cout<<maxx<<endl;
}
代码误区:
1. 数位树的节点存储的是什么?
- 存储节点idx下标,借由数位库tree[31*N][2],每个节点延伸出两条支路 0 & 1
2. 为什么本题输入无负数?
- 负数 ^ 正数 = 负数
- 正数 ^ 正数 = 正数
- 负数 ^ 负数 = 负数
- 最大值必然在正数组内31位异或结果 或 负数组内31位异或结果
- 最小值必然在正数去与负数数位树异或结果 或 负数去与正数数位树异或结果 中(前提是正负数都有哈)
3. 数位树的节点个数问题:
- 经过上面的分类讨论,我们发现正负数可以各自建树,且都只需要31位即可
- 所以总节点个数为 N * 31,比较危险了,int数组长度开到了106
4. 异或结果记录:
- 加法记录:
- 一位上存在不同0/1,则这一位上异或结果是1,res += 1<<i;
- 乘法记录:
- 一位上存在不同0/1,则这一位上异或结果是1,res = res*2 + 1;
- 一位上不存在不同0/1,则这一位上异或结果是0,res = res*2;
本篇感想:
- 看完本篇博客,恭喜已登 《练气境-后期》
练气境仅余并查集和堆,诸君加油
预计到35篇入图论,对dfs bfs不是很熟悉的同学看这里:【算法设计】用C++类和队列实现图搜索的广度优先遍历算法
距离登仙境不远了,加油