初学可持久化Trie树(字典树)

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);
}

 

小结

可持久化Tire树,关键在于每个节点对于前面的子树可继承,并且利用节点的元素个数来加以区分,(虽然不懂,但反正不管怎么说)达到了区间查询的作用。原理(虽然不懂)还是很巧妙的。

    原文作者:Trie树
    原文地址: https://blog.csdn.net/WWWengine/article/details/81187773
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞