详解Trie

一.Trie的概念

Trie又称字典树,前缀树(事实上前缀树这个名字就很好的解释了Trie的储存方式)

来一张图理解一下Trie的储存方式:(图片来自百度百科)

由这张图我们也可以知道Trie的特点:

  • Trie的根节点是空的
  • 除根节点外,每个节点储存一个单词/字母
  • 也就是说,从根节点到每个单词节点的路径上的所有字母连接而成的字符串就是该节点对应的字符串
  • 每个非叶子结点一般都会被多次使用,以节省遍历时的时间效率
  • 另外,每个节点下面的数字是他们的编号,这个具体在下面再展开

所以读者们不难看出,

Trie是典型的用空间来换时间的做法

二.Trie的操作

Trie的常用操作:插入,查询,删除

(插入,查询为常用操作,删除操作实在少见就不展开讲了)

在这之前我们需要填一下上面的坑:

那些非根节点下面的数字是什么意思?

事实上,在代码中,我们一般把根节点编号为0,然后把其余节点从1开始编号,然后存在一个数组中(数组的用处是储存每个节点的所有子节点,以便于直接使用下标来存取)

再具体一点,在我的代码中,用ch[i][j]来保存i的那个编号为j的子节点,当然,如果ch[i][j]==0,也就代表这个节点不存在,即i没有这个编号为j的子节点

好了,坑填完了

我们来明确一下另外一个数组的概念:val数组

对于需要用到Trie的题目,一般是不会只让你单纯构建一棵Trie的,一般都会有各种奇奇怪怪的要求,就像我们的例题(例题在下面),怎么处理这一些要求?我们可以把他们看作是一些“附加信息”,然后储存起来,val数组就是用来储存附加信息的

那么现在我们就来讲一下插入操作(具体解释都在代码中):

(我的代码一般是没有指针这种不是很友好的东西的,不过这里因为是定义在结构体里面的,比较麻烦,所以干脆写个指针,意思很好懂的,代码中有注解)

#define ll int
#define N 500010 
struct Trie{
    ll ch[N][26],sz,val[N];
    //val为附加信息
    //这里的ch数组,第二维的大小为26是因为字符串只由小写字母构成,第二维的大小一般是由字符串的组成决定
    //sz即为节点编号 
    Trie(){
        sz=1;//一开始的时候只有根节点这一个节点 
        memset(ch[0],0,sizeof(ch[0]));
        memset(val,0,sizeof(val));
    }//这里是初始化 
    ll idx(char c){return c-'a';}//返回字符c的编号 
    void insert(char *s,ll v){
    //插入操作 ,这里是整份代码中唯一用到指针的地方,因为函数是放在结构体里面 ,所以最好用个指针,其实等价于char s[] 
    //s代表要插入的字符串,v为附加信息 
        ll u=0,len=strlen(s+1);
        for(ll i=1;i<=len;i++){
            ll c=idx(s[i]);
            if(!ch[u][c]){//如果节点不存在就插入,不然就继续往下遍历 
                memset(ch[sz],0,sizeof(ch[sz]));
                val[sz]=0;//中间的节点是没有附加信息的 
                ch[u][c]=sz++;//新建节点 
            }
            u=ch[u][c];//往下遍历 
        }
        val[u]=v;//插入附加信息,注意,我们一般只在叶子节点插入附加信息,中间的节点一般是没有附加信息的,因为一个非叶子结点,在Trie中一般都会被不同的单词使用到(定义) 
    }
}tree;

接下来是查找操作,事实上查找操作基本上是和插入操作是差不多的,如果你真的理解了上面的insert操作,可以尝试着在不看我下面代码的情况下去自己写一下,同样,对于这个操作的解释放在代码中

    ll search(char *s){//这里的指针意义同上 
    //对于这一个查找操作,我们就以最简单的操作来展开:
    //查找这个字符串是否在Trie中出现过,如果出现过,返回它的附加信息 
        ll u=0,len=strlen(s+1);
        for(ll i=1;i<=len;i++){
            ll c=idx(s[i]);
            if(!ch[u][c])return -1;
            //没有这个节点,返回-1,即代表这个字符串没有在Trie中出现过
            //这个返回值得看题目所需要的附加信息来决定,因为这里是不可以与附加信息冲突的,在这里我们假定附加信息为0或正数 
            u=ch[u][c];//继续向下遍历 
        }
        return val[u];//这个节点存在,返回它的附加信息 
    }

三.Trie的题目

1.模板题:luogu P2580 于是他错误的点名开始了

题面篇幅有点长,就不贴了,大概讲一下题意:

给你n个字符串以及m个询问,每次询问需要回答:如果该询问的字符串没有在n个字符串里面出现过,输出“WRONG”,如果该询问的字符串出现过而且是第一次询问,输出“OK”,如果该询问的字符串出现过但不是第一次询问,输出“REPEAT”

仔细看一下题目,事实上就是字典树的模板题,对于insert操作和search操作进行一下微调就可以了

主要的调整是,附加信息不在insert操作里面插入,而是在search操作中插入

(在本题中,附加信息为该字符串是不是第一次被询问)

剩下的跟上面的代码内容都差不多了,接下来上代码

#include <cstdio>
#include <cstring>
#define ll int
#define inf 1<<30
#define mt(x,y) memset(x,y,sizeof(x))
#define il inline 
il ll max(ll x,ll y){return x>y?x:y;}
il ll min(ll x,ll y){return x<y?x:y;}
il ll abs(ll x){return x>0?x:-x;}
il void swap(ll &x,ll &y){ll t=x;x=y;y=t;}
il void read(ll &x){//快读 
    x=0;ll f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-f;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    x*=f;
}
using namespace std;
#define N 500010
struct Trie{
    ll val[N],ch[N][26],sz;
    Trie(){
        sz=1;
        memset(ch[0],0,sizeof(ch[0]));
        memset(val,0,sizeof(val));
    }//初始化 
    ll idx(char c){return c-'a';}
    void insert(char *s){
        ll u=0,len=strlen(s+1);
        for(ll i=1;i<=len;i++){
            ll c=idx(s[i]);
            if(!ch[u][c]){
                memset(ch[sz],0,sizeof(ch[sz]));
//                val[sz]=0;  这里并不需要这么标记中间信息,因为在初始化那里已经将所有的附加信息初始化为0了 
                ch[u][c]=sz++;
            }
            u=ch[u][c];
        }
    }
    ll search(char *s){
        ll u=0,len=strlen(s+1);
        for(ll i=1;i<=len;i++){
            ll c=idx(s[i]);
            if(!ch[u][c])return 0;
            //如果没有这个节点,也就意味着询问的这个字符串不存在,输出WRONG 
            u=ch[u][c];
        }
        if(!val[u]){
            val[u]=1;return 1;//这个字符串已经询问过了,标识一下,然后输出OK 
        }
        return 2;//这个字符串正确,但不是第一次出现,输出REPEAT 
    }
}tree;
ll n,m;
int main(){
    read(n);char s[100];
    for(ll i=1;i<=n;i++){
        scanf("%s",s+1);
        tree.insert(s);
    }
    read(m);
    for(ll i=1;i<=m;i++){
        scanf("%s",s+1);
        ll pd=tree.search(s);
        if(pd==0)printf("WRONG
");
        else if(pd==1)printf("OK
");
        else if(pd==2)printf("REPEAT
");
    }
    return 0;
}

 2.UVA11732 "strcmp()" Anyone?(这里给的是洛谷的链接,国内进uva比较慢)

update:2018.5.22

给一点提示:

1.注意要用左儿子右兄弟法储存

2.题目中的“比较次数”说的有点迷,这里阐述一下

相同长度为L的两个单词比较代价为2L-1,除了最后一次外要进行s结束的判断;

单词的比较长度应该为strlen(s)+1,结束符也是比较的一环;

如果两个单词相同,则多计算一次结束判断,比较次数+1。

这里就不给代码了(个人感觉这道题独立思考会更好一些,挺经典的一道题了,更主要是因为我这道题的代码打的比较丑

至于翻译网上找找就有的

3.[AHOI2005]病毒检测

update:2018.6.2

做法:Trie+dfs/bfs,也可以尝试用dp写

之后会搞一篇这道题的题解,坑先留着

四.总结

上面的题目打过一遍之后基本就可以把Trie掌握了,注:会不定时更新题目

Trie其实不算是多难的数据结构,算是字符串里面一个比较基本的东西吧,然后就是推荐如果学习完Trie可以把hash和kmp也顺便给学了,当然,如果你Trie和kmp都吃透了就可以尝试挑战一下AC自动机了!


转载请注明出处:http://www.cnblogs.com/henry-1202/p/9029822.html

原文地址:https://www.cnblogs.com/henry-1202/p/9029822.html