一.概念

Trie树及其应用_子节点

前缀树这个名字很好地体现了trie的存储方式,trie树的每个节点(除跟节点外)都保存了单词中的一个字母。每一条从根节点到叶节点的路径上的字母都拼凑出了一个单词。并且每一条从根节点到非叶节点的路径上的字母都拼凑出了一个单词的前缀。

它有以下特点:

  • 根节点是空的。

  • 最多会有N*len个节点。每个节点又引申出|S|个子节点指针,相当于一棵多叉树(甚至可能每个点叉的个度比高度还多)。

  • 每个非叶节点都会被重复利用,可以在遍历时节省时间效率

  • 用空间换时间的数据结构。

    处理前缀问题时,朴素的做法是一个一个枚举,而trie树通过公共前缀公用节点,可以不逐一比较,就可以判断。


二.操作
  • 成员
struct Trie{
  int ch[33];//用来存储该节点的字母及节点编号
  bool flg……//一些与题目相关的附加信息
}
int tot;//节点编号
  • 插入
void insert(char *s,int len)
{
    int now=0;//当前节点编号
    for(int i=0;i<len;i++){
        int let=s[i]-'a'+1;
        if(!tree[now].ch[let])
            tree[now].ch[let]=++tot;
        now=tree[now].ch[let];
    }
}

​ 时间复杂度\(O(len)\)

  • 遍历
void insert(char *s,int len)
{
   int now=0;
   for(int i=0;i<len;i++){
        int let=s[i]-'a'+1;
        if(tree[now].ch[let])
            now=tree[now].ch[let];
   }
}

​ 时间复杂度\(O(len)\)

以上均为最裸的模版,其余一些附加信息的记录与遍历可以根据题目要求编写。

用O(n)把待处理的字符串挂到trie上,最后一起查询或者边挂边查


三.应用

(1).前缀匹配

一些套路:

  • 在每个单词的结尾字母节点记一个flg,代表该单词出现过。

1.于是他错误的点名开始了

简化题意

给出一张单词表,在单词表中查询当前单词是否存在以及是否第一次出现。

思路

该单词是否出现

  • 建图时在每个单词对应的叶节点打上一个标记(flg),代表该单词出现过。
  • 查询时如果树上没有当前字母对应的节点了,直接return false。如果查到最后一个字母了,但是当前节点没有flg标记,也要return false。

是否第一次出现

  • 查询时,如果存在该单词,那么在该单词的词尾再打上一个标记(fir),代表当前该单词已经被访问过了。

code

AC记录

总结

算是trie树的模版题,主要考察附加信息的记录。

2.phone list

简化题意

给出一张单词表,查询单词表中是否存在一个单词为另一个的前缀。

思路

在插入该单词时,该单词可能是其它单词的前缀,也可能其他单词是该单词的前缀。

  • 是否是其他单词的前缀

    在向trie树中插入一个新的单词时,如果新建了一个字母节点,那么说明没有其他单词与该单词的当前字母相同,所以该单词就不是其他单词的前缀。

  • 其他单词是否是当前单词的前缀

    建图时在每个单词对应的叶节点打上一个标记(flg),代表该单词出现过。

    在插入当前单词时,如果当前字母节点的flg为true,那么说明有单词为该单词的前缀。

code

AC记录

总结:单词表之间的单词匹配,可以边挂边查。

3.L语言

简化题意

给出一张单词表,查询一段文章可以由单词表中的单词拼凑出的最长前缀的位置。

思路

这道题与前几道题不同的是,文章的前缀是由单词表的几个单词拼凑出来的,而不是单词表中的一个单词。那么在匹配单词表中的单词时就需要每次从根开始匹配,每当匹配到了单词表中一个单词的末尾,那么就在文章的当前位置打一个标记。两层循环,第一层循环枚举当前匹配的文章位置,第二层循环枚举从当前位置开始匹配,能与单词表中的单词匹配的最远位置。

code

AC记录

总结

如果查询的单词是由单词表中的一些单词拼凑出来的话,那应该每次从根匹配。


(2).前缀计数

一些套路:

  • 结尾字母节点:一个cnt,代表该单词出现次数。
  • 结尾字母节点:一个sev数组,代表该单词在那些单词中出现过。
  • end,代表有多少个单词以该字母节点结尾。
  • sum,代表有多少个单词经过该字母节点。

1.魔族密码

简化题意

给出一张单词表,查询最多有多少个单词有相同的前缀(包括它本身)。

思路

  • 建图时在每个单词对应的叶节点上记录一个cnt,代表该单词出现过的次数。
  • 查询时加上该单词每个字母对应的节点上的cnt,代表单词表中是该单词前缀的个数。最后也要加上自己。

code

AC记录

总结:模版题。

2.阅读理解

简化题意

给出n张单词表,查询一个单词在多少张单词表中出现过。

思路

  • 建图时在每个单词对应的叶节点上开一个sev[n],倘若该单词在第i个文章中出现过,就将sev[i]标记为true。
  • 一个标记flag代表单词表中是否有当前查询单词。查询时,如果没有当前字母对应的节点了,直接将flag标记为false。如果存在该单词,那么遍历叶节点的sev数组,如果sev[i]标记为true,就输出i。

code

90分记录

最大数据范围会MLE,只用trie树应该是过不了的。并不知道第一篇题解怎么过的/yun

总结:建树技巧。

3.Secret Message G

简化题意

给出一张单词表,查询一段信息是多少个单词的前缀以及多少个单词是它的前缀。

思路

  • 当前单词是多少个单词的前缀

    如果当前单词是其他单词的前缀,那么其他单词一定经过了该单词结尾的字母节点。

    建树时,对于每个字母节点记录一个sum值,代表有多少个单词经过了该字母节点。

  • 有多少个单词是该单词的前缀

    如果有一个单词是当前单词的前缀,那么这个单词一定是以当前单词的任一字母节点为结尾的。

    建树时,对于每个字母节点记录一个end值,代表有多少个单词是以该字母节点结尾的。

  • 统计答案

    在查询一段信息时,加上该信息每一字母节点的end值,也就统计了有多少个单词是当前单词的前缀。加上该单词结尾字母节点的sum值,也就统计了当前单词是多少个单词的前缀。

code

AC记录

总结

字典树计数题。


(3).字典序相关

一些套路:

与拓扑排序结合是个好思路~

1.First! G

简化题意

给出一张单词表,有多少个单词可以通过改变标准字母表顺序称为字母表中字典序最小的单词。

思路

一个单词可以成为字典序最小的单词的情况有很多种。

考虑在什么情况下一个单词永远不可能成为字典序最小的单词。排除掉这些单词,那么其他单词就是可能成为字典序最小的。

有以下两种情况:

  • 有一个单词是这个单词的前缀

    无论什么情况这个单词的字典序永远不可能比它的前缀小。

    转化为字典树查前缀的问题。

  • 这个单词中存在一个字母与其它一个字母之间的大小关系形成了一个环。

    (不考虑包含前缀的情况)

    一个单词如果是字典序最小的,那么它与其它单词相同前缀后的每一个字母都是字典序最小的。转化成为确定字母之间的大小关系。

    eg.omm,moo,mom
    omm,moo与mom都有相同前缀( ),相同前缀后的第一个字母m的字典序最小,所以moo与mom可能成为最小的。
    moo与mom有相同前缀(mo),相同前缀后的第一个字母m的字典序最小,所以mom是最小的。
    

    反应到字典树上,

    对于如果一个节点的父节点除了它自己外还有很多儿子节点,那么它在这些节点中的字典序应该是最小的。

    比如说

Trie树及其应用_trie树_02

这个根节点的两个子节点中,(可以看做一个分支),m的字典序最小,所以以m开头的单词有可能成为字典序最小的。

如果想要使当前查询的单词的字典序最小,那么它的每个字母在分支中的字典序都最小。从上向下查询时,每遇到一个分支,我们钦定当前单词的字母节点的字典序比其它的字母节点都小,如果构成的这些关系没有自相矛盾,就说明是可以构造出一种字母顺序表使当前查询单词在单词表中的字典序最小。

解决这样的问题,可以想到拓扑排序。如果钦定一个字母比另一个字母的字典序最小,那么就从这个字母节点向另一个字母节点连一条边。如果这些大小关系自相矛盾了,那么一定是构成了环,进行topo排序找环即可。

还是这张图,当前查询的单词是moo。

Trie树及其应用_数据结构_03

在第一个分支中,m向o连边,在第二个分支中o向m连边,m与o之间形成环,所以moo永远不可能成为最小的。

也就是说想要使moo的字典序最小,那么既要满足m的字典序比o小,又要满足o的字典序比m小,自相矛盾啊。如果只满足其中一个如果m的字典序比o小,那么mom比moo小。如果o的字典序比m小,那么omm比moo小。moo永远不会最小。

code

AC记录

总结

字典树存储前缀加拓扑排序判优先级并找环。


(4).与搜索结合:

一些套路:

确定方案,以及不能直接查询前缀的,可以考虑下dfs~

8.背单词

简化题意

重排一张单词表,尽量使每个单词的后缀排在前面。

思路

CwQwC

Ainu's Blog

两篇题解都讲的很好呀~可以用第二篇题解的图结合第一篇题解的文字看。

  • 颠倒单词后,建出trie树。

    可以用string的reverse函数。

    trie树是存储前缀的,将后缀变成前缀存储

  • 保留根节点与叶子节点,去除其它节点。

    重要的是单词个数,只留一个叶子节点代表这个单词就可以了。

    连边建出实际的树

  • 将直接祖先相同的子树按节点数从小到大排序。

    第i个根节点的贡献是前面所有子树大小的size和,最小化贡献,就要使排在前面的子树和尽量小。

    dfs一遍求出子树大小,在排序。

  • 用dfs序求出答案。

    证明dfs序是最优的,但在有些情况下不是唯一的最优解。

    假如不是dfs序的话,那么就是说在遍历完一个节点后,不是遍历它的子节点,而是遍历另一棵树的根节点。那么两棵子树中儿子节点到根节点的距离都会增加。dfs序是最优的。

    void get_ans(int now)
    {
    	int dfn=cnt++;
    	for(int i=0;i<e[now].size();i++){
    		ans+=cnt-dfn;
    		get_ans(e[now][i]);
    	}
    }
    

code

AC记录

总结

trie树存储前缀,子树大小排序加dfn序的连续性最小化关联节点的距离。

9.电子单词

简化题意

给出一张单词表,查询单词表中与该单词相同,比该单词多或少一个字母,与该单词有一个字母不同的单词的个数。

思路

在单词的任一位置增加,删减或替换一个字母。可以用dfs来解决。

void query(char *s,int len,int rt,int now,bool flag)
  //字典树中当前节点,查询单词的当前位置,是否修改三个维度
{
    if((now==len)&&(tree[rt].flg)&&(!flag)){
        sev=true;
        return ;
    }
    if((now==len)&&(tree[rt].flg)&&(flag)){
        if(!vis[rt]){
            vis[rt]=true;
            ++cnt;
        }
        return ;
    }
    int let=s[now]-'a'+1;
    if(!flag){
        if(now<len) query(word,len,rt,now+1,true);
      	//删除操作:字典树中节点位置不变,搜索单词的下一位置
        for(int i=1;i<=26;i++){
            if(tree[rt].ch[i]){
                query(word,len,tree[rt].ch[i],now,true);
              	//添加操作:字典树中节点位置到子节点位置,搜索单词当前位置。
                if(i!=let) 		query(word,len,tree[rt].ch[i],now+1,true);
              //替换操作:字典树中节点位置到子节点位置,搜索单词下一位置
            }
        }
    }
    if(now>=len) return;
    if(tree[rt].ch[let]) query(word,len,tree[rt].ch[let],now+1,flag);
}

code

AC记录

总结

dfs+trie树

10. Type Printer

思路
只有最后一个单词没有删除操作。

trie树上标记长度最长的单词,最后进行搜索,不回溯。

确定一种 DFS 序,使得在遍历所有节点的前提下走过的边数尽可能少。

code

AC记录

总结:Trie树+dfs


(5).AC自动机

在trie树的结构上应用kmp失配的思想。

(6).不在字符串上的应用

学长的博客里有

to be continued……


四.注意事项
  • 字典树的节点个数一半开maxn*maxlen,适当开大一点。

  • 看好给出的单词是01还是字母。

    01建树的时候是s[i]-'0'。

    字母建树的时候s[i]-'a'+1。

    (建错了会RE)

  • 给出的单词长度不确定时,可以用string,确定时可以用char[]。

    string的size,length,reverse一些函数,灵活运用。

  • 多测清空

  • memset记得加cstring函数


五.参考

学长的博客

一个动态更新的洛谷综合题单

Trie 树 基础练习

有些题还没做完