算法学习笔记(15): Trie(字典树)

算法,学习,笔记,trie,字典 · 浏览次数 : 58

小编点评

**Trie树的插入和查询** **插入** 1. 将字符串 *s* 的每个字符离散化,并记录在节点 *p* 的相应位置。 2. 逐个将字符在 *s* 中找到对应的节点。 3. 在遍历完所有字符后,更新 *cnt* 属性以记录字符串的长度。 **查询** 1. 从根节点 0 开始。 2. 逐个通过字符在 *s* 中找到对应的节点。 3. 如果遇到无法找到匹配的节点,则返回 0。 4. 记录所有匹配的节点,并返回其数量。 **时间复杂度** * 时间复杂度与字符串长度成正比,即 O(L),其中 L 是字符串的长度。 * 查询操作的时间复杂度为 O(log(N)),其中 N 是字符串的字符数。 **空间复杂度** * 每个节点最多可以存储 65 个字符,因此每个节点最多可以存储 65 * 8 个字符。 * 因此,Trie 树的总空间复杂度为 O(N),其中 N 是字符串的字符数。

正文

Trie树

Trie(字典树)是一种用于实现字符串检索的多叉树。

Trie的每一个节点都可以通过 c 转移到下一层的一个节点。

我们可以看作可以通过某个字符转移到下一个字符串状态,直到转移到最终态为止。这是后话……

我们以插入了字符串 abaab 三个字符串的Trie树为例:

其实一看图就非常清晰了

在上图中,如果我们需要继续插入一个字符串 abc,那么就只需要新建一个节点即可

思路清晰,那么代码如何实现?

  • 首先是插入部分:
struct Node {
    int kids[65];
    int cnt;
} nodes[N];

#define kids(p, j) nodes[p].kids[j]
#define cnt(p) nodes[p].cnt

void insert(char * s, int len) {
    int p = 0;
    for (int i = 0; i < len; ++i) {
        int j = discrete(s[i]);
        if (!kids(p, j)) kids(p, j) = ++usage; // 新建节点
        p = kids(p, j);
    }
    ++cnt(p);
}

discrete指的是离散化,例如这里是将 a-z0-25 表示

最终的 cnt 表示有几个字符串在当前节点结束。

  • 然后是查询部分

我们还是利用类似的思路,一个一个向下走。

例如我们要查询字符串 aba,那么我们从根节点 0 开始,通过 a 走到 1 节点,通过 b 走到 4 节点,发现没有 a 的子节点,表明没有这个字串,结束寻找。

// 这里是查询这个字符串出现了多少次,为0就是没有出现
int count(char * s, int len) {
    int p = 0;
    for (int i = 0; i < len; ++i) {
        int j = discrete(s[i]);
        if (!kids(p, j)) return 0;
        p = kids(p, j);
    }
    return cnt(p);
}

其实主要操作就这两个,我们考虑一下空间和时间复杂度:

时间复杂度很明显是与字符串长度相关的,我们每处理一个字符走一个节点,也就是 \(O(L)\) 的复杂度,那么总的复杂度就是 \(O(NL)\)

至于空间复杂度,每处理一个字符串至多新建 \(L\) 个节点,那么就是 \(O(L)\) ,每一个节点的大小关乎字符串的字符集大小,所以我们认为是 \(O(C)\) 那么总共就是 \(O(NLC)\) ,但是,在实际中,远远达不到此复杂度(除非毒瘤出题人想卡你),例如最初的图,一共 4 个字符串,但是只有 5 个节点……

例题

【模板】字典树 - 洛谷

注意题意,以询问所给作为前缀,求有多少个字符串满足此前缀

那么我们需要魔改一下 insert 函数即可……将 ++cnt(p) 放入循环中即可

还请读者仔细思考

[USACO12DEC]First! G - 洛谷

这道题非常的神奇……考虑先建Trie树,如果某一个字符串的字典序比其他任何字符串都大,那么一定不存在为其前缀的字符串。

再考虑字典序,如果使 s 其字典序最大,那么每一个分叉点上,s[i] 比其他所有存在的分叉都要大。

如样例:omm, moo, mom

如果要使 omm 最大那么在第一层上满足 o > m,其他层上没有分叉。

如果要使 moo 最大,那么第一层上满足 m > o,第三层上满足 o > m,条件相悖,所以不可行。

其他同理。

那么我们如何判断条件相悖?可以借鉴 2-SAT 的思路,通过大于关系建图,如果存在环,那么不可行。

判环用拓扑,谁用Tarjan啊

最终,每一个串判断一遍即可。

[BJOI2016]IP地址 - 洛谷

这道题就是Trie的一种特殊用法。

有点类似线段树的区间标记。

我们考虑改变一个规则对其整个子树都有影响,那么我们考虑什么时候影响抵消?更深的点会阻挡了标记的下传。那么我们记录一下各个点的标记情况,通过类似线段树的方法下传标记即可。

正确性显然。

扩展

Trie树实际上是 AC自动机 和 回文自动机 等自动机的载体,需要经过一点点小变换。

在此不展开叙述,详见我的其他文章。

与算法学习笔记(15): Trie(字典树)相似的内容:

算法学习笔记(15): Trie(字典树)

# Trie树 Trie(字典树)是一种用于实现字符串检索的多叉树。 Trie的每一个节点都可以通过 `c` 转移到下一层的一个节点。 > 我们可以看作可以通过某个字符转移到下一个字符串状态,直到转移到最终态为止。这是后话…… 我们以插入了字符串 `ab`,`aa`,`b` 三个字符串的Trie树为

算法学习笔记(20): AC自动机

# AC自动机 **前置知识**: - 字典树:可以参考我的另一篇文章 [算法学习笔记(15): Trie(字典树)](https://www.cnblogs.com/jeefy/p/17101290.html) - ~~KMP~~:可以参考 [KMP - Ricky2007](https://ww

谱图论:Laplacian二次型和Markov转移算子

以下部分是我学习CMU 15-751: TCS Toolkit的课堂笔记。接下来将要介绍的是谱图论(spectral graph theory)的关键,也就是Laplacian二次型(Laplacian quadratic form)。直观地理解,Laplacian二次型刻画了图的“能量”(ener...

算法学习笔记(6): 树链剖分

树链剖分 树链剖分是一个很神奇,但是在树上可以完成一些区间操作问题 简单来说,就是把一棵树分成一条条的链,通过维护链上的信息来维护整棵树的信息 基础知识可以参考我的另外一篇博客:算法学习笔记(5): 最近公共祖先(LCA) 这里假设你已经掌握了上述博客中的所有相关知识,并清晰了其背后的原理 性质?发

算法学习笔记(11): 原根

原根 此文相对困难,请读者酌情食用 在定义原根之前,我们先定义其他的一点东西 阶 通俗一点来说,对于 $a$ 在模 $p$ 意义下的阶就是 $a^x \equiv 1 \pmod p$ 的最小正整数解 $x$ 或者说,$a$ 在模 $p$ 意义下生成子群的阶(群的大小) 再或者说,是 $a$ 在模

算法学习笔记(30):Kruskal 重构树

Kruskal 重构树 这是一种用于处理与最大/最小边权相关的一个数据结构。 其与 kruskal 做最小生成树的过程是类似的,我们考虑其过程: 按边权排序,利用并查集维护连通性,进行合并。 如果我们在合并时,新建一个节点,其权值为当前处理的边的权值,并将合并的两个节点都连向新建的节点,那么就可以得

算法学习笔记(3.1): ST算法

ST表 在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。 当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\

C++算法之旅、09 力扣篇 | 常见面试笔试题(上)算法小白专用

算法学习笔记,记录容易忘记的知识点和难题。详解时空复杂度、50道常见面试笔试题,包括数组、单链表、栈、队列、字符串、哈希表、二叉树、递归、迭代、分治类型题目,均带思路与C++题解

C++算法之旅、08 基础篇 | 质数、约数

算法学习笔记,记录容易忘记的知识点和难题。试除法、分解质因数、筛质数、约数个数、约数之和、最大公约数

算法学习笔记(1): 欧几里得算法及其扩展

扩展欧几里得算法详解 在了解扩欧之前我们应该先了解欧几里得算法 欧几里得算法 这是一个递归求最大公约数(greatest common divisor)的方法 $$ gcd(a, b) = gcd(b, a % b) $$ 可以通过一个类似的简单公式推导而来 好像叫做辗转相减法来着? $$ gcd(