Description
给出n个非负整数A[1]..A[n],编程回答询问:l r x:询问 max {x xor A[i] | l<=i<=r}的值。
Input
第一行为整数n。 第二行为n个非负整数,表示A[1],A[2],…,A[n]。 第三行为整数m。 之后的m行,每行表示一种询问:l r x(1<=l<=r<=n)
Output
对于每个询问,给出你的回答。
学到Trie树的基本操作,便大概了解了一下可持久化Trie树。
还有很多细节没理解到位,先结合代码写一下笔记加深理解好了…如果有错误或好的理解方法还请评论指出。
对于这个题,与单个求最大异或值思想相同,贪心从最高位看对应的^能否取到。不同在于这题改为了区间求最大异或值。
我先贴出来:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef unsigned long long ull;
const int MAXN=500005;
const int MAXNODE=MAXN*64;
int ch[MAXNODE][2];
int root[MAXN];
int np;
ull sz[MAXNODE];
char c;
void scan(ull &x)
{
for(c=getchar();c<'0'||c>'9';c=getchar());
for(x=0;c>='0'&&c<='9';c=getchar()) x=x*10+c-'0';
}
char num[50];int ct;
void print(ull x)
{
ct=0;
if(x==0) num[ct++]='0';
while(x) num[ct++]=x%10+'0',x/=10;
while(ct--) putchar(num[ct]);
putchar('\n');
}
void init()
{
memset(sz,0,sizeof(sz));
memset(root,0,sizeof(root));
memset(ch,0,sizeof(ch));
np=0;
}
void pushup(int now)
{
sz[now]=sz[ch[now][0]]+sz[ch[now][1]];
}
void insert(int pre,int &now,int i,ull x)
{
now=++np;//新建一个版本
sz[now]=sz[pre];//复制前一个版本
if(i<0)
{
sz[now]++; //这个版本自己本身有一个元素
return;
}
int d=(x>>i)&1;
ch[now][d^1]=ch[pre][d^1]; //d^1子树与前一个版本共用,因为当前元素不走这边
insert(ch[pre][d],ch[now][d],i-1,x);//d子树新建 ,当前元素走的是这边
pushup(now); //更新这个节点上的值,用于后面查询判断
}
ull ques(int l,int r,int i,ull x)
{
if(i<0) return 0;
int d=(x>>i)&1;
int t=sz[ch[r][d^1]]-sz[ch[l][d^1]]; //说明r这边确实有d^1这条路(因为它比之前的版本大)
if(t>0) return ques(ch[l][d^1],ch[r][d^1],i-1,x)+((ull)1<<i);
else return ques(ch[l][d],ch[r][d],i-1,x);
}
int main()
{
ull N,M,i,j,L,R,x;
init();
scan(N);
for(i=1;i<=N;i++)
{
scan(x);
insert(root[i-1],root[i],63,x);//以元素下标作为版本号
}
cin>>M;
for(i=1;i<=M;i++)
{
scan(L);scan(R);scan(x);
print(ques(root[L-1],root[R],63,x));//询问这个版本区间
}
return 0;
}
数组大小
对于ch数组,存的是整个树的结构,最多有500000个元素,每个元素又是unsigned long long,所以每个可以分为64位,因此可能最多有500000*64个节点(与一般的二进制Tire树一样),而每个节点又可以引出0,1节点,所以二维数组ch[500005*64][2]。
sz[i]存的是从根到i节点形成的数的个数,而根据上面的,节点最多有500000*64个,所以sz[500000*64]。
root[i]存的是第i个元素从Tire树的哪一个节点开始往下走的(每一个元素的起点都是新的,也说是存Trie树的版本),元素共有500000个,所以root[500000]。
插入操作
传统二进制Trie树插入是这样的:
void insert(int &now,int i,ull x)
{
if(!now) now=++np;
if(i<0) return;
int d=(x>>i)&1;
insert(ch[now][d],i-1,x);
}
传统的树,确定了第i位d是0或1之后就直接往下插入。
可持续化树多出了:不断继承前一个版本的“d^1”这棵子树。因为元素x本身只用建“d”这棵子树,而“d^1”这棵子树的继承,是为了留给后面的元素如果需要就再次继承,往下建(这符合Trie树的初衷,节约空间),否则就需要额外占用空间。(也就是说,实际上ch数组没有用到那么大,但也可能卡点)
至于sz数组,记录了这个节点及其后代总共有多少个元素(节点处记录个数是基本操作),这使得每个节点都不同(因为每个节点的子树可能用到相同的子树,于是靠节点的元素个数区分),后边会用到。
void insert(int pre,int &now,int i,ull x)
{
now=++np; //新建一个版本(新的起点)
sz[now]=sz[pre];//复制前一个版本(继承)
if(i<0)
{
sz[now]++; //这个版本自己本身有一个元素
return;
}
int d=(x>>i)&1;
ch[now][d^1]=ch[pre][d^1]; //当前元素并用不上这棵子树,但是继承留给后边的用
insert(ch[pre][d],ch[now][d],i-1,x); //d子树新建 ,当前元素走的是这边
pushup(now); //更新这个节点上的元素个数,用于后面查询判断
}
区间查询
查询时基本思路与一般的Tire树相似,从最高位开始尽可能第i位取d^1这边,若取得到便接着d^1这棵子树向下继续遍历,若取不到就走d这棵子树。
对于元素第L~R号,如果R的d^1这棵子树的大小比L的d^1子树的大小要大,说明L~R至少存在一个第i位为d^1的元素,可以继续往下走,否则就没有这样的元素,i位取不到d^1,只能走d这棵子树。
之后的步骤就跟一般的Tire树一样了。
ull ques(int l,int r,int i,ull x) //A[l]~A[r],当前第i位,求解的元素为x
{
if(i<0) return 0; //如果已经分解完
int d=(x>>i)&1;
int t=sz[ch[r][d^1]]-sz[ch[l][d^1]]; //说明r的d^1这棵子树比较大,l~r之间至少有一个i位为d^1的元素
if(t>0) return ques(ch[l][d^1],ch[r][d^1],i-1,x)+((ull)1<<i);
else return ques(ch[l][d],ch[r][d],i-1,x);
}
小结