Trie字典树学习笔记

文章介绍了Trie字典树的基本概念,强调其在存储和查询字符串时的效率优势,并详细说明了如何构建和查询Trie树。通过实例展示了在处理字符串集合时,如何使用邻接矩阵来表示和更新字典树,并提供了两个编程题目的解题思路。
摘要由CSDN通过智能技术生成

注:

  • 前言是废话
  • 线段树不分左右,说左右是为了好理解
  • 实操指实际操作
  • qwq 指根

理论

前言

Trie字典树 是一个树(

定义

Trie字典树 指的是:某个字符串集合构造的有根树。

好处

由于 Trie字典树 较好的利用了字符串的公共前缀,因此有效的节约存储空间。

查询效率比哈希树高。

用途

用于统计、排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。

性质

  • 根结点不包含字符,除根结点外每一个结点都只包含一个字符;
  • 从根结点到某一结点,路径上经过的字符连接起来,为该结点对应的字符串;
  • 每个结点的所有子结点包含的字符都不相同。

正片

input:

abc
ghi
abcdef
abf

构建

先处理 abc

第一个进来 a,发现 qwq 左侧和右侧都没有 a,于是将 a 弄到左边。

Snipaste_2023-01-30_12-39-17.png

依次类推,将 b 和 c 也弄进来。

Snipaste_2023-01-30_12-41-19.png

然后我们发现 c 是这个字符串结尾,于是标记一下。

Snipaste_2023-01-30_12-43-00.png

然后处理 ghi,因为 g 没出现过,所以这个字符串丢去右边。

Snipaste_2023-01-30_12-47-00.png

接着是 abcdef,前面 abc 刚刚好都有,所以只用从 c 往下建 def

Snipaste_2023-01-30_12-52-27.png

最后是 abf,ab 已经有了,直接在 b 的旁边构建一个 f 就刑。

Snipaste_2023-01-30_12-54-26.png

题外话:没错,仅有小写字母是二十六叉树。

所以说,我们的数组大概是这样子的:

Snipaste_2023-01-30_13-00-46.png

但是可以发现,如果再来一个 abc,它会和原来的重合,所以我们再用一个数组来标记以某个节点为结束的单词有几个。

Snipaste_2023-01-30_12-57-59.png

另:第 i 个处理的字母我们将其标记为 i 号节点。

这个数组怎么看呢,很简单。

0 0 0 指根, a 0 , i a_{0,i} a0,i 就是指根的子节点。

也就是说, a i , j a_{i,j} ai,j 就是指 i号节点 的子节点。

题外话:没错,这棵树不区分左右子节点。

所以说,如果我们用邻接矩阵储存仅有小写字母的字典树,数组大概要开 max ⁡ { s i . s i z e } × 26 \max\{s_i.size\} \times 26 max{si.size}×26

构建实操

例题:东方博宜2381

首先我们定义一个 p p p 为当前搜索的根,从 0 0 0 也就是根开始找起。

void insert(char s[]){
    int p = 0;
    
}

然后循环 s s s 数组。

void insert(char s[]){
    int p = 0;
    //for(int i = 0; s[i] != '\0'; i++)
    for(int i = 0; s[i]; i++){
        
    }
}

如果查找当前根找不到,就添加这个点。

void insert(char s[]){
    int p = 0;
    //for(int i = 0; s[i] != '\0'; i++)
    for(int i = 0; s[i]; i++){
        int x = s[i]-'a';
        if(!t[p][x]) t[p][x] = ++ind;
    }
}

然后更新 p p p,而且无论是否新建点(无论 s i s_i si 是否是当前节点 p p p 的子节点), p p p 都应该更新到 t p , x t_{p,x} tp,x

还有因为最后结束循环时还会更新一次,所以说我们要标记结束点时刚刚好就可以用 p p p

void insert(char s[]){
    int p = 0;
    //for(int i = 0; s[i] != '\0'; i++)
    for(int i = 0; s[i]; i++){
        int x = s[i]-'a';
        if(!t[p][x]) t[p][x] = ++ind;
        p = t[p][x];
    }
    cnt[p]++;
}

查询实操

查询大致一样,只是如果当前根没有,直接 return

这道题如果查找了找不到的直接返回 0 0 0(没有投票,票数为 0 0 0),所以说找不到就 return 0

刚刚好,如果查询完改数组最后一个不是结束点,直接返回 c n t p cnt_p cntp,刚刚好就是 0 0 0

void insert(char s[]){
    int p = 0;
    for(int i = 0; s[i]; i++){
        int x = s[i]-'a';
        if(!t[p][x]) return 0;
        p = t[p][x];
    }
    return cnt[p];
}

练习讲解

例题一:洛谷P8306

这个题和和东方博宜的板子很像,但是难一些。

首先我们的数组要扩到 65 65 65,然后要写个函数处理字符串里的字母。

因为只有大小写字母和数字,所以 if 判断然后返回就行。

但是直接减去 Aa0 并不可行,所以我们应该在储存时加一个数,使大小写字母和数字的存储错开。

int get(char x){
    if(x >= 'A' && x <= 'Z')
        return x-'A'; //大写字母
    else if(x >= 'a' && x <= 'z')
        return x-'a'+26; //小写字母
    else return x-'0'+52; //数字
} 

另外,查询时,因为是问的前缀,所以我们 for 储存时一边循环一边 c n t p cnt_p cntp++。

void insert(char s[]){
    int p = 0;
    for(int i = 0; s[i]; i++){
        int x = get(s[i]);
        if(!t[p][x]) t[p][x] = ++ind;
        p = t[p][x];
        cnt[p]++;
    }
}

值得一提的题外话:警钟长鸣

例题二:洛谷P2580

值得一提的是,这比洛谷的板子要简单,只是在查询的时候需要更改一下。

  • 每次查询,如果在循环查找名称时,没有找到,直接输出 WRONG 并且 return。
  • 每次查询结束,先判断是否存在这个名字,如果并没有( c n t p = 0 cnt_p = 0 cntp=0),就输出 WRONG。(这就是 hack 数据)
  • 如果存在该名字,将 c n t cnt cnt 数组这个地方修改为 − 1 -1 1,并输出 OK
  • 如果 c n t cnt cnt 数组这个地方为 − 1 -1 1,输出 REPEAT
void query(string s){
    int p = 0;
    for(int i = 0; s[i]; i++){
        int x = s[i]-'a';
        if(!t[p][x]){
            cout << "WRONG" << endl;
            return ;
        }
        p = t[p][x];
    }
    if(!cnt[p])
        cout << "WRONG" << endl;
    else if(cnt[p] == -1)
        cout << "REPEAT" << endl;
    else cout << "OK" << endl,cnt[p] = -1;
}

是吧,和上面的第一题很像。


O v e r Over Over

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值