0-1 Trie

大家都知道,$\text{trie}$ 很有用,具体可以见我的 字符串笔记,而 $\text{0-1 trie}$ 就是字符集只有 ${0,1}$ 的 $\text{trie}$,可以维护各种带修的异或问题。

那默认大家都会 $\text{trie}$ 了,我们直接讲点 $\text{0-1 trie}$ 的应用。

应用 1 维护异或极值

P4551 最长异或路径

对于此题,我们先预处理出根节点到每个点的异或值,记作 $a_u$,此时一条路径 $(u,v)$ 就转化为 $(root,u)\oplus(root,v)$,异或值就是 $a_u\oplus a_v$。

正确性在于,异或两个相同数时会变成 $0$,所以从 $lca(u,v)$ 往上的重复部分都被抵消了。

此时我们把问题转化为在一个数列中找异或值最大的两个数。

所以我们以每个 $a_i$ 的二进制从高位到低位建一棵 $\text{0-1 trie}$。对于每个 $a_i$,尝试每一位都在树上寻找与这一位不同的另一个方向,这样异或以后该位为 $1$。

如果有就统计上答案,否则先跟着走。因为二进制高位更大则一定更大,所以贪心正确。

#include<bits/stdc++.h>
using namespace std;
#define F(i,x,y) for(int i=(x);i<=(y);++i)
#define Fo(i,x,y) for(int i=(x);i>=(y);--i)
const int N=1e5+10,INF=0x3f3f3f3f;
int n;
int dis[N],cnt,ans;
int t[N<<5][2];
void insert(int x){
int p=0;
Fo(i,31,0){
int c=(x>>i)&1;
if(!t[p][c]) t[p][c]=++cnt;
p=t[p][c];
}
}
int query(int x){
int p=0,res=0;
Fo(i,31,0){
int c=(x>>i)&1;
if(t[p][c^1]) p=t[p][c^1],res=res*2+1;
else p=t[p][c],res=res*2;
}
return res;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n;
F(i,1,n) cin>>dis[i],insert(dis[i]);
F(i,1,n) ans=max(ans,query(dis[i]));
cout<<ans<<endl;
return 0;
}

应用 2 维护异或和

我们先来点定义:设 $t_{u,0/1}$ 表示 $u$ 的 左右儿子,$w_u$ 表示 $u$ 子树中的点数,$s_u$ 表示以 $u$ 为根子树的异或和。

对于一些二进制数,从低位向高位建立 $\text{0-1 trie}$,然后类似动态开点线段树的方式维护。

异或和中当前位是 $1$ 的充要条件为:当前位恰有奇数个 $1$。所以我们只关心奇偶性,不关心具体的值。

  • 插入/删除操作

我们先把每一个数都往前补位补满 $0$,然后我们一直往下走,走到底以后将到根节点所有 $w$ 增加 $1$,递归往上处理信息

因为插入与删除几乎完全相同,区别只有走到底以后增加还是减少,所以合并成了一个函数。记得动态开点。

void modify(int &p,int x,int d,int op){
if(op==1&&!p) p=++cnt;
if(d>Maxh) return (void)(w[p]=w[p]+op);
modify(t[p][x&1],x>>1,d+1,op);
pushup(p);
}

代码中的 $\text{op}$ 表示插入/删除,$\text{Maxh}$ 是定义最大深度(二进制最大长度)。

  • 合并信息

无非就是合并 $w$ 和 $s$,对于 $w$,就是两个子节点的和。

$s$ 要分类讨论一下,左儿子的信息可以直接乘二合并,右儿子则要看一下 $w$ 的奇偶性。

至于为什么乘二,儿子走到父亲的时候相当于在末尾加了一位,也就是左移一位。

void pushup(int p){
w[p]=s[p]=0;
if(ls) w[p]+=w[ls],s[p]^=(s[ls]<<1);
if(rs) w[p]+=w[rs],s[p]^=(s[rs]<<1)|(w[rs]&1);
}

这里的 $\text{ls,rs}$ 是 define 的左右儿子。

  • 全局加 $1$

对于一个二进制,加 $1$ 操作实际就是从低位向高位找第一个 $0$,将其变为 $1$ 后把低位所有的 $1$ 变成 $0$。

比如 $1000111 + 1 = 1001000$。

所以我们不停地交换左右儿子(把低位 $1$ 变成 $0$)直到交换后左二子(原右儿子)不是 $1$,表示这一位一开始是 $0$。

void update(int p){
swap(ls,rs);
if(ls) update(ls);
pushup(p);
}
  • 合并 $\text{0-1 trie}$

与线段树合并相同,对于两个位置,有一个空时直接赋值为另一个,否则手动维护一下 $w$ 和 $s$。

int merge(int a,int b){
if(!a) return b;
if(!b) return a;
w[a]=w[a]+w[b];
s[a]^=s[b];
t[a][0]=merge(t[a][0],t[b][0]);
t[a][1]=merge(t[a][1],t[b][1]);
return a;
}

P6018 [Ynoi2010] Fusion tree

前面的神秘题目背景是什么(晕)

我们考虑对于每个点开 $\text{0-1 trie}$,维护其直系子节点的信息。

以下记 $tire_x$ 表示 $x$ 的那棵树,记 $a_x$ 表示 $x$ 点的值。

操作 $1$,对于儿子,我们修改 $tire_x$ 即可,对于父亲,父亲的信息保存在父亲的父亲(祖父)的 $\text{0-1 trie}$ 里,所以修改 $a_{fa_x}$ 和 $\large trie_{fa_{fa_x}}$。

这里修改祖父时直接删除原父亲值并加入一个新的修改值即可。

操作 $2$,直接修改 $a_x$,并修改 $tire_{fa_x}$ 的信息。

操作 $3$,发现直接查询 $s_x\oplus a_x$ 并不正确,因为没有考虑父亲执行操作 $1$ 时对儿子 $a$ 值的影响。

暴力修改肯定不优,我们用一个标记数组来记录此点执行了几次操作 $1$。查询时异或上即可。

细节比较多,注意数据中有 $\text{hack}$ 点,根节点默认设 $1$ 就能避免。

#include<bits/stdc++.h>
using namespace std;
#define F(i,x,y) for(int i=(x);i<=(y);++i)
#define ls t[p][0]
#define rs t[p][1]
const int N=5e5+10,M=N*23,Maxh=21;
int n,m,rt[N];
int t[M][2],w[M],s[M],cnt;
void pushup(int p){
w[p]=s[p]=0;
if(ls) w[p]+=w[ls],s[p]^=(s[ls]<<1);
if(rs) w[p]+=w[rs],s[p]^=(s[rs]<<1)|(w[rs]&1);
}
void modify(int &p,int x,int d,int op){
if(op==1&&!p) p=++cnt;
if(d>Maxh) return (void)(w[p]=w[p]+op);
modify(t[p][x&1],x>>1,d+1,op);
pushup(p);
}
void update(int p){
swap(ls,rs);
if(ls) update(ls);
pushup(p);
}
int a[N],tag[N],fa[N];
struct edge{
int v,next;
}e[N<<1];
int head[N],tot;
void add(int u,int v){
e[++tot]={v,head[u]};
head[u]=tot;
}
void dfs(int u,int f){
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
if(v==f) continue;
fa[v]=u;
dfs(v,u);
}
}
int find(int x){
if(!fa[x]) return a[x];
return tag[fa[x]]+a[x];
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
int op,x,y;
F(i,1,n-1){
cin>>x>>y;
add(x,y);
add(y,x);
}
dfs(1,0);
F(i,1,n){
cin>>a[i];
if(fa[i]) modify(rt[fa[i]],a[i],0,1);
}
while(m--){
cin>>op;
if(op==1){
cin>>x;
tag[x]++;
if(x!=1){
if(fa[fa[x]]) modify(rt[fa[fa[x]]],find(fa[x]),0,-1);
a[fa[x]]++;
if(fa[fa[x]]) modify(rt[fa[fa[x]]],find(fa[x])+1,0,1);
}
update(rt[x]);
}
else if(op==2){
cin>>x>>y;
if(x!=1) modify(rt[fa[x]],find(x),0,-1);
a[x]-=y;
if(x!=1) modify(rt[fa[x]],find(x),0,1);
}
else{
cin>>x;
int res=s[rt[x]];
res^=find(fa[x]);
cout<<res<<endl;
}
}
return 0;
}

P6623 [省选联考 2020 A 卷] 树

考虑每个点建立 $\text{0-1 trie}$,一开始存权值,使用 dfs 自底向上合并子树以后全局加,表示距离的增加。

有没有人解释一下数据范围有什么特殊含义 qwq。

#include<bits/stdc++.h>
using namespace std;
#define F(i,x,y) for(int i=(x);i<=(y);++i)
#define ls t[p][0]
#define rs t[p][1]
const int N=525100,M=N*23,Maxh=21;
int n,rt[N],a[N];
int t[M][2],w[M],s[M],cnt;
long long ans;
void pushup(int p){
w[p]=s[p]=0;
if(ls) w[p]+=w[ls],s[p]^=(s[ls]<<1);
if(rs) w[p]+=w[rs],s[p]^=(s[rs]<<1)|(w[rs]&1);
}
void modify(int &p,int x,int d){
if(!p) p=++cnt;
if(d>Maxh) return (void)(w[p]++);
modify(t[p][x&1],x>>1,d+1);
pushup(p);
}
int merge(int a,int b){
if(!a) return b;
if(!b) return a;
w[a]=w[a]+w[b];
s[a]^=s[b];
t[a][0]=merge(t[a][0],t[b][0]);
t[a][1]=merge(t[a][1],t[b][1]);
return a;
}
void update(int p){
swap(ls,rs);
if(ls) update(ls);
pushup(p);
}
struct edge{
int v,next;
}e[N<<1];
int head[N],tot;
void add(int u,int v){
e[++tot]={v,head[u]};
head[u]=tot;
}
void dfs(int u){
for(int i=head[u];i;i=e[i].next){
int v=e[i].v;
dfs(v);
update(rt[v]);
rt[u]=merge(rt[u],rt[v]);
}
ans+=1ll*s[rt[u]];
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n;
F(i,1,n) cin>>a[i],modify(rt[i],a[i],0);
int x;
F(i,2,n) cin>>x,add(x,i);
dfs(1);
cout<<ans<<endl;
return 0;
}