01Trie
补算法。
前置或许是字典树?但没什么必要,我们新定义一种树结构,每个点的儿子存 \(0/1\),这样我们就可以把一个整数二进制拆分扔上去,这就是 01trie。
直接来一个板题吧。
P4551 最长异或路径
给定一棵带权树,求树上最大路径异或和。
\(n\leq 10^5,V < 2^{31}\),其中 \(V\) 表示值域。
不难写出 \(a_i\) 表示从根到 \(i\) 的异或和,那么我们记路径两端 \(u,v\),路径异或和即 \(a_u \oplus a_v\),因为重合边都异或掉了。
问题就转成了序列上任意取两个值的最大异或值,这就是 01trie 的板子。
先将所有数扔到 trie 树上,然后考虑枚举一个 \(a_u\),记为 \(k\),从最高位,也就是树的根开始按位决策,假设 \(k\) 在当前位下为 \(1\),那么我们向 \(0\) 的儿子走显然最优,反之亦然,这样一直走到叶子就好了,最后统计所有值的最大数就是答案,复杂度是 \(O(n\log V)\)。
因为是从统一的最高位开始决策,所以字符串写法需要在前面补 \(0\),如果是直接算就不用管了。
此处给一个非递归写法。
const ll N=1e6+5,M=2e4+5,mod=1e9+7;
ll n,a[N],root=1,id=1,tr[N][2];
vector<pii> g[N];
inline void dfs(ll x,ll fa){for(pii vv:g[x]) if(vv.fi!=fa) a[vv.fi]=a[x]^vv.se,dfs(vv.fi,x);}
inline void insert(ll k)
{
	ll now=root;
	Fo(31,i,0)
	{
		ll num=(k>>i)&1;
		if(tr[now][num]) now=tr[now][num];
		else tr[now][num]=++id,now=id;
	}
}
inline ll ask(ll k)
{
	ll now=root,sum=0;
	Fo(31,i,0)
	{
		ll num=(k>>i)&1;
		if(tr[now][num^1]) now=tr[now][num^1],sum+=(1<<i)*(num^1);
		else now=tr[now][num],sum+=(1<<i)*num;
	}
	return sum^k;
}
signed main(){
	read(n);ll u,v,w;fo(1,i,n-1) read(u),read(v),read(w),g[u].pb({v,w}),g[v].pb({u,w});dfs(1,0);
	fo(1,i,n) insert(a[i]);ll maxn=0;fo(1,i,n) maxn=max(maxn,ask(a[i]));wr(maxn),pr;
	return 0;
}
P6018 [Ynoi2010] Fusion tree
给定一棵树,每个点有权,支持三个操作:
1 x对 \(x\) 的父亲及儿子加一
2 x v对 \(x\) 减 \(v\)
3 x查询 \(x\) 的父亲及儿子的权值异或和\(n,q\leq 5\times 10^5,a_i \leq 10^5\),保证 \(a_i\) 任意时刻非负。
一个很聪明的做法,我们把父亲单拎出来,维护其他儿子,就能避免一些很抽象的标记维护,并且保证复杂度(因为省去了被子节点修改的操作)。
看到异或和就可以往 trie 的方面想了,我们对每一个点建一棵 trie 树,那么要维护的就是 trie 树全局异或和,全局加一,对某个树减值就相当于插入和删除,所以我们只要维护这四个操作就好了,因为我们维护的全局异或和,所以要将 trie 树反过来,从低位到高位维护。
我们挨个操作来看:
- pushup(ll x)传递子树信息,设 \(sz_i\) 表示 \(i\) 这个点被经过的次数,看做 \(i\) 到父亲的那条边也是可以的,那么显然有 \(sz_i = sz_{ls} + sz_{rs}\),令 \(val_i\) 表示以 \(i\) 为根的子树异或和,因为儿子是高位,所以应该加上两个儿子 \(val\) 值的异或和当前上一位,当前位就是 \(sz_{rs}\) 个 \(1\) 全部异或起来的结果,此处默认 \(rs\) 的节点权值是 \(1\)。
inline void pushup(ll x){sz[x]=sz[ch[x][0]]+sz[ch[x][1]];val[x]=(val[ch[x][0]]^val[ch[x][1]])<<1|(sz[ch[x][1]]&1);}
- ins(ll &x,ll k,ll dep=0)插值,因为是从低位到高位所以 \(dep\) 从 \(0\) 开始,而且我们是对每个点开 trie 树,还是要维护一下动态开点的。
inline void ins(ll &x,ll k,ll dep=0){x?x:x=++tot;if(dep==M) sz[x]++;else ins(ch[x][(k>>dep)&1],k,dep+1),pushup(x);}
- del(ll &x,ll k,ll dep=0)删除,同插入,只是需要改成减 \(sz\) 就好了。
inline void del(ll &x,ll k,ll dep=0){if(!x);else if(dep==M) sz[x]--;else del(ch[x][(k>>dep)&1],k,dep+1),pushup(x);}
- rev(ll x)区间加一,考虑加一的过程,从低到高,在找到第一个 \(0\) 之前,所有的 \(1\) 都会变成 \(0\),到 trie 树也就等价于交换左右子树,对于第一个 \(0\),从 \(0\) 到 \(1\),其实也是交换,所以函数名叫了 rev。。
inline void rev(ll x){if(!x) return;swap(ch[x][0],ch[x][1]);rev(ch[x][0]);pushup(x);}
之后就做完了,注意操作时需要关心当前点有无父亲和 \(lazy_tag\) 的影响。
复杂度 \(O(n \log V)\),全部代码只有 26 行。
const ll N=5e5+5,M=21,mod=1e9+7;
ll n,m,a[N],fa[N],lz[N],val[N*M],sz[N*M],rt[N],ch[N*M][2],tot;vector<ll> g[N];
inline void dfs(ll x,ll f){fa[x]=f;for(ll y:g[x]) if(y!=f) dfs(y,x);}
inline void pushup(ll x){sz[x]=sz[ch[x][0]]+sz[ch[x][1]];val[x]=(val[ch[x][0]]^val[ch[x][1]])<<1|(sz[ch[x][1]]&1);}
inline void ins(ll &x,ll k,ll dep=0){x?x:x=++tot;if(dep==M) sz[x]++;else ins(ch[x][(k>>dep)&1],k,dep+1),pushup(x);}
inline void del(ll &x,ll k,ll dep=0){if(!x);else if(dep==M) sz[x]--;else del(ch[x][(k>>dep)&1],k,dep+1),pushup(x);}
inline void rev(ll x){if(!x) return;swap(ch[x][0],ch[x][1]);rev(ch[x][0]);pushup(x);}
inline ll qry(ll x){return fa[x]?lz[fa[x]]+a[x]:a[x];}
inline void Mod(ll x,ll k){(fa[x]?del(rt[fa[x]],qry(x)):void()),a[x]+=k,(fa[x]?ins(rt[fa[x]],qry(x)):void());}
inline void Upd(ll x){lz[x]++,(fa[x]?Mod(fa[x],1):void()),rev(rt[x]);}
inline void Ask(ll x){wr(val[rt[x]]^qry(fa[x])),pr;}
signed main(){
	read(n),read(m);ll u,v,op,x,y;fo(1,i,n-1) read(u),read(v),g[u].pb(v),g[v].pb(u);dfs(1,0);
	fo(1,i,n) read(a[i]),fa[i]?ins(rt[fa[i]],a[i]):void();
	fo(1,i,m){read(op),read(x);(op==1?Upd(x):(op==2?read(y),Mod(x,-y):Ask(x)));}
	return 0;
}
P6623 [省选联考 2020 A 卷] 树
- merge(ll &a,ll b)01trie 合并,其实是跟线段树合并差不多的,但是这种写法不大好,建议在线题就换成新建点,不然逆天错误就很抽象了。
inline void merge(ll &x,ll y){if(!y) return;if(!x) return x=y,void();val[x]^=val[y],sz[x]+=sz[y];merge(ch[x][0],ch[y][0]),merge(ch[x][1],ch[y][1]);}
这个题直接从叶子开始枚举,支持字典树全局加一,合并,查询异或和就做完了。
P4735 最大异或和
可持久化 01trie 板子,感觉可持久化很好写啊,比主席树友好多了。
先转成前缀形式,然后就变成在 \(i \in [l-1,r-1]\) 中找一个 \(s_i \oplus s_n \oplus x\) 的最大值了,扔到 trie 树上搞,令 \(k = s_n \oplus x\),从最高位开始,如果当前位 \(k\) 是 \(1\) 的话,我们走 \(0\),最好,但是要看 \(0\) 那个子树有没有点,只要拿 \(r-1\) 那颗 trie 树和 \(l-2\) 那棵树一减就好了,其余可持久化部分都跟线段树差不多,就是每次只更新一条链,来保证空间。
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f3f3f3f3f
#define inf 0x3f3f3f3f
#define pr putchar('\n')
#define fi first
#define se second
#define pp putchar(' ')
#define pii pair<ll,ll>
#define pdi pair<ll,ll>
#define mem(aa,bb) memset(aa,bb,sizeof(aa))
#define fo(a,i,b) for(register ll i = a ; i <= b ; ++ i )
#define Fo(a,i,b) for(register ll i = a ; i >= b ; -- i )
#define pb push_back
//#pragma GCC optimize(2)
using namespace std;
typedef int ll;
//typedef long long ll;
//typedef __int128 ll;
typedef double db;
inline void read(ll &opp){ll x=0,t=1;char ch;ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-'){t=-1;}ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}opp=x*t;return; }
inline void wr(ll x){if(x<0){putchar('-');x=-x;}if(x>9){wr(x/10);}putchar(x%10+'0');}
const ll M=25,M1=3e5,N=M1<<6,mod=1e9+7;
ll n,m,a[M1],ans,rt[M1],val[N],sz[N],ch[N][2],tot;
inline void ins(ll &x,ll las,ll k)
{
	x=++tot;ll now=x;
	sz[now]=sz[las]+1;
	Fo(M-1,i,0)
	{
		ll num=(k>>i)&1; 
		ch[now][num]=++tot;
		ch[now][num^1]=ch[las][num^1];
		now=ch[now][num],las=ch[las][num];
		sz[now]=sz[las]+1;
	}
}
inline ll ask(ll now,ll las,ll k)
{
	ll sum=0;
	Fo(M-1,i,0)
	{
		sum<<=1;ll num=(k>>i)&1;
		if(sz[ch[now][num^1]]-sz[ch[las][num^1]]>0) sum|=1,now=ch[now][num^1],las=ch[las][num^1];
		else now=ch[now][num],las=ch[las][num];
	}
	return sum;
}
signed main(){
	read(n),read(m);ins(rt[0],0,0);
	fo(1,i,n){ll x;read(x),s^=x,ins(rt[i],rt[i-1],s);}
	fo(1,i,m)
	{
		char ch;cin>>ch;ll l,r,x;
		if(ch=='A') read(x),++n,s^=x,ins(rt[n],rt[n-1],s);
		else read(l),read(r),read(x),wr(ask(rt[r-1],l>=2?rt[l-2]:0,s^x)),pr;
	}
	return 0;
}

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号