一.01字典树
主要可以直接求解x和一组数中最小的异或后的值,或最大异或值;从而对许多异或求最值问题很有用处,一般可以直接套上板子求得想要的答案;
01字典树是一棵最多31层的二叉树,其每个节点的两条边分别代表二进制的某一位的值为0还是1.将某个路径上的值连起来就得到一个二进制串
1.模板:
for循环版本:采用for循环进行插入与查询操作,只是递归可以实现更多的操作,可以进行回溯操作,而for循环实现比较麻烦;
中间采用30位进行for循环,刚好可以对int为里的31的最左边以为移到第一位进行异或取并判断是否为1或0;
struct Trie {
int n,idx;
vector<vector<int>> Tr;
Trie (int n) : n(n), Tr((n + 1) * 30,vector<int>(2,0)), idx(0) {}
inline void insert(int x)
{
int p = 0;
for(int i = 30;i >= 0;i --) {
int nt = x >> i & 1;
if(!Tr[p][nt]) Tr[p][nt] = ++ idx;//存储新结点位置
p = Tr[p][nt];
}
}
inline int query(int x) //与x异或后的最小值
{
int p=0,ans=0;
for(int i=30;i >=0 ;i --){
int bit=x >>i & 1;
if(Tr[p][bit]) p=Tr[p][bit];//贪心先查找相同的数,判断该位置是否存在结点,若不为0则该位置存在值可以继续向下查找;
else p=Tr[p][bit^1],ans+=1<<i;//若要求的是异或后的最大值,则需将if内改为Tr[p][bit^1],先判断不同的使得异或最大;
}
return ans;
}
};
Trie tr(n);//定义一个字典树
递归版本:采用递归构建的01字典树
struct Tree_01{
int n,idx;
vector<vector<int>> tr;
Tree_01 (int n) : n(n),tr((n+1)*30,vector<int>(2,0)),idx(1){}//构造器
inline void insert(int &u,int x,int k)//插入一个数
{
if(!u) u=++idx; //存储到达的位置的结点
if(k==-1) return ;
int bit=x >> k & 1;
insert(tr[u][bit],x,k-1);//递归建树
}
inline int query(int u ,int x,int k)
{
if(k==-1) return 0;
int bit=x >> k & 1;
if(tr[u][bit]) return query(tr[u][bit],x,k-1);//先找相等的,求出的为与x异或后最小值
else return query(tr[u][bit^1],x,k-1)+(1 << k);
}
};
Tree_01 tr(n);//定义一个字典树
2.应用:
对基础的01字典树的应用,简单的插入与查询直接使用的基本操作
题解:Codeforces Round 888 (Div. 3)F. Lisa and the Martians(异或,01字典树,排序)_codeforces div3 888-CSDN博客
3.进阶:
题目链接:F-最小异或对
与前一题不同,他是求解数组中任意两个数异或最小值,每次需要进行插入,删除,查询三个操作;会不断改变01字典树的结构,要是每次进行O(n*k)的时间复杂度查询(k为31)的话,时间将变为n*n*k,肯定会时间超限;
所以得采用可以进行回溯的01字典树,对与最下面的数进行更改了后进行向上转移更改最小值;这时候必须采用递归形式的01字典树才能够简单的实现回溯操作;
思路:
从高位到低位建立01Trie,每个结点都维护3个信息:
- 当前子树内能凑出的最小异或对的答案
- 子树内的元素个数
- 如果子树内只有一个元素,记录这个元素的值.
首先对于叶子结点,如果包含的元素数量不大于1,那么当前答案为无穷大,否则答案为0.
在01Trie上,除了叶子结点外每个结点都有两个儿子.那么对于当前结点选取两个元素有3种可能,在左儿子中选取两个元素,在右儿子中选取两个元素,或者左右儿子各选一个元素.
如果能在同一个儿子里选择两个元素答案一定好于在两个儿子中各选一个元素,所以可以用儿子的答案更新当前结点.
如果两个儿子结点包含的元素数量都不大于1,那么我们只能在两个儿子中各选一个元素然后更新答案.
当然如果当前子树中包含的元素数量不大于1,答案为无穷大.
添加和删除时使用类似线段树的方式从下往上更新答案,每次查询时只需输出根节点的答案即可.
可以发现这里的01Trie完全等价于动态开点的权值线段树.
每次插入和删除的复杂度为01Trie的高度,查询的复杂度为O(1),总的时间复杂度为O(nlogA),空间复杂度也为O(nlogA)
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
typedef pair<int,int> pr;
#define int long long
#define fr(i,l,r) for(int i=l;i<=r;i++)
#define pb(x) push_back(x)
#define all(a) a.begin(),a.end()
#define fi first
#define se second
const int N = 2e5+10;
const int mod=998244353,inf=LONG_LONG_MAX;
int dx[]={0,0,-1,0,1},dy[]={0,-1,0,1,0};
int n,m,k;
int a[N];
struct node{
int ans,cnt,val;
node() : ans(inf), cnt(0), val(0) {}
node(int x) : ans(inf), cnt(1), val(x) {}
}tr[32*N];
int son[32*N][2];
int idx;
node operator+(const node &a,const node &b)
{
node res=node();
res.ans=min(a.ans,b.ans);
res.cnt=a.cnt+b.cnt;
if(a.cnt==1&&b.cnt==1) res.ans=a.val ^ b.val;
if(res.cnt==1) res.val=a.cnt ? a.val : b.val;
return res;
}
void insert(int &u,int x,int k)
{
if(!u) u=++idx;
if(k==-1)
{
++tr[u].cnt;
if(tr[u].cnt==1) tr[u]=node(x);
else if(tr[u].cnt==2) tr[u].ans=0;
return ;
}
int d=(x >> k & 1);
insert(son[u][d],x,k-1);
tr[u] = tr[son[u][0]] + tr[son[u][1]];
}
void delet(int &u,int x,int k)
{
if(k==-1)
{
--tr[u].cnt;
if(tr[u].cnt==0) tr[u]=node();
else if(tr[u].cnt==1) tr[u]=node(x);
return ;
}
int d=(x >> k & 1);
delet(son[u][d],x,k-1);
tr[u] = tr[son[u][0]] + tr[son[u][1]];
}
void solve()
{
cin>>n;
int root = ++idx;//从1开始进行插入与删除,1为最上面的根结点;
while(n--)
{
int x;
string s;
cin>>s;
if(s=="ADD")
{
cin>>x;
insert(root, x, 30);
}
else if(s=="DEL")
{
cin>>x;
delet(root, x,30);
}
else
{
cout<<tr[root].ans<<endl;
}
}
}
signed main()
{
ios;
int t=1;
//cin>>t;
while(t--) solve();
return 0;
}
要是有什么问题,可以发在评论区中。