学习笔记:Trie树详讲

引入

假如我们给出大量的字符串,查询其中某两串的公共前缀,显然不断枚举比较是否相同的办法对于多次询问太慢了,这就要用到Trie树。
Trie树即字典树,是一种能快速检索字符串的多叉树,但是Trie树的内存消耗很大,是用空间换时间的思想。那么它是如何实现的呢?

原理

假如给出abc,abd,abcd,bcd,efg五个字符串,先来看看构造出来的树。
在这里插入图片描述
我们发现根节点是没有字符;第一字符不同在不同的子树上,而abc,abd第一字符相同就在同一子树上;而abc和abd由于最后一个字符不同,所以又分支了出去。原理十分简单。
如果我们已经将一个字符串输完了,只需要将最后节点标记一下,下次遍历时只要到了该节点,那么搜到的所有点组合起来就是该字符串。
我们来详细地了解如何建立和查询一棵Trie树。

建立

我们开一个二维数组存储,trie[ i ][ j ],表示i节点的第j个子节点的编号为a,其中如果是小写字母那么第二维就可以是26,0~9的数字那么就是10。
我们再引入一个指针p来指向i这个节点,如果trie[p][i]==0,说明该字符还没建立,那就建立并把p移到该新节点上,如果trie[p][i]!=0那么就说明这里已经建了,就直接跳到它的子节点。

void Build()
{
    
    
	int p=0;
	for(int i=0;i<strlen(str);i++)
	{
    
    
		if(trie[p][str[i]-'a']==0) 
		{
    
    
			trie[p][str[i]-'a']=++tot;
		}
		p=trie[p][str[i]-'a'];
	}
}

查询

例如查询一个字符串,看它是否在字典树中。
首先第一个节点,如果得到的编号没有定义如等于0,那么说明不存在,如果第一个节点编号存在,那么就跳到它下面的一个编号,继续判定。如果全部枚举完了,并且最后一个节点有标记了是一个字符串,那么就找到了。

int Search()
{
    
    
	int p=0;
	for(int i=0;i<strlen(str);i++)
	{
    
    
		if(trie[p][str[i]-'a']==0) return 0;
		p=trie[p][str[i]-'a'];
	}
	return 1;
}

板子传送门HDU 1251

#include<bits/stdc++.h>
using namespace std;
#define N 100005

int trie[N][26];
int ed[N];
int tot;
char str[15];

void Build()
{
    
    
	int p=0;
	for(int i=0;i<strlen(str);i++)
	{
    
    
		if(trie[p][str[i]-'a']==0) 
		{
    
    
			trie[p][str[i]-'a']=++tot;
		}
		p=trie[p][str[i]-'a'];
		ed[p]++;  //记录到了目前节点有重复了,如abc,abcd,则a点ed为2,b点ed为2.c点ed为2,d点ed为1 
	}
}

int Search()
{
    
    
	int p=0;
	for(int i=0;i<strlen(str);i++)
	{
    
    
		if(trie[p][str[i]-'a']==0) return 0;
		p=trie[p][str[i]-'a'];
	}
	return ed[p];
}

int main()
{
    
    
	freopen("trie.in","r",stdin);
	freopen("trie.out","w",stdout);
	while(gets(str) && str[0]!='\0') Build();
	while(scanf("%s",&str)!=EOF)
		cout<<Search()<<endl;
	return 0;
}

ed在for循环内统计每个节点重复次数,如果放到外面,就是统计每个字符串重复的次数。
PS:特别要注意读入的问题。

深入

最长异或路径luoguP4551
解决这道题,我们先看看这个问题:

在给定的N个整数A1,A2,……,AN中选出两个进行xor运算,得到的结果最大是多少?
xor表示二进制的异或(^)运算符号。
对于100%的数据: N≤105;0≤Ai<231

既然是异或位运算,那么就联想到二进制,那么我们就把每个整数写成二进制数,由于A最大为231那我们就全部作成长度为30的01串,不足的加前导0。
实现转换为二进制:

for(int i=30;i>=0;i--)
	{
    
    
		int t=x>>i&1;
	}

由于异或只有0^1=1,那么肯定A!和A2必须要从最高位开始尽量的每一位不相同。就有点像找公共前缀。那我们考虑使用字典树解决。
首先建树差不多从高到低建就可以了,重点是查询操作。
我们每处理建了一个01串,我们就去询问用这个01串来在当前建的字典树中找,不断更新答案,(那会不会本来就是用这个01串找,但这个是第一建树,就找不了呢?)不会,因为尽管它没找到,但它存到了字典树中,只要和它匹配的另一01串去更新就行了。
那该如何找?
从高到低取出每一位,如果是1,那就找1^1=0,如果是0,那就找0^1=1,如果不存在对应的数字,那就只能用相同的了。

#include<bits/stdc++.h>
using namespace std;
#define N 100005

int n,a,ans=0,tot;
int trie[N*31][2];//只有0和1两种

void Build(int x)
{
    
    
	int p=0;
	for(int i=30;i>=0;i--)
	{
    
    
		int t=x>>i&1;
		if(trie[p][t]==0)
			trie[p][t]=++tot;
		p=trie[p][t];
	}
}

int Search(int x)
{
    
    
	int p=0,sum=0;
	for(int i=30;i>=0;i--)
	{
    
    
		int t=x>>i&1;
		if(trie[p][t^1]!=0)
		{
    
    
			p=trie[p][t^1];
			sum+=1<<i;
		}
		else 
		{
    
    
			p=trie[p][t];
		}
	}
	return sum;
}

int main()
{
    
    
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
    
    
		scanf("%d",&a);
		Build(a);
		ans=max(ans,Search(a));
	}
	cout<<ans;
	return 0;
}

最长异或路径luoguP4551
有了上面的铺垫,我们来看这道题。

首先我们要知道
因为a^0=a,a^a=0,所以(a^b)^(b^c)=a^c
那么要求2个叶子节点的最大异或和,我们可以求根节点到每个叶子节点的异或和,要求2个叶子节点的异或和就等于两个到根的异或和的异或。
求根到每个叶子的很简单,直接dfs遍历即可,然后就把题转换为了根到哪2个叶子的异或和最大。
咦?这不就是我们上面做的题吗 ,直接ctrl+c/v。
我这里使用邻接表存储。

#include<bits/stdc++.h> 
using namespace std; 
 
const int N=100010; 
int n,m,ans,tot=1; 
int f[N],first[N]; 
int trie[N*31][2]; 
 
struct edge         //邻接表数据结构 
{
    
     
  int to,next,v; 
}e[N*2];            //共有N-1条边,但是双向,所以数量翻倍  
 
void build(int x) //构建Trie树  
{
    
     
  int p=0; 
  for(int k=30;k>=0;k--) 
  {
    
     
    int t=x>>k&1; 
    if(trie[p][t]==0) trie[p][t]=++tot; 
    p=trie[p][t]; 
  } 
} 
 
int query(int x) //Trie树查询  
{
    
     
  int p=0; 
  int tmp=0; 
  for(int k=30;k>=0;k--) 
  {
    
     
    int t=x>>k&1; 
    if(trie[p][t^1]) 
    {
    
     
      p=trie[p][t^1];  
      tmp=tmp+(1<<k);  //叠加新的异或结果:1;也可写成:tmp=tmp|(1<<k); 
    }     
    else 
    {
    
     
      p=trie[p][t]; 
    } 
  } 
  return tmp; 
} 
void insert(int u,int v,int w) //建立邻接表  
{
    
     
  e[++m].to=v; 
  e[m].next=first[u]; 
  first[u]=m; 
  e[m].v=w; 
} 
 
void dfs(int x,int fa) //算出x所有子节点的f[i],x的父亲是fa  
{
    
     
  for(int i=first[x];i;i=e[i].next) 
    if(e[i].to!=fa)                //避免往回搜,此处也可以用做标记的方式  
    {
    
     
      f[e[i].to]=f[x]^e[i].v; 
      dfs(e[i].to,x); 
    } 
} 
 
int main() 
{
    
     
    //freopen("xorpath.in","r",stdin); 
    //freopen("xorpath.out","w",stdout);  
 
  	scanf("%d",&n); 
  	for(int i=1;i<n;i++)              //读入每条边并插入到邻接表 
 	{
    
     
   		int u,v,w; 
    	scanf("%d%d%d",&u,&v,&w);     
    	insert(u,v,w); 
   	 insert(v,u,w); 
  	} 
   
  	dfs(1,0);                         //算出根到每个节点的XOR路径长度值f[i]  
   
    for(int i=1;i<=n;i++)  
  	{
    
     
        build(f[i]);                  //Trie树中插入f[i]  
        ans=max(ans,query(f[i]));     //更新最大值  
    }   
  	printf("%d",ans); 
 	return 0; 
} 

ok一道紫题就这么轻松的A了

おすすめ

転載: blog.csdn.net/pigonered/article/details/120831736