[字符串学习笔记] 4. Trie
4.1. 概念
Trie 又称 字典树,是储存 字符 的一种树形数据结构。
对于 \(\texttt{abbc}, \texttt{abc}, \texttt{bbb}\) 这些字符串,所构造的 Trie 如下:

Trie 用边来表示一个字母,而根结点至另一个节点的路径链表示一个字符串。如 \(0 \to 1 \to 2 \to 5\) 表示 \(\texttt{abc}\),而 \(0 \to 6 \to 7\) 表示 \(\texttt{bb}\)。
通常来说,\(\delta(u, i)\) 表示节点 \(u\) 通过边权为 \(i\) 的边到达的下一个节点。边权在字符串的 字符集 内,即 \(i \in \Sigma\)。
01 Trie 指字符集 \(\Sigma = \{\texttt 0, \texttt 1\}\) 的 Trie,通常用来解决异或等位运算问题。
4.2. 实现
Trie 的 插入 操作简单直观。先令当前节点 \(u\) 为根,一步步深入即可。如果要确认一条路径是否是插入的字符串,则要维护一个 字符串末尾 标识。
时间复杂度 \(\Theta(|s|)\)。
int top, tr[...][26];
bool e[...];
void insert(string s) {
int u = 0;
for (char i : s) {
if (tr[u][i - 'a'] == 0)
tr[u][i - 'a'] = ++top;
u = tr[u][i - 'a'];
}
e[u] = 1;
}
查询 操作分为很多种。这里以查询是否在给定的字符串集中出现为例,时间复杂度 \(\Theta(|s|)\):
bool query(string s) {
int u = 0;
for (char i : s) {
if (tr[u][i - 'a'] == 0)
return 0;
u = tr[u][i - 'a'];
}
return e[u];
}
注意,Trie 数组第一维为字符总个数,第二维为 \(|\Sigma|\)。
例题 CF898C Phone Numbers
题解
由于后缀不好处理,将所有的字符串翻转。
首先可以注意到,如果 只取 根结点到叶子结点的字符串,就不会取到某一个字符串的后缀。使用 dfs 解决即可。
对于不同的名字,使用 unordered_map 存储结构体化的 Trie 即可。
参考代码:
struct Trie {
int top, tr[N][10];
vector<string> res;
void insert(string s) {
// 略
}
void query(int u, string s) {
bool leaf = 1;
for (int i = 0; i < 10; i++)
if (tr[u][i]) {
query(tr[u][i], s + char(i + '0'));
leaf = 0;
}
if (leaf) {
res.push_back(s);
return;
}
}
};
unordered_map<string, Trie> S;
signed main() {
cin >> n;
while (n--) {
cin >> name >> m;
while (m--) {
cin >> tel;
reverse(tel.begin(), tel.end());
S[name].insert(tel);
}
}
cout << S.size() << '\n';
for (auto [i, j] : S) {
j.query(0, "");
cout << i << ' ' << j.res.size() << ' ';
for (string k : j.res) {
reverse(k.begin(), k.end());
cout << k << ' ';
}
cout << '\n';
}
}
例题 UVA11488 Hyper Prefix Sets
题解
令 \(s(u)\) 表示根节点到 \(u\) 的路径链所代表的字符串。将 Trie 考虑为一棵树,则
逆向思考,假设答案所选的字符串集 \(T\) 的 \(\operatorname{lcp}\) 为 \(s(p)\)。
由于要最大化 \(|s(p)| \times |T|\),肯定要 尽量多选 有 \(s(p)\) 这个前缀的字符串。在 Trie 的插入操作中,深入到节点 \(p\) 时次数 \(l[p] \gets l[p] + 1\)。插入完所有的字符串后,\(l[p]\) 就是有 \(s(p)\) 这个前缀的字符串个数。
同时,为了快速获得 \(s(p)\) 的长度,也要记录节点的深度 \(d[p]\)。故答案为 \(\max d[i] \times l[i]\)。
参考代码:
void insert(string s) {
int u = 0;
for (int i : s) {
int &v = tr[u][i - '0'];
if (v == 0) v = ++top;
d[v] = d[u] + 1;
u = v;
l[u]++;
}
}
void solve() {
cin >> n;
// 初始化,略
for (int i = 1; i <= n; i++) {
cin >> s;
insert(s);
}
int res = 0;
for (int i = 1; i <= top; i++)
res = max(res, d[i] * l[i]);
cout << res << '\n';
}
例题 UVA12506 Shortest Names
题解
不难发现,如果节点 \(u\) 在插入 Trie 时经过的次数 \(l_u = 1\),那么根到 \(u\) 的链便仅为一个字符串的前缀。
查询时同时记录根到当前节点的长度,第一次 \(l_u = 1\) 时直接返回即可。
void insert(string s) {
int u = 0;
for (char i : s) {
if (tr[u][i - 'a'] == 0)
tr[u][i - 'a'] = ++top;
u = tr[u][i - 'a'];
l[u]++;
}
}
int query(string s) {
int u = 0, d = 1;
for (char i : s) {
if (l[tr[u][i - 'a']] == 1)
return d;
u = tr[u][i - 'a'];
d++;
}
return d;
}
void solve() {
cin >> n;
// 初始化,略
for (int i = 1; i <= n; i++) {
cin >> s[i];
insert(s[i]);
}
int res = 0;
for (int i = 1; i <= n; i++)
res += query(s[i]);
cout << res << '\n';
}
例题 SP10381 DICT - Search in the dictionary!
题解
先对所有字符串建 Trie,同样维护 Trie 上以 \(u\) 结尾的字符串个数 \(e[u]\)。每一次询问 \(s\) 时,
- 若 Trie 内不包含 \(s\),显然无解。
- 否则,从 \(s\) 的末尾节点开始 dfs,如果搜到节点 \(u\) 满足 \(e[u]\) 为真,就将根到 \(u\) 路径代表的字符串存入答案。
参考代码:
void dfs(int u, string s) {
for (int i = 0; i < 26; i++)
if (tr[u][i]) {
if (e[tr[u][i]])
res.push_back(s + char(i + 'a'));
dfs(tr[u][i], s + char(i + 'a'));
}
}
void query(string s) {
int u = 0;
for (char i : s) {
if (tr[u][i - 'a'] == 0) {
cout << "No match.\n";
return;
}
u = tr[u][i - 'a'];
}
dfs(u, s);
sort(res.begin(), res.end());
for (string i : res)
cout << i << '\n';
}
例题 UVA1401 Remember the Word
题面
给定字符串 \(s\) 及字符串集 \(\mathbf S\),求有多少不同的有序子集 \(\mathbf T \subseteq \mathbf S\),满足 \(\mathbf T\) 中的字符串按顺序拼接后与 \(s\) 相等。
例如,当 \(s = \texttt{abcd}\),\(\mathbf S = \{\texttt a, \texttt b, \texttt{cd}, \texttt{ab}\}\) 时,\(\mathbf T\) 可能为 \(\{\texttt a, \texttt b, \texttt{cd}\}\) 或 \(\{\texttt{ab}, \texttt{cd}\}\)。
题解
考虑 dp。令 \(f[i]\) 表示组成后缀 \(s[i \ldots |s| - 1]\) 的方案数。当前 \(i\) 下的 \(s[i \ldots |s| - 1]\) 记作 \(p\)。
显然有转移方程
将 \(\mathbf S\) 内的字符串存入 Trie,按照转移方程 dp 即可。
参考代码:
void DP() {
f[s.size()] = 1;
for (int i = s.size() - 1; i >= 0; i--) {
int u = 0;
for (int j = i; j < s.size(); j++) {
if (tr[u][s[j] - 'a'] == 0)
break; // 不存在直接退出
u = tr[u][s[j] - 'a'];
if (e[u])
f[i] = (f[i] + f[j + 1]) % P;
}
}
}
void solve(int tc) {
// 初始化,略
cin >> s >> n;
while (n--) {
cin >> t;
insert(t);
}
DP();
cout << "Case " << tc << ": " << f[0] << '\n';
}

浙公网安备 33010602011771号