- 字典树
- 1. 算法分析
- 2. 模板
- 2.1 字符串操作
- 2.2 数字操作
- 3. 典型例题
字典树
1. 算法分析
trie树既可以对字符串进行操作,也可以对数字进行操作
- 对字符串进行操作:把字符串的每一个字符看成一个结点
- 对数字进行操作:每一个数字都可以看成是一个32位的二进制数,通过这种方式把每一个数字看成是一个长度为32的字符串,然后套用对字符串的操作
2. 模板
2.1 字符串操作
- “I x”向集合中插入一个字符串x;
- “Q x”询问一个字符串在集合中出现了多少次。
#include
using namespace std;
int const N = 2e4 + 10;
int idx, cnt[N], son[N][26]; // idx唯一标识每一个字符,idx = 0表示根节点, cnt[i]记录以idx = i结尾的字符串的出现次数, son[p][u]表示以p为父节点,u为子节点的字符,如果存在那么son[p][u] = idx
char s[N];
// 插入操作
void insert(char s[]) {
int p = 0; // 从根节点往下
for (int i = 0; s[i]; ++i) { // 循环每一个字符
int u = s[i] - 'a'; // 当前字符是什么
if (!son[p][u]) son[p][u] = ++idx; // 判断当前字符在trie树中是否存在,不存在就生成
p = son[p][u]; // 父节点往下走
}
cnt[p] ++; // 记录当前字符串的数目
}
// 询问操作
int query(char s[]) {
int p = 0; // 从根节点开始
for (int i = 0; s[i]; ++i) { // 循环每一个字符
int u = s[i] - 'a'; // 当前字符是什么
if (!son[p][u]) return 0; // 如果当前字符不存在,表示这个字符串没有出现过
p = son[p][u]; // 当前字符存在,父节点往下走
}
return cnt[p]; // 返回当前字符串的数目
}
int main() {
int n;
cin >> n;
while (n--) {
char op[2];
scanf("%s", op);
if (op[0] == 'I') {
scanf("%s", s);
insert(s);
}
else {
scanf("%s", s);
printf("%d\n",query(s));
}
}
return 0;
}
2.2 数字操作
acwing最大异或对
在给定的N个整数A1,A2……AN中选出两个进行xor(异或)运算,得到的结果最大是多少?
#include
using namespace std;
int const N = 1e5 + 10;
int idx, son[N * 32][2];
// 插入操作
void insert(int num) {
int p = 0; // 从根节点开始
for (int i = 30; i >= 0; --i) { // 从第30位开始
int u = (num >> i & 1); // 判断num这一位上的数字
if (!son[p][u]) son[p][u] = ++idx; // 如果不存在,就建立一个结点
p = son[p][u]; // 从父节点往下走
}
}
// 查询操作
int query(int num) {
int p = 0, ans = 0; // 从根节点开始
for (int i = 30; i >= 0; --i) { // 从第30位开始
int u = (num >> i & 1); // 判断这一位上的数字
if (son[p][u ^ 1]) { // 如果能选择和num第i位上数字不同的数字的那条路
p = son[p][u ^ 1]; // 走这条路
ans |= (1 << i); // 更新答案
}
else p = son[p][u]; // 没得选,只能走和num第i位上数字相同的那条路
}
return ans;
}
int main() {
int n;
scanf("%d", &n);
int ans = 0;
while (n--) {
int num;
scanf("%d", &num);
insert(num);
ans = max(ans, query(num));
}
cout << ans;
return 0;
}
3. 典型例题
POJ 1056 IMMEDIATE DECODABILITY
题意: 问是否有一个串S,S是另一个字符串的前缀
题解: 直接Trie建立,再从头遍历一遍,要注意的是,自己是自己的前缀,统计前缀个数时要注意一下
代码:
#include
#include
#include
#include
using namespace std;
int const N = 1e4;
int son[N][26], cnt[N], flg = 1, kase = 1, idx, s_cnt;
char s[1000][105];
// 插入操作
void insert(char s[]) {
int p = 0; // 从根节点往下
for (int i = 0; s[i]; ++i) { // 循环每一个字符
int u = s[i] - '0'; // 当前字符是什么
if (!son[p][u]) son[p][u] = ++idx; // 判断当前字符在trie树中是否存在,不存在就生成
p = son[p][u]; // 父节点往下走
cnt[p]++;
}
return;
}
// 询问操作
int query(char s[]) {
int p = 0; // 从根节点开始
int res = 0;
for (int i = 0; s[i]; ++i) { // 循环每一个字符
int u = s[i] - '0'; // 当前字符是什么
if (!son[p][u]) return 0;
p = son[p][u]; // 当前字符存在,父节点往下走
}
return cnt[p] >= 2;
}
int main() {
while (scanf("%s", s[s_cnt++]) != EOF) {
while(scanf("%s", s[s_cnt++]) && s[s_cnt - 1][0] != '9');
for (int i = 0; i < s_cnt - 1; ++i) insert(s[i]);
for (int i = 0; i < s_cnt - 1; ++i)
if (query(s[i])) flg = 0;
if (flg) printf("Set %d is immediately decodable\n", kase++);
else printf("Set %d is not immediately decodable\n", kase++);
memset(cnt, 0, sizeof cnt);
memset(son, 0, sizeof son);
idx = 0, flg = 1, s_cnt = 0;
}
return 0;
}
POJ 2503 Shortest Prefixes
题意: 给你一堆字符串,让你给出每个字符串的最短标识前缀(即这个前缀只存于这一个字符串中)字符串数目<=1000,每个长度<=100
题解: 把所有字符串插入到字典树中,每次插入的时候记录一下前缀。然后每个字符串开始进行查询,查询的时候看当前字符串的前缀数目是否大于等于2,如果是输出当前字符,不是则输出当前字符后return
代码:
#include
#include
#include
#include
using namespace std;
int const N = 2e4 + 10;
char s[1010][21];
int cnt[N], idx, son[N][26], s_cnt;
// 插入操作
void insert(char s[]) {
int p = 0; // 从根节点往下
for (int i = 0; s[i]; ++i) { // 循环每一个字符
int u = s[i] - '0'; // 当前字符是什么
if (!son[p][u]) son[p][u] = ++idx; // 判断当前字符在trie树中是否存在,不存在就生成
p = son[p][u]; // 父节点往下走
cnt[p]++;
}
return;
}
// 询问操作
void query(char s[]) {
int p = 0; // 从根节点开始
int res = 0;
for (int i = 0; s[i]; ++i) { // 循环每一个字符
int u = s[i] - '0'; // 当前字符是什么
p = son[p][u]; // 当前字符存在,父节点往下走
if (cnt[p] >= 2) printf("%c", s[i]);
if (cnt[p] == 1) {
printf("%c", s[i]);
return;
}
}
return ;
}
int main() {
while (scanf("%s", s[s_cnt++]) != EOF) insert(s[s_cnt - 1]);
for (int i = 0; i < s_cnt; ++i) {
printf("%s ", s[i]);
query(s[i]);
printf("\n");
}
return 0;
}
codeforces 613 D Dr. Evil Underscores
题意: 给定n个数字(a1, a2, ..., an),找到一个x,使得max(x xor ai)最小,输出这个最小值
题解: 本题可以建立一颗字典树,而后一旦发现分支只有0,那么走0的分支,对答案没有贡献;一旦发现分支只有1,那么走1的分支,对答案没有贡献;一旦发现两个分支都有,那么做两个分支都往下递归,然后取较小的那个即可
代码:
#include
using namespace std;
int n;
int const N = 1e5 + 10;
int a[N];
int son[N * 32][2], idx;
// 建立字典树
void insert(int x)
{
int p = 0;
for (int i = 30; i >= 0; --i)
{
int u = (x >> i & 1);
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
}
// 递归判断
int dfs(int root, int floor)
{
if (floor == -1) return 0;
if (!son[root][0]) return dfs(son[root][1], floor - 1);
else if (!son[root][1]) return dfs(son[root][0], floor - 1);
else if (son[root][0] && son[root][1]) return (1 << floor) + min(dfs(son[root][1], floor - 1), dfs(son[root][0], floor - 1));
}
int main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
{
scanf("%d", &a[i]);
insert(a[i]);
}
int ans = dfs(0, 30);
cout << ans << endl;
return 0;
}
luoguP2292 [HNOI2004]L语言
题意: n个字符串组成字典,m个模式串,询问每个模式串的前缀能被理解的长度,能被理解的前缀指这个前缀是否是由若干个字典中的单词组成,即符合要求的前缀是从模式串开头到最后一个完整单词的末尾位置。
1 <= n <= 20, 1 <= m <= 50, 1 <= |字符串长度s| <= 10, 1 <= |模式串长度t| <= 2*10^6^
题解: dp[i]表示到长度为i的时候前缀是由字符串组成,因此dp[i] = dp[j] && ex[j, i](ex[j, i]表示j ~i 这段是一个单词)。而查询j ~ i这段是否为一个单词,可以使用trie树查询,一开始的时候先建立trie树,这样就知道所有的前缀
代码:
#include
using namespace std;
int const N = 2e6 + 10;
unordered_map res;
int son[N][26], cnt[N], idx, n, m, dp[N];
char s[N];
void insert(char s[]) {
int p = 0;
for (int i = 1; s[i]; ++i){
int u = s[i] - 'a';
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p]++; // 记录这个字符串可以作为前缀
}
int query(char s[]) {
if (res[s + 1]) return res[s + 1]; // 记忆化
int ans = 0;
int len = strlen(s + 1);
for (int i = 0; i <= len; ++i) dp[i] = 0;
dp[0] = 1;
for (int i = 0; i <= len; ++i) {
if (!dp[i]) continue;
else ans = i;
for (int j = i + 1, p = 0; j <= len; ++j) { // 按照dp的转移状态划分
int u = s[j] - 'a';
if (!son[p][u]) break;
p = son[p][u];
if (cnt[p] > 0) dp[j] = 1; // i~j这段是一个字符串,那么dp可以转移
}
}
return res[s + 1] = ans; // 返回时顺便记录一下答案
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) { // 插入所有的字符串
scanf("%s", s + 1);
insert(s);
}
for (int i = 1; i <= m; ++i) {
scanf("%s", s + 1);
printf("%d\n", query(s));
}
return 0;
}
hdu 3460-Ancient Printer
题意: 给一台打印机,但这台打印机比较古老,只有三种功能:
1 打印的都是小写字母
2 只能在 尾部追加 或者删除一个字母
3 打印当前单词
问输入的几个字符串 最少 多少步数可以打印完
题解: 画图可以看出,打印的过程就像是dfs的过程,每个除了最后一个字符串外其他字符串都可以被搜索两遍。因此贪心地想,要打印次数最少,那么最长的字符串被搜索1次。则答案为 字典树上点个数*2+n-最长字符串长度
代码:
#include
using namespace std;
int const N = 5e5 + 10;
int son[N][26], n, idx, maxv = -1;
char s[55];
void insert(char s[]) {
int p = 0;
for (int i = 0; s[i]; ++i) {
int u = s[i] - 'a';
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
return;
}
int main() {
while (cin >> n) {
maxv = -1;
memset(son, 0, sizeof son);
idx = 0;
for (int i = 1; i <= n; ++i) {
scanf("%s", s);
maxv = max(maxv, (int)strlen(s));
insert(s);
}
cout << idx * 2 + n - maxv << endl;
}
return 0;
}
HDU 4825 Xor Sum
题意: Zeus 和 Prometheus 做了一个游戏,Prometheus 给 Zeus 一个集合,集合中包含了N个正整数,随后 Prometheus 将向 Zeus 发起M次询问,每次询问中包含一个正整数 S ,之后 Zeus 需要在集合当中找出一个正整数 K ,使得 K 与 S 的异或结果最大。Prometheus 为了让 Zeus 看到人类的伟大,随即同意 Zeus 可以向人类求助。你能证明人类的智慧么?
T<10,1=N,M<=100000
题解: acwing最大异或对的模板题
代码:
#include
using namespace std;
typedef long long LL;
int const N = 32 * 1e5;
LL son[N][2], T, n, m, idx, kase = 1, val[N];
void insert(LL num) {
LL p = 0;
for (int i = 32; i >= 0; --i) {
int u = (num >> i & 1);
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
val[p] = num;
}
LL query(LL num) {
LL p = 0;
for (int i = 32; i >= 0; --i) {
int u = (num >> i & 1);
if (son[p][u ^ 1]) p = son[p][u ^ 1];
else p = son[p][u];
}
return val[p];
}
int main() {
cin >> T;
while (T--) {
memset(son, 0, sizeof son);
idx = 0;
LL t;
printf("Case #%d:\n", kase++);
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%lld", &t);
insert(t);
}
for (int i = 1, t; i <= m; ++i) {
scanf("%lld", &t);
printf("%lld\n", query(t));
}
}
return 0;
}
poj3764 The xor-longest Path
题意: 给定一棵N个节点的树,树上的每条边都有一个权值,从树中选择两个点x和y,把从x到y路径上的所有边权xor异或起来,得到的最大结果是多少。
题解: 一条路径的异或值=根节点到第一个端点的异或值^根节点到第二个端点的亦或值,因此先dfs预处理出所有根节点到任意一个点的异或值,然后套上acwing最大异或对的操作即可
代码:
#include
#include
#include
using namespace std;
int const N = 1e5 + 10;
int idx, son[N * 32][2], n, e[N * 32 * 2], ne[N * 32 * 2], w[N * 32 * 2], h[N], idx2, val[N * 32];
void add(int a, int b, int c) {
e[idx2] = b, w[idx2] = c, ne[idx2] = h[a], h[a] = idx2++;
}
// 插入操作
void insert(int num) {
int p = 0; // 从根节点开始
for (int i = 31; i >= 0; --i) { // 从第30位开始
int u = (num >> i & 1); // 判断num这一位上的数字
if (!son[p][u]) son[p][u] = ++idx; // 如果不存在,就建立一个结点
p = son[p][u]; // 从父节点往下走
}
}
// 查询操作
int query(int num) {
int p = 0, ans = 0; // 从根节点开始
for (int i = 31; i >= 0; --i) { // 从第30位开始
int u = (num >> i & 1); // 判断这一位上的数字
if (son[p][u ^ 1]) { // 如果能选择和num第i位上数字不同的数字的那条路
p = son[p][u ^ 1]; // 走这条路
ans |= (1 << i); // 更新答案
}
else p = son[p][u]; // 没得选,只能走和num第i位上数字相同的那条路
}
return ans;
}
// dfs预处理出所有根节点到任意一个点的异或值
void dfs(int u, int fa, int sum) {
val[u] = sum;
insert(sum);
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j == fa) continue;
dfs(j, u, sum ^ w[i]);
}
return;
}
int main() {
while (scanf("%d", &n) != EOF) {
memset(son, 0, sizeof son);
memset(val, 0, sizeof val);
idx = idx2 = 0;
memset(h, -1, sizeof h);
for (int i = 1; i <= n - 1; ++i) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
a++, b++;
add(a, b, c), add(b, a, c);
}
dfs(1, -1, 0); // dfs预处理出所有根节点到任意一个点的异或值
int res = -1;
for (int i = 1; i <= n; ++i) {
// cout << val[i] << endl;
res = max(res, query(val[i]));
}
printf("%d\n", res);
}
return 0;
}