Trie(字典树)是一种用于实现字符串快速检索的多叉树结构。Trie的每个节点都拥有若干个字符指针,若在插入或检索字符串时扫描到一个字符c,就沿着当前节点的c字符指针,走向该指针指向的节点。下面我们来详细讨论Trie的基本操作过程。
初始化:
一颗空Trie仅包含一个根节点,该点的字符指针均指向空。
插入:
当需要插入一个字符串S时,我们令一个指针P起初指向根节点。然后,依次扫描S中的每个字符c。
1.若P的c字符指针指向一个已经存在的节点Q,则令P = Q
2.若P的c字符指针指向空,则新建一个节点Q,令P的c字符指针指向Q,然后令P = Q
当S中的字符扫描完毕时,在当前节点P上标记它是一个字符串的末尾。
检索:
当需要检索一个字符串S在Trie中是否存在时,我们令一个指针P起初指向根节点,然后依次扫描S中的每个字符c:
1.若P的c字符指针指向空,则说明S没有被插入过Trie,结束检索
2.若P的c字符指针指向一个已经存在的节点Q,则令P = Q
当S中的字符扫描完毕时,若当前节点P被标记为一个字符串的末尾,则说明S在Trie中存在,否则说明S没有被插入过Trie
在上图所示的例子中,需要插入和检索的字符串都由小写字母构成,所以Trie的每个节点具有26个字符指针
,分别为a到z。可以看出在Trie中,字符数据都体现在树的边(指针)上,树的节点仅保存一些额外信息,例如单词结尾标记等。其空间复杂度时O(NC),其中N是节点个数,C是字符集的大小。
1、AcWing 142. 前缀统计
题意 :
- 给定 N 个字符串 S1,S2…SN,接下来进行 M 次询问,每次询问给定一个字符串 T,求 S1∼SN 中有多少个字符串是 T 的前缀。
- 输入字符串的总长度不超过 10^6,仅包含小写字母。
思路 :
- 不论是插入还是检索,一开始指针p都初始化为0,如果当前这个节点是空的,如果是插入,那么新建这个节点并将指针p指向这个节点,如果是检索,说明没有这个字符串,就break
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e6 + 10;
int n, m;
int son[N][26], idx;
int cnt[N];
char s[N];
void insert() {
int p = 0;
for (int i = 0; s[i]; ++ i) {
int ch = s[i] - 'a';
if (!son[p][ch]) son[p][ch] = ++ idx;
p = son[p][ch];
}
++ cnt[p];
}
int search() {
int p = 0, res = 0;
for (int i = 0; s[i]; ++ i) {
int ch = s[i] - 'a';
if (!son[p][ch]) break;
p = son[p][ch];
res += cnt[p];
}
return res;
}
int main() {
scanf("%d%d", &n, &m);
while (n -- ) {
scanf("%s", s);
insert();
}
while (m -- ) {
scanf("%s", s);
printf("%d\n", search());
}
}
2、AcWing 143. 最大异或对
题意 :
- 在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
思路 :
- “选出两个”,因此可以用Trie来找在确定某一个(即遍历)的情况下,另一个可以使结果最大的值
- 一定注意我们是从最高位开始存的!因为是要结果最大,因此位越高,权重越大
#include <iostream>
using namespace std;
const int N = 1e5 + 10, M = 31 * 1e5 + 10;
int n, a[N];
int son[M][2], idx;
void insert(int x) {
int p = 0;
for (int i = 30; i >= 0; -- i) {
int &s = son[p][x >> i & 1];
if (!s) s = ++ idx;
p = s;
}
}
int search(int x) {
int p = 0, res = 0;
for (int i = 30; i >= 0; -- i) {
int now = x >> i & 1;
if (son[p][!now]) {
res += 1 << i;
p = son[p][!now];
} else p = son[p][now];
}
return res;
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; ++ i) {
scanf("%d", &a[i]);
insert(a[i]);
}
int mx = 0;
for (int i = 0; i < n; ++ i) mx = max(mx, search(a[i]));
printf("%d", mx);
}
3、AcWing 144. 最长异或值路径
题意 :
- 给定一个树,树上的边都具有权值。
- 树中一条路径的异或长度被定义为路径上所有边的权值的异或和:
思路 :
- 假设d[i]表示根节点到x的路径上所有边权的xor值
- 由于 a xor a = 0的性质,我们可以得知树上x到y的路径上所有边权的异或和等于d[x] xor d[y]
- 因此,问题转化为从d[1] ~ d[n]这n个数中选两个,xor的结果最大,即求解“最大异或对”
- 注意是h[u] = idx ++ ;!!!而不是++idx
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10, K = 3e6 + 10;
int n;
int h[N], c[M], e[M], ne[M], idx;
int d[N];
int son[K][2];
void add(int u, int v, int w) {
e[idx] = v; c[idx] = w; ne[idx] = h[u]; h[u] = idx ++ ;
}
void dfs(int u, int father, int sum) {
d[u] = sum;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j != father) dfs(j, u, sum ^ c[i]);
}
}
void insert(int x) {
int p = 0;
for (int i = 30; i >= 0; -- i) {
int now = x >> i & 1;
if (!son[p][now]) son[p][now] = ++ idx;
p = son[p][now];
}
}
int search(int x) {
int p = 0, res = 0;
for (int i = 30; i >= 0; -- i) {
int now = x >> i & 1;
if (son[p][!now]) {
res += 1 << i;
p = son[p][!now];
} else p = son[p][now];
}
return res;
}
int main() {
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 0, u, v, w; i < n - 1; ++ i) {
scanf("%d%d%d", &u, &v, &w);
add(u, v, w);
add(v, u, w);
}
dfs(0, -1, 0);
for (int i = 0; i < n; ++ i) insert(d[i]);
int mx = 0;
for (int i = 0; i < n; ++ i) mx = max(mx, search(d[i]));
printf("%d", mx);
}