字典树
-
-
算法训练营第三章
-
-
-
名称:字典树(Trie树)
-
本质:哈希树的一种变种。
-
一些abstract:用于统计、排序和存储大量的字符串(不限于字符串),经常用于文本词频统计。优点:利用公共前缀减少查询时间,减少字符串比较。
字典树是一棵多叉树,树的每一条边代表一个字符串的字母。在字典树上一个单词就是从根节点开始一直向下走直到遇到一个带有end标记的位置。
存储形式是一个二维数组,第一维度代表节点个数,第二维度代表都有可能是什么字符,相当于数字和字符的对应,数组的值代表当前节点代表的单词word沿着这个字符a的边组成的字符串word+a是哪个节点序号代表的字符串。
-
-
操作:
所有字符串长度相加记为N。字符的种类记为k。插入的字符串长度为n。
创建字典树总复杂度为O(N)。
空间复杂度为O(Nk)。
-
创建:插入一个字符串到字典树中。
板子:
tot = 1; // tot代表当前有多少个节点,默认只有一个根节点,代表空串
void insert(string s) {
int len = s.length(), p = 1; // 默认1为根节点
for(int i = 0; i < len; i++) {
int ch = s[i] - 'a';
if (!trie[p][ch]) {
trie[p][ch] = ++tot; // 如果当前位置没有出现过,就新建一个节点
}
p = trie[p][ch]; // 沿着ch边往下走
}
end[p] = true; // end数组代表节点序号是不是一个字符串的末尾。
}复杂度:为单个字符串的时间复杂度为O(n)。
-
查找某字符串是否存在:沿着字典树走,看看字符串能否走完,走不完不存在,走完了如果没有终止标记则也不存在。
板子:
bool search(string s) {
int len = s.length, p = 1;
for(int i = 0; i < len; i++) {
p = trie[p][s[i]-'a'];
if(!p) return false;
}
return end[p];
}复杂度:为单个字符串的时间复杂度为O(n)。
-
-
应用以及优缺点?
应用1:字符串检索,可以看看字符串是否出现过、出现的频率。
应用2:统计一个串所有前缀单词的个数,可以判断一个单词是否为另一些单词的前缀。
应用3:看字符串之间的公共前缀。
应用4:排序,从小的字母开始遍历,按照遇到end为true的顺序就是字典序。
应用5:后期的一些自动机都是在trie树的基础上构造的。
-
例题:
-
最长异或路径:
注意这种路径的问题,都可以转化成,dfs求出每个节点到根节点的路径的异或和,然后所有的路径都可以变成两端节点的异或和的异或值。于是用O(n)的时间处理出一个异或和数组w,变成求出i, j使得w[i] ^ w[j]最大。
这是一个经典的0/1字典树的问题,先在0/1字典树中看怎么到达当前数字的各位取反,比如这个数为27个0后面1010的31位二进制数。显然优先找前面数字的反比较好,就沿着字典树找27个1和0101,对于每个位置的数,如果找到了就沿着走,找不到就只能沿着另一条路走,而不管怎么走,都是已有的数字构成的路。(因为找到了说明二者异或这一位是1,而如果这一位为0,后面就算全是1也不是最大。)找到最大值,然后插入新的数字,继续找下一个数的答案。
所以,大概是这样的代码:
// 提前算出来异或和数组
void dfs(int u, int fa) { // 当前节点u,父亲节点fa
for(int i=head[u];i;i=e[i].next) {
to = e[i].to;
if (to==fa) continue;
dx[to] = dx[u] ^ e[i].w;
dfs(v, u);
}
}
// 倒着插入 因为越大的位越要先匹配,如果这一位不能满足,后面就算全满足也不会有这样的结果大
void insert(int num) {
int p = 1; // 根为1
for(int i=30;i>=0;i--) {
bool k = num&(1<<i);
if (!trie[p][k]) trie[p][k] = ++tot; // 新建
p = trie[p][k];
}
}
// 找这个数在当前0/1字典树中的最优解
int find(int num) {
int p = 1, res = 0;
for(int i=30;i>=0;i--) {
bool k = num&(1<<i);
if (trie[p][k^1]) { // 如果找到了匹配的路径
res += (1<<i);
p = trie[p][k^1];
} else {
p = trie[p][k];
}
}
return res;
}
-