字典树专题
title: 字典树专题
categories:
- ICPC
tags:
- null
abbrlink: 5f205474
date: 2023-07-15 00:00:00
01 trie
找序列中任意两数的最大异或和
int n, m;
int a[N];
int idx=0;
int ch[N*31][2];
void insert(int x){
int p=0;
for(int i=30;i>=0;i--){
int u=(x>>i)&1;
if(!ch[p][u])ch[p][u]=++idx;
p=ch[p][u];
}
}
int query(int x){
int p=0;int ans=0;
for(int i=30;i>=0;i--){
int u=(x>>i)&1;
if(ch[p][!u]){
p=ch[p][!u];
ans+=1<<i;
}
else p=ch[p][u];
}
return ans;
}
void solve(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
insert(a[i]);
}
int ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,query(a[i]));
}
cout<<ans<<endl;
}
奶牛异或
题意:找出最大区间异或和,相同的时候取右端点最小的,再相同取长度最短的
Solution:利用异或的可逆性,先算区间异或前缀和,问题转化回上题,注意处理边界,对于只有一个数的情况,直接赋初值解决
int n, m;
int a[N];
int idx=0;
int ch[N*31][2];
void insert(int x){
int p=0;
for(int i=20;i>=0;i--){
int u=(x>>i)&1;
if(!ch[p][u])ch[p][u]=++idx;
p=ch[p][u];
}
}
int query(int x){
int p=0;int ans=0;
for(int i=20;i>=0;i--){
int u=(x>>i)&1;
if(ch[p][!u]){
p=ch[p][!u];
ans+=1<<i;
}
else p=ch[p][u];
}
return ans;
}
void solve(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++)a[i]^=a[i-1];
int ans=a[1];
map<int,int>mp;
int ansl=1,ansr=1;
mp[0]=0;
for(int i=1;i<=n;i++){
if(i>=2){
int tmp=query(a[i]);
if(ans<tmp){
ans=tmp;
ansr=i;
ansl=mp[tmp^a[i]]+1;
}
}
insert(a[i]);
mp[a[i]]=i;
}
cout<<ans<<" "<<ansl<<" "<<ansr<<endl;
}
继续深入这个快速找异或和最大值的算法,这次将目标转化到树上 The XOR-longest Path
题意:给定一棵n个点的带边权树,求树上最长的异或和路径。
Solution:首先考虑将问题转化成点之间的问题,我们做从根开始的树上异或前缀和d[u],对于任意一条路径u到v的异或和都恰好是$$d[u]\oplus d[v]$$,其中lca的部分由于异或特性恰好抵消。
int n, m;
int a[N];
int idx=0;
int ch[N*31][2];
void insert(int x){
int p=0;
for(int i=30;i>=0;i--){
int u=(x>>i)&1;
if(!ch[p][u])ch[p][u]=++idx;
p=ch[p][u];
}
}
int query(int x){
int p=0;int ans=0;
for(int i=30;i>=0;i--){
int u=(x>>i)&1;
if(ch[p][!u]){
p=ch[p][!u];
ans+=1<<i;
}
else p=ch[p][u];
}
return ans;
}
struct edge{int v,w;};
int d[N];
vector<edge>e[N];
void dfs(int u,int fa){
for(auto [v,w]:e[u]){
if(v==fa)continue;
d[v]=d[u]^w;
dfs(v,u);
}
}
void solve(){
cin>>n;
for(int i=1;i<=n-1;i++){
int u,v,w;cin>>u>>v>>w;
e[u].push_back({v,w});
e[v].push_back({u,w});
}
dfs(1,0);
for(int i=1;i<=n;i++)insert(d[i]);
int ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,query(d[i]));
}
cout<<ans<<endl;
}
Vitya and Strange Lesson
https://codeforces.com/problemset/problem/842/D
问题描述
$mex$ 是一个序列中没有出现过的最小非负整数。
给出你一个长度为 $n$ 的非负整数序列以及 $m$ 个询问,每次询问先给你一个整数 $x$ ,然后:
- 把序列中所有数异或上 $x$
- 输出序列的 mex
注意,在每个询问过后序列是发生变化的。
Solution:考虑异或的结合律
由于异或是对二进制的每一位单独进行操作,每一位之间相互不会影响,所以我们考虑每个数字的二进制表示并对每一位进行思考。
对于询问的一个数 x,在原来的字典树上,假设此时本来应该向左儿子走,但是如果 x 的这一位是 1,即相当于进行左右子树的交换操作,那么我们就应该调转方向,向右儿子走。
如果现在输入的是 $x_{i}$,那么现在真正进行操作的 x 就应该是 $x=x1⊕x2⊕x3…xi−1⊕xi$
所以现在已经解决了 m 个查询的问题,只需要思考如何在 01Tire
上查找 mex,这比较简单,只需要判断当前子树是否为满二叉树,如果是只能调转方向,否则尽量向着 0 的方向走,以保证 mex最小的性质。
但我们还必须注意一个坑点:同一个数不能被插入两次!因为这会导致子树的大小发生变化,造成答案错误。
积累:mex具有二分性,性质是前mex-1个数是不是都出现过
实现方面值得一提的是query内部的逻辑,也是本题的核心。
- 考虑当前二叉树已经较小的一边二叉树已经满了,我们走到相反方向,发现如果这个点不存在,那依然加上当前为贡献,但mex是最小的,所以后面位全是0,直接return。
- 另一方面,如果当前二叉树没满,我们走入较小部分,如果也不存在这个节点,那只可能说明这里面一个数都没有。反证法:若存在x在面,那建立$trie$的时候就会把这个节点建出来。所以说明当前以及后面位全是0才是正确的。
int n, m;
int a[N];
int idx=0;
int ch[N*31][2];
bool st[N];
int cnt[N*31];
void insert(int x){
int p=0;
for(int i=30;i>=0;i--){
int u=(x>>i)&1;
if(!ch[p][u])ch[p][u]=++idx;
p=ch[p][u];cnt[p]++;
}
}
//里面一个数都没有的时候就是答案找到的时候
int query(int x){
int p=0;int ans=0;
for(int i=30;i>=0;i--){
int u=(x>>i)&1;
int nx=ch[p][u];
if(cnt[nx]<(1<<i)){
p=ch[p][u];
if(p==0)return ans;
}
else {
p=ch[p][!u];
ans|=(1<<i);
if(p==0)return ans;
}
}
return ans;
}
void solve(){
cin>>n;cin>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
if(st[a[i]]==0){
st[a[i]]=true;
insert(a[i]);
}
}
int sum=0;
for(int i=1;i<=m;i++){
int x;cin>>x;
sum^=x;
int ans=query(sum);
cout<<ans<<endl;
}
}
VK Cup 2018 - Round 1C
题意:给出a,b两个序列,要求调整b序列的顺序,要求调整后a序列异或上b序列的结果字典序最小
Solution:考虑我们需要让前面的尽可能小,所以我们先将b序列中的树全部插入到trie里,每次按顺序为a中数找出异或后最小的数。值得注意的是每次确定当前位的策略的时候需要删掉当前点的编号的出现次数。
nt n, m;
int a[N];
int cnt[N*31];
int idx=0;
int ch[N*31][2];
void insert(int x){
int p=0;
for(int i=30;i>=0;i--){
int u=(x>>i)&1;
if(!ch[p][u])ch[p][u]=++idx;
p=ch[p][u];
cnt[p]++;
}
}
int query(int x){
int p=0;int ans=0;
for(int i=30;i>=0;i--){
int u=(x>>i)&1;
if(ch[p][u]&&cnt[ch[p][u]]){
p=ch[p][u];
cnt[p]--;
//ans+=1<<i;
}
else{p=ch[p][!u];
cnt[p]--;
ans+=1<<i;
};
}
return ans;
}
void solve(){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++){
int x;cin>>x;
insert(x);
}
for(int i=1;i<=n;i++){
cout<<query(a[i])<<" ";
}
}
Xor-MST
最小异或生成树:给出n个点,任意两个点连边的代价是两个点权值异或的结果,求最小生成树
Solution:感觉还是有点模糊。大概是字典树上分治,我们考虑每个字典树上有分叉的点,对于每个分叉的点两边想要连通必须在当前这位发出1<<d的代价,至于下面的位我们考虑贪心,在size小的一边对每个数在另一边寻找最适合(也就是结果最小)自己的匹配的数,对于这个过程由于每次都是从当前位的下一位开始循环,这里给出一种递归的查询写法。
注意事项:不能全部开longlong,会mle,反思看哪些地方需要用longlong,还有就是如果字典树封装好的空间会优化当前
int n, m;
int a[N];
int ch[N*30][2];
int idx=0;
vector<int>e[N*30];
ll ans=0;
void insert(int x){
int p=0;
for(int i=29;i>=0;i--){
int u=(x>>i)&1;
if(!ch[p][u])ch[p][u]=++idx;
p=ch[p][u];
e[p].push_back(x);
}
}
int query(int p,int d,int x){
if(d<0)return 0;
int u=(x>>d)&1;int res=0;
if(ch[p][u]){
p=ch[p][u];
res+=query(p,d-1,x);
}
else {
p=ch[p][!u];
res+=1<<d;
res+=query(p,d-1,x);
}
return res;
}
void mdiv(int p,int d){
if(d<0)return ;
int ls=ch[p][0],rs=ch[p][1];
if(ls&&rs){
int mn=inf;
if(e[ls].size()>e[rs].size())swap(ls,rs);
for(auto x:e[ls]){
mn=min(mn,query(rs,d-1,x));
}
ans+=(ll)mn+(1LL<<d);
}
if(ls)mdiv(ls,d-1);
if(rs)mdiv(rs,d-1);
}
void solve(){
cin>>n;
for(int i=1;i<=n;i++){
int x;cin>>x;
insert(x);
}
mdiv(0,29);
cout<<ans<<endl;
}
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 爱飞鱼的blog!