字符串算法的总结到了Trie字典树这里,个人感觉Trie字典树不难,无论是思想还是代码部分,都是很容易理解的,但是作为AC自动机的前置知识,我们还是要好好学习一下的。
简介
字典树,也称 Trie 或字母树,指的是某个字符串集合对应的形如下图所示的有根树。树的每条边上恰好对应一个字符,每个顶点代表从根到该结点的路径所对应的字符串(将所有经过的边上的字符按顺序连接起来)。有时我们也称 Trie 上的边为转移,顶点为状态。
字符串集合{“AAA” “AAG” “T” “TCA” “TG”}
顶点上还能存储额外的信息,例如,上图中从根到加粗圆圈经过的边上字母组成的字符串是实际字符串集合中的元素。这种结点也可以称为单词节点,在实际代码中可以用一个 bool 类型的数组去记录。实际上,任意一个线结点所代表的字符串,都是实际字符串集合中某些串的前缀。特别的,根节点表示空串。
我们可以看见,对于任意一个结点,它到它的子结点边上的字符都互不相同。Trie 很好地利用了串的公共前缀,节约了存储空间。
若将字符集看做是小写英文字母,则 Trie 也可以看做是一个 26 叉树,在插入询问新字符串时与树一样,找到对应的边往下走。
实现方法
-
初始化
一棵空的 Trie 仅包含一个根结点,该点的字符指针均指向空。(在用数组实现时,就不用管太多,我是 Trie+0 为起始地址,只要保证此时 Trie 数组为空即可) -
插入
当需要插入一个字符串 S 时,我们令一个指针 rt 初始指向根结点。然后,依次扫描 S 中的每个字符 c:
(1)若指针 rt 的 c 字符指针指向一个已经存在的结点 Q,则令 P=Q。
(2)若指针 rt 的 c 字符指针指向空,则新建一个节点 Q,令 P 的 c 字符指针指向 Q,然后令 P=Q。
(3)当 S 中的字符扫描完毕时,在当前结点指针 P 上标记它是一个字符串的末尾。
例如,现在有字符串集合{“cab”,“cos”,“car”,“cat”,“cate”,“rain”} 将集合里的字符串依次插入字典树,过程如下图
在这个例子中,我们利用 Trie 存字符串,大概节省了一半的空间。
这里以由26个小写字母组成的字符串为例,代码如下。
//ch[rt][c]表示结点 rt 的 c 字符指针指向的结点
void insert(char *s)//将字符串插入字典树
{
int rt=0;//起始指针指向根结点
for(int i=0;s[i];++i)
{
char c=s[i]-'a';
if(!ch[rt][c])//没有相关子节点,新建一个
{
ch[rt][c]=++tot;//结点个数+1
}
rt=ch[rt][c];//继续向下找
}
word[rt]=1;//表示一个字符串的结束
}
- 查询
当需要查询一个字符串 S 在 Trie 中是否存在时,我们令一个指针 rt 起初指向根结点,然后依次扫描 S 中的每个字符 c:
(1)若指针 rt 的 c 字符指针指向空,则说明 S 没有插入过 Trie,结束查询。
(2)若指针 rt 的 c 字符指针指向一个已经存在的结点 Q,则令 rt=Q。
(3)若字符串 S 的字符扫描完毕时,若当前结点 rt 被标记为一个字符串的末尾,则说明 S 在 Trie 中已经存在,否则说明 S 没有被插入过 Trie。
顺着上面的例子 ,查询字符串 ”cat“ 是否在Trie里,起初当前结点为根节点,即 rt=0,我们发现根节点有以字符 c 为指针的结点,rt=ch[rt][c] 。继续往下找,发现当前结点有以字符 a 为指针的结点,rt=ch[rt][a]。继续往下找,发现当前结点有以字符 t 为指针的结点,rt=ch[rt][t],并且此时字符串 “cat” 已经扫描完毕了,同时当前结点也被标记为字符串的末尾,所以 ”cat“ 在 Trie 中。
代码如下
bool find(char *s)//看字符串是否在Trie中
{
int rt=0;//起始指针指向根结点
for(int i=0;s[i];++i)
{
char c=s[i]-'a';
if(!ch[rt][c])//没有相关子节点,return false
return false;
rt=ch[rt][c];//继续向下找
}
if(!word[rt])//扫描完毕,不是字符串结尾,return false
return false;
return true;
}
样例分析
Phone List(POJ3630)
题目大意
给 n 个号码,判断是否有某个号码的前缀是另一个号码。
解题思路
这个甚至不需要判断函数,直接在每次插入新字符串时,在扫描字符串过程中,看是否有其它字符串的结尾标记,如果有就冲突了。然后再看是否开辟新的节点,如果扫描完毕也没有开辟新的节点,那说明这个字符串是已经插入 Trie 中的某个字符串的前缀,也产生冲突。
AC代码
#include <iostream>
#include <stdio.h>
#include <cstring>
using namespace std;
int T,n;
const int maxn=1e4+5;
const int maxm=1e5+5;
int ch[maxm][10];
bool word[maxm];
char str[maxn];
int tot;
bool insert(char *s)//插入新字符串
{
int len=strlen(s);
int rt=0;
bool flag=false;
for(int i=0;i<len;++i)
{
int c=s[i]-'0';
if(!ch[rt][c])
{
ch[rt][c]=++tot;
flag=true;
}
rt=ch[rt][c];
if(word[rt])//判断是否是其它串的结尾
return false;
}
word[rt]=true;
return flag;
}
void init()//初始化
{
tot=0;
memset(word,false,sizeof(word));
memset(ch,0,sizeof(ch));
}
int main()
{
scanf("%d",&T);
while(T--)
{
init();
scanf("%d",&n);
bool flag=false;
for(int i=1;i<=n;++i)
{
scanf("%s",str);
if(flag)
continue;
if(!insert(str))
{
flag=true;
continue;
}
}
if(!flag)
puts("YES");
else
puts("NO");
}
return 0;
}
,