字典树

目录
  • 字典树
    • 1. 算法分析
    • 2. 模板
      • 2.1 字符串操作
      • 2.2 数字操作
    • 3. 典型例题

字典树

1. 算法分析

trie树既可以对字符串进行操作,也可以对数字进行操作

  • 对字符串进行操作:把字符串的每一个字符看成一个结点
  • 对数字进行操作:每一个数字都可以看成是一个32位的二进制数,通过这种方式把每一个数字看成是一个长度为32的字符串,然后套用对字符串的操作

2. 模板

2.1 字符串操作

  1. “I x”向集合中插入一个字符串x;
  2. “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;
}

你可能感兴趣的:(字典树)