数据结构 - Trie 树

Trie 树又称为字典树,前缀树, 是一种用于高效存储和查找的字符串集合的数据结构。

结构如上图,头节点都为空,表示开始的节点。然后依次往下走,如果当前有以某一个字母为开头的继续往下走。如果没有创建出来,继续往下走。然后在以字母结尾的位置做出一个标记。记录有多少个字母是以它结尾的。


题目: Trie字符串统计

来源: https://www.acwing.com/problem/content/description/837/

维护一个字符串集合,支持两种操作:

“I x”向集合中插入一个字符串x;

“Q x”询问一个字符串在集合中出现了多少次。

共有N个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。

输入格式

第一行包含整数N,表示操作数。

接下来N行,每行包含一个操作指令,指令为”I x”或”Q x”中的一种。

输出格式

对于每个询问指令”Q x”,都要输出一个整数作为结果,表示x在集合中出现的次数。

每个结果占一行。

数据范围

1≤N≤2∗10^4

输入样例:

1
2
3
4
5
6
5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:

1
2
3
1
0
1

C++实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

using namespace std;

const int N = 1e5 + 10;
// 因为是只有小写字母,所以第二维度的值为26
// cnt 表示以某一个节点结尾的个数有多少个
// idx为下标的使用情况, 题目规定所有字符串的总长度是10^5
int son[N][26], cnt[N], idx;
char str[N];

// 插入操作
void insert(char *str) {
int p = 0;
for(int i = 0; str[i]; i++) {
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p]++;
}

// 查询操作
int query(char *str) {
int p = 0;
for(int i = 0; str[i]; i++) {
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}

int main() {
int n;
cin >> n;
while(n--) {
string op;
cin >> op >> str;
if (op == "I") insert(str);
else printf("%d\n", query(str));
}
return 0;
}

题目: 最大异或对

来源: https://www.acwing.com/problem/content/145/

在给定的N个整数$A_1,A_2, ……, A_N$中选出两个进行xor(异或)运算,得到的结果最大是多少?

输入格式
第一行输入一个整数N。

第二行输入N个整数$A_1~A_N$。

输出格式
输出一个整数表示答案。

数据范围
$1≤N≤10^5$,
$0≤A_i<2^{31}$
输入样例:

1
2
3
1 2 3

输出样例:

1
3

暴力解法: 时间复杂度$O(n^2)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

int a[N], n;

int main() {
cin >> n;
for(int i = 0; i < n; i++) cin >> a[i];

int res = 0;
for(int i = 0; i < n; i++) {
for(int j = 0; j < i; j++) {
res = max(res, a[i] ^ a[j]);
}
}

cout << res << endl;
return 0;
}

字典树解法:时间复杂度$O(n)$

当某一位有异或值得时候就往有异或值为1的那条路径往下走,如果没有就直接往下走。

C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>
#include <algorithm>

using namespace std;

// M 表示节点个数, N = 10^5, Ai = 31 位数
const int N = 1e5 + 10, M = 3e6 + 10;

// idx 表示节点的使用情况
int son[M][2], idx;
int a[N], n;

// 如果有节点就往下走,没有就创建出来继续往下走
void insert(int x) {
int p = 0;
for(int i = 30; ~i; i--) {
int &s = son[p][x >> i & 1];
if (!s) s = ++idx;
p = s;
}
}

// 查询
// 当某一个节点有异或值得时候,那么加上该位的值,res += 1 >> i; 没有就是 res += 0 >> i, 这里等于0 所以省略掉
int query(int x) {
int p = 0, res = 0;
for(int i = 30; ~i; i--) {
int s = x >> i & 1;
if (son[p][!s]) {
res += 1 << i;
p = son[p][!s];
} else p = son[p][s];
}
return res;
}


int main() {
// 读入数据
cin >> n;
for(int i = 0; i < n; i++) {
cin >> a[i];
insert(a[i]); // 创建字典树
};

// 查询
int res = 0;
for(int i = 0; i < n; i++) res = max(res, query(a[i]));

cout << res << endl;
return 0;
}