Trie字典树
\(Trie\)字典树
概念
设想一个问题:给定一篇文章 \(T\) 和一系列字符串 \(S_i\),求每一个 \(S_i\) 在 \(T\) 中的出现次数。
显然如果对于每一个 \(S_i\),均与 \(T\) 中与每一个字符串 \(T_i\) 比较,其时间复杂度达到了 \(O(|S|·|T|)\),显然无法处理大量的数据。
我们再观察每一次比较步骤:
- 先比较 \(S_i\) 与 \(T_j\) 的第一个字符
- 如果相等则继续比较第二个字符,以此类推
- 如果不相等则直接返回
显然,我们希望 \(S_i\) 与 \(T_j\) 再尽量靠前的地方产生差异。而对于每一个 \(S_i\),所有 \(T_j\) 中与 \(S_i\) 相等的前缀都没有用,每次反而重复计算,于是我们有了字典树:
字典树 \(Trie\),指的是将一个字符串集合 \(S\) 转换为一种特殊的有根树,如下图:
graph TB
1(( ))
2((A))
3((A))
4((A))
5((G))
6((T))
7((C))
8((G))
9((A))
1--->2
2--->3
3--->4
3--->5
1--->6
6--->7
6--->8
7--->9
\(S = \{AAA,AAG,T,TCA,TG\}\)
很容易看出。\(Trie\)字典树很好的利用了公共前缀,节约了储存空间和访问时间
算法实现
对于每一个节点 \(u\),我们有两个属性:
-
与 \(u\) 相连的子节点 \(v\)
注意:每一次访问节点都会查找它的所有子节点,这时我们直接用数组下标表示这个字母,节约时间
-
这个节点作为字符串末尾的次数,值为 \(0\) 则代表这不是一个字符串末尾
const int maxn = 1e4,maxl = 10,maxc = 26; // 最大字符串数量,最大字符串长度,最大字符值域(0~9 = 10,a~z = 26)
struct node {
int cnt; // 作为末尾的次数
int to[maxc + 5]; // to[t]:值为t的子节点的编号
};
int nodecnt = 0; // 节点计数器
node tree[maxn + 5]; // 树
插入字符串
我们从根节点(超级原点,确保第一位不相同也能插入)开始插入,当前为 \(u\),插入字符串为 \(S_t\),当前为第 \(t\) 位:
- 寻找 \(u\) 的子节点 \(v_{S_t}\),如果没有编号则新建节点
- \(u\) 向下移动到 \(v_{S_t}\),\(t\) 向后向后移一位至 \(S_{t+1}\)
- 重复上述步骤直到整个 \(S\) 插入完毕,末尾计数器增加
void insert(char *s) { // 插入s
int u = 0,len = strlen(s); // 当前节点(0为超级原点)
for (int i = 0;i < len;i ++) { // 依次插入s的每一个字符
int t = s[i] - 'a'; // 当前字符对应值
if (tree[u].to[t] == 0) tree[u].to[t] = ++ nodecnt; // 新建节点
u = tree[u].to[t]; // 向下转移
}
tree[u].cnt ++; // 末尾计数器增加
}
查询字符串(字符串存在与否,字符串数量)
查询字符串基本逻辑与插入差不多,从根开始查找:
- 寻找 \(u\) 的子节点 \(v_{S_t}\),如果没有编号则直接返回 \(0\)(这个节点下方没有匹配的子节点,代表没有这个字符串)
- \(u\) 向下移动到 \(v_{S_t}\),\(t\) 向后向后移一位至 \(S_{t+1}\)
- 重复上述步骤直到整个 \(S\) 插入完毕,返回末尾计数器(如果等于 \(0\) 则相当于没有这个字符串)
int count(char *s) { // 查询s
int u = 0,len = strlen(s); // 当前节点(0为超级原点)
for (int i = 0;i < len;i ++) { // 依次访问s的每一个字符
int t = s[i] - 'a'; // 当前字符对应值
if (tree[u].to[t] == 0) return 0; // 没有找到
u = tree[u].to[t]; // 向下转移
}
return tree[u].cnt; // 返回末尾计数器
}
查询前缀关系
定义:查找文章 \(T\) 中有多少个字符串 \(T_i\) 是给定字符串 \(S_i\) 的前缀
查询字符串的前缀关系基本逻辑与查询差不多,从根开始查找:
- 寻找 \(u\) 的子节点 \(v_{S_t}\),如果没有编号则直接返回答案 \(ans\)(这个节点下方没有匹配的子节点,代表查询结束)
- \(u\) 向下移动到 \(v_{S_t}\),\(t\) 向后向后移一位至 \(S_{t+1}\)
- 答案 \(ans\) 增加 \(u\)(当前为 \(v_{s_t}\)) 的末尾计数器,注意:一定在转移后增加(根没有意义)
- 重复上述步骤直到整个 \(S\) 插入完毕,返回记录的答案 \(ans\)
int countPre(char *s) { // 查询s的前缀数量
int u = 0,len = strlen(s); // 当前节点(0为超级原点)
int ans = 0; // 答案记录
for (int i = 0;i < len - 1;i ++) { // 依次访问s的每一个字符
int t = s[i] - 'a'; // 当前字符对应值
if (tree[u].to[t] == 0) break; // 查找完毕
u = tree[u].to[t]; // 向下转移
ans += tree[u].cnt; // 记录答案
}
return ans; // 返回答案
}
例题
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5e5,maxc = 26;
struct TRIE {
struct node {
int cnt;
int to[maxc + 5];
int flag;
};
int nodecnt = 0;
node tree[maxn + 5];
void insert(char *s) {
int u = 0,len = strlen(s);
for (int i = 0;i < len;i ++) {
int t = s[i] - 'a';
if (tree[u].to[t] == 0) tree[u].to[t] = ++ nodecnt;
u = tree[u].to[t];
}
tree[u].cnt ++;
tree[u].flag = 1;
}
bool find(char *s) {
int u = 0,len = strlen(s);
for (int i = 0;i < len;i ++) {
int t = s[i] - 'a';
if (tree[u].to[t] == 0) return 0;
u = tree[u].to[t];
}
return tree[u].flag;
}
int count(char *s) {
int u = 0,len = strlen(s);
for (int i = 0;i < len;i ++) {
int t = s[i] - 'a';
if (tree[u].to[t] == 0) return 0;
u = tree[u].to[t];
}
return tree[u].cnt;
}
};
TRIE trie;
char s[55];
int main() {
int n; scanf("%d",&n);
for (int i = 1;i <= n;i ++) {
scanf("%s",s);
trie.insert(s);
}
int m; scanf("%d",&m);
for (int i = 1;i <= m;i ++) {
scanf("%s",s);
int t = trie.count(s);
if (t == 0) printf("WRONG\n");
else if (t == 1) {
printf("OK\n");
trie.insert(s);
} else printf("REPEAT\n");
}
return 0;
}

浙公网安备 33010602011771号