自动机模型+字典树v2.0
有时候我们要维护一个字符串集合,然后支持插入、删除、查询某个字符串出现次数和查询某个字符串作为前缀的出现次数。
显然的,暴力肯定 T 飞。
hash:我来!(非常好数据,使我的 hash WA)
所以我们需要字典树。
字典树有三大两大优点:
-
速度快
-
无失误(hash 有一定概率会冲突)
-
支持多模式串匹配
但是,字典树的内存特别庞大,所以数据规模庞大的时候还是乖乖用 hash 吧。
不过,字典树似乎不支持快速判断区间回文串(hash + 树状数组可以
字典树的结构
下图是一个由
可以看到,字典树是一个有向树,其中红色节点是“接收状态”。
解释
至于为什么,首先需要理解自动机的概念。自动机用于判断一组信号序列是否合法(很像图灵机是不是)。
比如说,家门口-韶山路-东塘-中雅培粹对面,就是一个可以去学校的信号序列。
但是,又比如家门口-砂子塘社区-砂子塘小学,也是一个可以去学校的信号序列,尽管它会去到另一个学校。自动机上的接收状态不是唯一的。
以下是一个判断某个数是否是偶数(以二进制输入)的自动机:
而 Trie 树,就是在给定一个字符串后,如果这个字符串是字符串集合的一个串,那么就接受(上面的“查询某个字符串出现次数和查询某个字符串作为前缀的出现次数”需要两个 Trie,但是可以压成一个)
字典树的操作
插入
很简单,遇到不存在的节点创造一个。
具体来说,我们把字符一个一个插入进去。发现节点不存在之后,我们新建一个节点,然后继续插入。
下图是上面的字典树插入
绿色的节点是新来的节点,棕色的是新来的接受节点(红+绿=棕)
在插入的时候,记得维护信息。当维护的信息过于复杂的时候,循环写法可能会特别复杂,此时推荐递归。
void _insert(node *cur, int id, const string &x) { if(id == x.size()) { cur -> cnt++; cur -> sb++; return ; } cur -> sb++; if(cur -> son[x[id]] == NULL) { cur -> son[x[id]] = new node; } _insert(cur -> son[x[id]], id + 1, x); } void insert(const string &s) { _insert(root, 0, s); }
删除
像插入一样,我们从最下面的节点开始进行,在维护信息的时候,如果这个节点不需要了,就将它删除。
下图是删除
由于类似于“迭代器失效”的问题,所以删除的递归会比循环好写。
bool _erase(node *cur, int id, const string &x) { if(id == x.size()) { cur -> cnt--; cur -> sb--; return 1; } if(cur == NULL) { return 0; } cur -> sb--; bool tmp = _erase(cur -> son[x[id]], id + 1, x); if((!(cur -> sb)) && cur != root) { delete cur; } return tmp; } //返回值代表是否成功删除 bool erase(const string &s) { return _erase(root, 0, s); }
查找某个字符串|数某个字符串的出现次数
我们顺着字典树的边往下走,如果到达的状态是接受状态,那就表明找到了。
如下图(绿色的边代表走过的边,蓝色的点代表走到的点)展示了查找
查找某个前缀
还是往下走,但是到达的状态不必是接受状态,只要存在就行了。但是,为了这个任务,我们需要额外维护子树和。
如图展示了查找
node *_find(node *cur, int id, const string &x) { if(id == x.size()) { return cur; } if(cur == NULL) { return cur; } return _find(cur -> son[x[id]], id + 1, x); } //维护了子树和和出现次数,所以一个 find 就足够做上面的所有任务 node *find(const string &s) { return _find(root, 0, s); }
完整版
写的指针+封装。
#include <cstddef> #include <vector> #undef NULL #define NULL nullptr struct trie { struct node { node *son[256]; int cnt, sb; node() : cnt(0), sb(0) { std::fill(son, son + 256, NULL); } node(const node &b) : cnt(b.cnt), sb(b.sb) { std::copy(b.son, b.son + 256, son); } ~node() { for(int i = 0; i < 256; i++) { delete son[i]; } } }; node *root; trie() : root(NULL) { root = new node; } ~trie() { delete root; } void _insert(node *cur, int id, const string &x) { if(id == x.size()) { cur -> cnt++; cur -> sb++; return ; } cur -> sb++; if(cur -> son[x[id]] == NULL) { cur -> son[x[id]] = new node; } _insert(cur -> son[x[id]], id + 1, x); } void insert(const string &s) { _insert(root, 0, s); } bool _erase(node *cur, int id, const string &x) { if(id == x.size()) { cur -> cnt--; cur -> sb--; return 1; } if(cur == NULL) { return 0; } cur -> sb--; bool tmp = _erase(cur -> son[x[id]], id + 1, x); if((!(cur -> sb)) && cur != root) { delete cur; } return tmp; } //返回值代表是否成功删除 bool erase(const string &s) { return _erase(root, 0, s); } node *_find(node *cur, int id, const string &x) { if(id == x.size()) { return cur; } if(cur == NULL) { return cur; } return _find(cur -> son[x[id]], id + 1, x); } //维护了子树和和出现次数,所以一个 find 就足够做上面的所有任务 node *find(const string &s) { return _find(root, 0, s); } };
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具