字典树
字典树的原理是复用前缀信息,所以字典树又叫前缀树。
构建过程
这里只介绍基本的构建框架,因为字典树的能实现的功能很多,所以结点信息种类也很多,不可能把所有的信息都写上,所以只写框架,后续再根据题目自己补充。
假设字符集是 a ∼ z \mathrm a\sim\mathrm z a∼z 共 26 \mathrm {26} 26 个字符最开始字典树有一个根结点 1 \mathrm 1 1,然后这个根结点需要维护 26 26 26 条边 ( c h a r , v ) (\mathrm {char},\mathrm v) (char,v),表示这条边对应的字符种类,以及这条边的另一个端点 v \mathrm v v。最初 26 \mathrm {26} 26 条边都不存在。
加入字符串
最初我们在树根,设其深度为 d e e p \mathrm {deep} deep,根节点的深度为 0 \mathrm 0 0,全局维护一个结点池 c n t \mathrm {cnt} cnt,初值为 1 \mathrm 1 1。我们要将字符串 s \mathrm s s 加入树中:
- 对于当前字符 s d e e p \mathrm {s_{deep}} sdeep,检查当前结点是否存在对应字符的边 ( s d e e p , v ) \mathrm {(s_{deep},v)} (sdeep,v);
- 如果存在,则前往下一个结点 v \mathrm v v,重复检查过程;
- 如果不存在,那么给当前结点建边 ( s d e e p , c n t + 1 ) \mathrm {(s_{deep},cnt+1)} (sdeep,cnt+1),然后跳转到下一个结点 c n t + 1 \mathrm {cnt+1} cnt+1;
- 直到访问到字符串最后一位字符。
查询字符串是否存在
最初我们在树根,我们查询字符串 s \mathrm s s 是否在树中出现过。
- 对于当前字符 s d e e p \mathrm {s_{deep}} sdeep,检查当前结点是否存在对应字符的边 ( s d e e p , v ) \mathrm {(s_{deep},v)} (sdeep,v);
- 如果存在,则前往下一个结点 v \mathrm {v} v;
- 如果不存在,直接报告不存在;
- 如果访问到最后一个字符仍存在,那么报告存在。
删除字符串
这个具体题目有具体的删除方式,主要是看我们给每个结点定义了怎样的信息,参照之前每个字符串是如何贡献的,删除字符串就是将字符串的贡献取消。
模板1
查询某个字符串是否出现,以及是否出现过两次。
需要给每个结点加一些额外信息,即 e n d \mathrm{end} end,其中 e n d \mathrm {end} end 表示当前结点以末尾的形式出现的次数。
cpp
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18
const int M = 1e5;
const int N = 1e4;
int tr[50 * N + 50][27];
int End[50 * N + 50];
int cnt = 1;
int exist (string &s) {
int next = 1;
for (int i = 0; i < s.size(); i++) {
if (!tr[next][s[i] - 'a']) {
return 0;
}
next = tr[next][s[i] - 'a'];
}
return End[next];
}
void add (string &s) {
int next = 1;
for (int i = 0; i < s.size(); i++) {
if (!tr[next][s[i] - 'a']) {
tr[next][s[i] - 'a'] = ++cnt;
}
next = tr[next][s[i] - 'a'];
}
End[next] ++;
return;
}
void slove () {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
string s;
cin >> s;
add (s);
}
int m;
cin >> m;
for (int i = 1; i <= m; i++) {
string s;
cin >> s;
int t = exist(s);
if (t != 0) add (s);
if (t == 0) cout << "WRONG" << endl;
else if (t == 1) cout << "OK" << endl;
else cout << "REPEAT" << endl;
}
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
slove();
}
模板2
判断在所有字符串中,是否存在一个字符串是另一个字符串的前缀。
实现这个功能,我们需要给结点维护两个信息,一个是 p a s s \mathrm {pass} pass,一个是 e n d \mathrm {end} end。
p a s s \mathrm {pass} pass 统计的是,当前结点被经过多少次, e n d \mathrm {end} end 统计的是,以当前结点为终点的字符串有多少个。
那么我们只需要判断,是否存在一个结点的 e n d \mathrm {end} end 大于 0 \mathrm 0 0 的情况下, p a s s \mathrm {pass} pass 至少为 2 \mathrm 2 2。
所以只需要按顺序添加字符串,然后判断这个字符串的末尾结点的 p a s s \mathrm{pass} pass 是否大于 1 \mathrm{1} 1。
cpp
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18
const int M = 1e5;
const int N = 1e4;
int tr[50 * N + 50][27];
int End[50 * N + 50];
int Pass[50 * N + 50];
int cnt = 1;
int exist (string &s) {
int next = 1;
for (int i = 0; i < s.size(); i++) {
if (!tr[next][s[i] - '0']) {
return 0;
}
next = tr[next][s[i] - '0'];
}
return next;
}
void add (string &s) {
int next = 1;
Pass[next] ++;
for (int i = 0; i < s.size(); i++) {
if (!tr[next][s[i] - '0']) {
tr[next][s[i] - '0'] = ++cnt;
}
next = tr[next][s[i] - '0'];
Pass[next] ++;
}
End[next] ++;
return;
}
void slove () {
for (int i = 1; i <= cnt; i++) {
for (int j = 0; j < 10; j++) {
tr[i][j] = 0;
}
Pass[i] = 0;
End[i] = 0;
}
cnt = 1;
int n;
cin >> n;
int flag = 0;
vector <string> v (n + 1);
for (int i = 1; i <= n; i++) {
cin >> v[i];
add(v[i]);
}
for (int i = 1; i <= n; i++) {
int next = exist (v[i]);
if (Pass[next] > 1) {
flag = 1;
}
}
if (flag) cout << "NO" << endl;
else cout << "YES" << endl;
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int T;
cin >> T;
while (T--)
slove();
}
异或字典树
将每个二进制当作一个字符串 s t r i n g \mathrm {string} string,构建一棵字符集为 { 0 , 1 } \mathrm {\{0,1\}} {0,1} 的异或字典树。
最长异或路径
给定一棵 n \mathrm n n 个点的带权树,结点下标从 1 \mathrm 1 1 开始到 n \mathrm n n。求树中所有异或路径的最大值。
异或路径指的是树上两个结点之间唯一路径上的所有边权的异或值。
解
找到一条路径 ( X , Y ) \mathrm {(X,Y)} (X,Y) 使得路径异或值最大,而路径 ( X , Y ) \mathrm {(X,Y)} (X,Y) 的异或值这其实等价于 X \mathrm {X} X 到 L C A ( X , Y ) \mathrm {LCA(X,Y)} LCA(X,Y) 的路径异或值异或值 异或上 Y \mathrm {Y} Y 到 L C A ( X , Y ) \mathrm {LCA(X,Y)} LCA(X,Y) 的路径异或值。
进一步地等价于 X \mathrm {X} X 到 R o o t \mathrm {Root} Root 的路径异或值 异或上 Y \mathrm Y Y 到 R o o t \mathrm {Root} Root 的路径异或值。
所以,我们其实只需要预处理出每个点到 R o o t \mathrm {Root} Root 的路径异或值,特别注意 R o o t \mathrm {Root} Root 到 R o o t \mathrm {Root} Root 自己的路径异或值是 0 0 0。
所以我们现在的问题就变成,任选这 n \mathrm n n 个值( n \mathrm n n 个路径异或值)中的两个,使得二者的异或之和最大。
把这 n \mathrm n n 个异或值用二进制方式存入字典树内,不妨令所有异或值均具有 32 \mathrm {32} 32 位,我们从最高位开始存入字典树,树高是 32 \mathrm {32} 32。
将所有二进制存入字典树后,我们要想最终异或的结果最大,那么就要尽可能地让高位二进制位不同。
枚举每个异或值作为答案之一,另外一个异或值就需要贪心地从 t i r e \mathrm {tire} tire 树里面找。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18
const int N = 1e5 + 7;
int trie[32 * N][3];
int cnt = 1;
void add (string &s) {
int st = 1;
for (int i = 0; i < s.size(); i++) {
if (!trie[st][s[i] - '0']) {
trie[st][s[i] - '0'] = ++cnt;
}
st = trie[st][s[i] - '0'];
}
}
// 查找与给定字符串 s 异或后最大的字符串
int query (string &s) {
int ans = 0, st = 1, deep = 31;
for (int i = 0; i < s.size(); i++) {
int f = s[i] - '0';
if (trie[st][1 ^ f]) {
ans += (1ll << deep);
st = trie[st][1 ^ f];
} else {
st = trie[st][f];
}
deep --;
}
return ans;
}
void slove () {
int n;
cin >> n;
vector <PII> g[n + 1];
for (int i = 1; i <= n - 1; i++) {
int u, v, w;
cin >> u >> v >> w;
g[u].emplace_back(v, w);
}
vector <int> dp (n + 1, 0);
function <void(int, int)> dfs =[&] (int u, int fa) {
for (auto [v, w] : g[u]) {
if (v == fa) continue;
dp[v] = dp[u] ^ w;
dfs (v, u);
}
};
dfs (1, 0);
int ans = 0;
for (int i = 1; i <= n; i++) {
string s;
for (int j = 31; j >= 0; j--) {
if (dp[i] & (1ll << j)) s += '1';
else s += '0';
}
add(s);
}
for (int i = 1; i <= n; i++) {
string s;
for (int j = 31; j >= 0; j--) {
if (dp[i] & (1ll << j)) s += '1';
else s += '0';
}
ans = max(ans, query(s));
}
cout << ans << endl;
}
signed main () {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
slove();
}