学习笔记:字典树 (Trie 树)

字典树的构建与检索

字典树,顾名思义,就是查字典怎么查,在这个树里就怎么检索。Trie 这个名字来自英文单词 retrieval(检索)。

树的边上有字符,而从祖先到子孙的一条路径就是一个字符串。从一个节点不会向下延伸出相同字符的边。

对于插入字符串,我们动态开点,与之前字符串匹配的前缀共用节点,直到出现新的字符再开点。

设 \(tr(i,c)\) 表示 \(i\) 节点向下沿字符 \(c\) 的边指向的节点。检索一个字符串是容易的。

(上面说的好像依托史,还是看代码吧。)

单次插入或查询字符串 \(s\) 的时间复杂度为 \(O(|s|)\)。总空间复杂度为 \(O(N|\Sigma|)\),其中 \(N\) 为字符串的最大长度。

洛谷-P8306 【模板】字典树 代码:

#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const N = 3e6 + 10;
int tt, n, q;
int t[70][N], cnt[N], idx;
string s;
int work(char c) {
    if (isupper(c)) return c - 'A';
    if (islower(c)) return c - 'a' + 26;
    return c - '0' + 52;
}
void add(string s) {
    int l = s.length(), p = 0;
    f(i, 0, l - 1) {
        int c = work(s[i]);
        if (!t[c][p]) t[c][p] = ++idx;
        p = t[c][p];
        ++cnt[p];
    }
    return;
}
int query(string s) {
    int l = s.length(), p = 0;
    f(i, 0, l - 1) {
        int c = work(s[i]);
        if (!t[c][p]) return 0;
        p = t[c][p];
    }
    return cnt[p];
}
void solve() {
    f(i, 0, 61) f(j, 0, idx) t[i][j] = 0;
    f(i, 1, idx) cnt[i] = 0;
    idx = 0;
    cin >> n >> q;
    f(i, 1, n) {
        cin >> s;
        add(s);
    }
    f(i, 1, q) {
        cin >> s;
        cout << query(s) << '\n';
    }
}
signed main() {
    cin.tie(0)->sync_with_stdio(false);
    
    cin >> tt;
    while (tt--) solve();
    
    return 0;
}

01-Trie

字符集为 \(\{0,1\}\) 的 Trie。

01-Trie 的经典应用,就是在一堆 01 串中查找与给定 01 串的异或和最大的。

由于只要高位 \(i\) 为 \(1\),低位无论有多少 \(0\) 也比 \(i\) 位为 \(0\) 的大,所以我们贪心地从高位到低位匹配,找与当前位相反的,如果有就选,没有就不选,这样得到的串与给定串的异或和一定是最大的。

单次插入或查询时间复杂度 \(O(\log V)\),总空间复杂度 \(O(n\log V)\),其中 \(V\) 为插入的二进制数的值域大小。

可持久化 Trie

其实线段树的结构与 Trie 差不多,都是二叉树或者多叉树,并且可以从上到下修改,每次修改只与一条路径有关。

因此可持久化 Trie 与可持久化线段树的思路也相似,每次在上一版本的基础上连边,只连与当前添加的串有关的节点。代码见例题。

例题

洛谷-P4551 最长异或路径

给定一棵树,边有权值,定义路径的权值为边的权值的异或和。

求所有 \((i,j)\) 之间的最短路径中,权值的最大值。

\(1\le n\le10^5\)。

设从节点 \(u\) 到节点 \(v\) 的简单路径上边权的异或和为 \(d(u,v)\),那么由于异或满足 \(x\oplus x=0\),有 \(d(u,v)=d(1,u)\oplus d(1,v)\)。

因此 DFS 求出所有 \(d(1,i)\),加入 01-Trie,再查询使 \(d(1,j)\oplus d(1,i)\) 最大的 \(d(1,j)\),更新答案即可。

Source code - Virtual Judge

洛谷-P4735 最大异或和(可持久化 01-Trie)

给定一个非负整数序列 \(a_1,\cdots,a_N\),初始长度为 \(N\)。有 \(M\) 个操作,有以下两种操作类型:

  1. A x:添加操作,表示在序列末尾添加一个数 \(x\),序列的长度 \(N\) 加 \(1\)。
  2. Q l r x:询问操作,找到一个位置 \(p\),满足 \(l \le p \le r\),使得:\(a_p\oplus a_{p+1}\oplus\dots\oplus a_N\oplus x\) 最大,输出最大值。

\(1\le N,M \le 3\times 10 ^ 5\),\(0\leq a_i\leq 10 ^ 7\)。

首先计算前缀和,则询问转化为找到 \(l-1\le p\le r-1\) 使得 \(s_p\oplus(s_N\oplus x)\) 最大。Source code - Virtual Judge

Codeforces-1720D2 Xor-Subsequence (hard version)

给定长度为 \(n\) 的 0-indexed 序列 \(a\)。称一个子序列(其下标序列为 \(b_0,b_1,\cdots,b_{m-1}\))是美丽的,当且仅当:

  • 对于所有 \(i=0,1,2,\cdots,m-2\),有 \(a_{b_i}\oplus b_{i+1}<a_{b_{i+1}}\oplus b_i\),其中 \(\oplus\) 表示按位异或运算。

求出最长的美丽子序列长度。\(\sum n\le3\cdot10^5\)。

Easy version: \(0\le a_i\le200\)。

Hard version: \(0\le a_i\le10^9\)。

考虑朴素的线性 DP,设 \(f(i)\) 表示以 \(i\) 结尾的最长美丽子序列长度。那么

\[f(i)=\max f(j)+1,\qquad\text{where }a_j\oplus i<a_i\oplus j. \]

对于 easy version,枚举 \(a_j\),选择满足条件的最大的 \(f(j)\) 进行转移即可。

对于 hard version,我们考虑如何快速找到合法的 \(j\)。考虑将条件转化,尽量变成只需要维护关于 \(i\) 的函数的问题。

设 \(X=a_j\oplus i\),\(Y=a_i\oplus j\),要使得 \(X<Y\),一定存在 \(k\) 使得 \(X\) 和 \(Y\) 的前 \(k\) 位相同,而 \(X\) 的第 \(k+1\) 位为 \(0\)、\(Y\) 的第 \(k+1\) 位为 \(1\)。因此从高到低位考虑。

(下面的 \(a_j,i,a_i,j\) 都表示对应的一个二进制位。)

对前 \(k\) 位有 \(a_j\oplus i=a_i\oplus j\),即 \(a_i\oplus i=a_j\oplus j\)。

对第 \(k+1\) 位有 \(a_j\oplus i=0,a_i\oplus j=1\),即下面两种情况之一:

  • \(a_j=i=a_i=\overline{j}\),即 \(a_i\oplus i=0,a_j\oplus j=1\),且 \(j=\overline i\);
  • \(a_j=i=\overline{a_i}=j\),即 \(a_i\oplus i=1,a_j\oplus j=0\),且 \(j=i\)。

综上所述,\(a_j\oplus i<a_i\oplus j\) 等价于存在 \(k\ge0\) 使得 \(a_i\oplus i\) 和 \(a_j\oplus j\) 的前 \(k\) 位相同,且第 \(k+1\) 位满足:

  • \(a_i\oplus i\) 和 \(a_j\oplus j\) 不同;
  • \(j=a_i\oplus1\)。

于是就转化为了 0/1 Trie 上搜索的问题。

Submission #385508087 - Codeforces

参考资料

posted @ 2026-09-10 16:58  f2021ljh  阅读(19)  评论(0)    收藏  举报