自动机模型+字典树v2.0
有时候我们要维护一个字符串集合,然后支持插入、删除、查询某个字符串出现次数和查询某个字符串作为前缀的出现次数。
显然的,暴力肯定 T 飞。
hash:我来!(非常好数据,使我的 hash WA)
所以我们需要字典树。
字典树有三大两大优点:
-
速度快
-
无失误(hash 有一定概率会冲突)
-
支持多模式串匹配
但是,字典树的内存特别庞大,所以数据规模庞大的时候还是乖乖用 hash 吧。
不过,字典树似乎不支持快速判断区间回文串(hash + 树状数组可以 \(O(\log N)\) 完成)。
字典树的结构
下图是一个由 \(\{ \text{am}, \text{are}, \text{be}, \text{been}, \text{can} \}\) 构成的 Trie:
可以看到,字典树是一个有向树,其中红色节点是“接收状态”。
解释
至于为什么,首先需要理解自动机的概念。自动机用于判断一组信号序列是否合法(很像图灵机是不是)。
比如说,家门口-韶山路-东塘-中雅培粹对面,就是一个可以去学校的信号序列。
但是,又比如家门口-砂子塘社区-砂子塘小学,也是一个可以去学校的信号序列,尽管它会去到另一个学校。自动机上的接收状态不是唯一的。
以下是一个判断某个数是否是偶数(以二进制输入)的自动机:
而 Trie 树,就是在给定一个字符串后,如果这个字符串是字符串集合的一个串,那么就接受(上面的“查询某个字符串出现次数和查询某个字符串作为前缀的出现次数”需要两个 Trie,但是可以压成一个)
字典树的操作
插入
很简单,遇到不存在的节点创造一个。
具体来说,我们把字符一个一个插入进去。发现节点不存在之后,我们新建一个节点,然后继续插入。
下图是上面的字典树插入 \(\text{argue}\) 的效果:
绿色的节点是新来的节点,棕色的是新来的接受节点(红+绿=棕)
在插入的时候,记得维护信息。当维护的信息过于复杂的时候,循环写法可能会特别复杂,此时推荐递归。
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);
}
删除
像插入一样,我们从最下面的节点开始进行,在维护信息的时候,如果这个节点不需要了,就将它删除。
下图是删除 \(\text{been}\) 的效果:
由于类似于“迭代器失效”的问题,所以删除的递归会比循环好写。
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);
}
查找某个字符串|数某个字符串的出现次数
我们顺着字典树的边往下走,如果到达的状态是接受状态,那就表明找到了。
如下图(绿色的边代表走过的边,蓝色的点代表走到的点)展示了查找 \(\text{are}\):
查找某个前缀
还是往下走,但是到达的状态不必是接受状态,只要存在就行了。但是,为了这个任务,我们需要额外维护子树和。
如图展示了查找 \(\text{ar}\) 的前缀的效果,表示法同上:
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);
}
};