浅析字典树
字典树是一种比较特殊的树,边上有边权(至少我是怎么理解的)。又名“Trie”树。
大概长这样:
(插入了cap,cat,csp,co,code)
边上是字符集,点上是点的编号,涂成蓝色的代表是单词的结尾。
字符集可以是a-z
,0-9
,true和false
等等。
字典树有效的组织了单词的关系,节省了时间和空间。
插入单词
void insert(char *s) {
int p = 1, len = strlen(s);
for (int i = 0; i < len; ++i) {
bool now = s[i] - 'a';
if (trie[p][now] == 0) trie[p][now] = ++m;
p = trie[p][now];
}
isend[p] = true;
}
这里trie[x][c]
表示从节点x
由c
这条边能达到节点trie[x][c]
。
其中,m
为节点总数计数,要在插入前赋初始值为 (1) 。
这里isend
用于标记是否是结尾,有时还需要记录次数。
查询
每道题都有他查询的内容,需要因地制宜的写查询函数。
查询前缀是否出现:
bool query(char *s) { // 此代码没有编译运行测试过,仅供思路参考,不建议直接复制。
int p = 1, len = strlen(s);
for (int i = 0; i < len; ++i) {
bool now = s[i] - 'a';
if (trie[p][now] == 0) return false;
p = trie[p][now];
}
return true;
}
查询出现次数(注意配套修改insert
函数):
int query(char *s) { // 此代码没有编译运行测试过,仅供思路参考,不建议直接复制。
int p = 1, len = strlen(s);
for (int i = 0; i < len; ++i) {
bool now = s[i] - 'a';
if (trie[p][now] == 0) return 0;
p = trie[p][now];
}
return isend[p];
}
应用
本文主要谈应用字典树解题,如果上面的讲解不明白,可以自行Baidu一下qwq。
例题一 于是他错误的点名开始了
这题是字典树最基本的运用,查询存在性和重复性。
把所有化学生的姓名插入字典树,再在每次教练点名时,查询。
例题二 The XOR Largest Pair
这题看似和字典树没啥关系,也找不到可以插入的字符串。
最容易想到的做法就是暴力,两两枚举xor
,时间复杂度 (O(n^2)) 。
其实这里可以把每个数的二进制值插入字典树中,
枚举每个数,在字典树中查询与它尽量二进制按位尽量相反的树。
code:
void insert(int x) {
int p = 1;
for (int i = 30; i >= 0; --i) {
bool now = (x & (1 << i));
if (trie[p][now] == 0) trie[p][now] = ++m;
p = trie[p][now];
}
isend[p] = true;
}
int lookup(int x) {
int res = 0;
int p = 1;
for (int i = 30; i >= 0; --i) {
bool now = (x & (1 << i));
now = !now;
if (trie[p][now]) { // 成功匹配,此位相反
res |= (1 << i);
p = trie[p][now];
} else { // 匹配失败,这一位只能相等
p = trie[p][!now];
}
}
return res;
}
例题三 最长XOR序列
描述:还是给你一个序列,要求这个序列的某个子序列的异或和最大,求这个最大值。
(注:子序列是连续的,子串才是不连续的。
异或和是指连续异或的值,如4,7,10
的异或和是4 xor 7 xor 10
)
这里要用到异或的几个性质:
-
(a operatorname{xor} b operatorname{xor} c=(a operatorname{xor} b)operatorname{xor} c=aoperatorname{xor}(boperatorname{xor}c))
-
(a operatorname{xor} a=0)
-
(aoperatorname{xor}0=a)