保姆级教学——字典树

字典树

字典树的原理是复用前缀信息,所以字典树又叫前缀树。

构建过程

这里只介绍基本的构建框架,因为字典树的能实现的功能很多,所以结点信息种类也很多,不可能把所有的信息都写上,所以只写框架,后续再根据题目自己补充。

假设字符集是 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

模版题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();
}
相关推荐
adam_life13 天前
【P8306 【模板】字典树】
数据结构·算法·字典树·trie·哈希表··结构体
逝雪Yuki4 个月前
数据结构与算法——字典(前缀)树的实现
数据结构·c++·字典树·前缀树·左程云
逝雪Yuki4 个月前
牛客——接头密匙
c++·字典树·前缀树·数据结构与算法
mikey棒棒棒8 个月前
前缀树(Trie)(字典树)
字典树·前缀树·trie
一直学习永不止步1 年前
LeetCode题练习与总结:数组中两个数的最大异或值--421
java·算法·leetcode·字典树·数组·位运算·哈希表
神探阿航1 年前
数据结构——Trie
数据结构·c++·算法·字典树·trie
闻缺陷则喜何志丹2 年前
【字典树(前缀树) 哈希映射 后序序列化】1948. 删除系统中的重复文件夹
linux·c++·算法·哈希算法·字典树·哈希映射·后序序列化
闻缺陷则喜何志丹2 年前
【回溯 字典树(前缀树)】212. 单词搜索 II
c++·算法·力扣·字典树·前缀树·回溯·单词
EQUINOX12 年前
LeetCode 第390场周赛个人题解
算法·leetcode·职场和发展·线段树·字典树·贪心