数据结构——trie(字典)树

数据结构------trie(字典)树

  • [字典 (trie) 树](#字典 (trie) 树)
    • 字典树的实现
    • [P8306 【模板】字典树 - 洛谷](#P8306 【模板】字典树 - 洛谷)
    • [P2580 于是他错误的点名开始了 - 洛谷](#P2580 于是他错误的点名开始了 - 洛谷)
    • [P10471 最大异或对 - 洛谷 01-Trie](#P10471 最大异或对 - 洛谷 01-Trie)
  • OJ汇总

字典 (trie) 树

Trie 树又叫字典树或前缀树,是一种能够快速插入和查询字符串的数据结构。它利用字符串的公共前缀,将字符串组织成一棵树形结构,从而大大提高了存储以及查找效率。

各个编程语言都有提供红黑树和哈希表,都能做到字符串插入和查询。学习字典树肯定是学习红黑树和哈希表做不到的事。

我们可以把字典树想象成一棵多叉树每一条边代表一个字符从根节点到某个节点的路径就代表了一个字符串 。通过字典树存储字符串可以节省存储字符串的空间。例如,要存储 "abc""abd""acde" 以及 "cd" 时,构建的字典树如下:

在字典树的每一个结点位置,额外维护一些信息时,就可以做到很多事情:

  • 查询某个单词是否出现过,并且出现几次。

    但若是查找前缀例如 "ac" 的话,会出现可以查到,实际字符串并不存在的情况,存储 "ac" 同样麻烦。

    所以每个结点需要创建一个 passend 变量,pass 标记当前节点一共经过了多少次,end 表示当前节点是多少个字符串的结尾。

  • 查询有多少个单词是以某个字符串为前缀。

  • 查询所有以某个前缀开头的单词。例如输入法中,输入拼音的时候,可以提示可能的单词。

字典树的功能有很多,这里列举了最常用的 3 种操作。

字典树的实现

实现一个能够查询单词出现次数 以及查询有多少个单词是以某个字符串为前缀的字典树,默认会是小写字母。

准备工作:

cpp 复制代码
using vi = vector<int>;
struct DictTreeNode { // 描述字典树的边和结点信息
    using cvi = const vi;
    using ci = const int;
    vi tree;
    int pss; // 当前节点一共经过了多少次
    int ed;  // 当前节点是多少个字符串的结尾
    DictTreeNode(cvi &_tree = vi(128, 0), int _pss = 0, int _ed = 0) {
        tree = _tree;
        pss = _pss;
        ed = _ed;
    }
    int &operator[](ci &aim) { // 字典树支持下标访问
        return tree[aim];
    }
    ci &operator[](ci &aim) const { // 字典树支持下标访问
        return tree[aim];
    }
};

这里 tree[i] 表示 i 号结点的孩子信息, tree[i][1] 表示 b 的路径信息,tree[i].pss 表示 有多少个字符串经过 i 号结点, tree[i].ed 表示多少个字符串以 tree[i] 结尾。

其余模板见洛谷的 P8306 字典树模板题。

P8306 【模板】字典树 - 洛谷

P8306 【模板】字典树 - 洛谷

字典树模板题,要求是能查找前缀。这是一个内存限制为 1 GB 的OJ,可以使用 128 个空位表示所有字符,然后利用整个字符的 ASCII 码进行查询。但字符串只包含大小写字母和数字,可以剔除前47个字符,节省内存。

这里为了字典树的完整性,就不节省内存了,只要内存足够,可适用绝大多数场景。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
struct DictTreeNode { // 描述字典树的边和结点信息
    using cvi = const vi;
    using ci = const int;
    vi tree;
    int pss; // 当前节点一共经过了多少次
    int ed;  // 当前节点是多少个字符串的结尾
    DictTreeNode(cvi &_tree = vi(128, 0), int _pss = 0, int _ed = 0) {
        tree = _tree;
        pss = _pss;
        ed = _ed;
    }
    int &operator[](ci &aim) { // 字典树支持下标访问
        return tree[aim];
    }
    ci &operator[](ci &aim) const { // 字典树支持下标访问
        return tree[aim];
    }
};
struct dict_tree {
    using Dtn = DictTreeNode;
    using Vdtn = vector<Dtn>;
    using cst = const string;
    Vdtn dt; // 字典树本体

    dict_tree(cst &st = "") {
        insert(st);
    }

    void insert(cst &st) {
        if (dt.empty())
            dt.push_back(Dtn());
        int cur = 0;
        dt[cur].pss++;
        for (size_t i = 0; i < st.size(); i++) {
            int path = st[i] - '\0';
            if (dt[cur][path] == 0) {
                dt[cur][path] = dt.size();
                dt.push_back(Dtn());
            }
            cur = dt[cur][path];
            dt[cur].pss++;
        }
        dt[cur].ed++;
    }

    int find(cst &st) const {
        int cur = 0;
        for (size_t i = 0; i < st.size(); i++) {
            if (dt[cur][st[i] - '\0'] == 0)
                return 0;
            cur = dt[cur][st[i] - '\0'];
            if (cur >= dt.size())
                return 0;
        }
        return dt[cur].ed;
    }

    int find_pre(cst &st) const {
        int cur = 0;
        for (size_t i = 0; i < st.size(); i++) {
            if (dt[cur][st[i] - '\0'] == 0)
                return 0;
            cur = dt[cur][st[i] - '\0'];
            if (cur >= dt.size())
                return 0;
        }
        return dt[cur].pss;
    }
};

void ac() {
    int n, q;
    dict_tree dt;
    string st;
    cin >> n >> q;
    for (int i = 0; i < n; i++) {
        cin >> st;
        dt.insert(st);
    }
    for (int i = 0; i < q; i++) {
        cin >> st;
        cout << dt.find_pre(st) << '\n';
    }
}

int main() {
    int T = 1;
    cin >> T;
    while (T--)
        ac();
    return 0;
}

P2580 于是他错误的点名开始了 - 洛谷

P2580 于是他错误的点名开始了 - 洛谷

这里要求查询加查重。可以修改字典树的实现,让字典树在查询时也能做到顺便插入一个字符串进去,减少标记重复的成本。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
struct DictTreeNode { // 描述字典树的边和结点信息
    using cvi = const vi;
    using ci = const int;
    vi tree;
    int pss; // 当前节点一共经过了多少次
    int ed;  // 当前节点是多少个字符串的结尾
    DictTreeNode(cvi &_tree = vi(26, 0), int _pss = 0, int _ed = 0) {
        tree = _tree;
        pss = _pss;
        ed = _ed;
    }
    int &operator[](ci &aim) { // 字典树支持下标访问
        return tree[aim];
    }
};
struct dict_tree {
    using Dtn = DictTreeNode;
    using Vdtn = vector<Dtn>;
    using cst = const string;
    Vdtn dt; // 字典树本体

    dict_tree(cst &st = "") {
        insert(st);
    }

    void insert(cst &st) {
        if (dt.empty())
            dt.push_back(Dtn());
        int cur = 0;
        dt[cur].pss++;
        for (size_t i = 0; i < st.size(); i++) {
            int path = st[i] - 'a';
            if (dt[cur][path] == 0) {
                dt[cur][path] = dt.size();
                dt.push_back(Dtn());
            }
            cur = dt[cur][path];
            dt[cur].pss++;
        }
        dt[cur].ed++;
    }

    int find(cst &st) {
        int cur = 0;
        for (size_t i = 0; i < st.size(); i++) {
            if (dt[cur][st[i] - 'a'] == 0)
                return 0;
            cur = dt[cur][st[i] - 'a'];
            if (cur >= dt.size())
                return 0;
        }
        return dt[cur].ed++; // 每查到一个字符串就插入一个
    }
};

int main() {
    int n, q;
    dict_tree dt;
    string st;
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> st;
        dt.insert(st);
    }
    cin >> q;
    for (int i = 0; i < q; i++) {
        cin >> st;
        int ans = dt.find(st);
        if (ans == 0)
            cout << "WRONG\n";
        else if (ans == 1) {
            cout << "OK\n";
        } else
            cout << "REPEAT\n";
    }
    return 0;
}

P10471 最大异或对 - 洛谷 01-Trie

P10471 最大异或对 The XOR Largest Pair - 洛谷

1472:【例题2】The XOR Largest Pair

有的资料对字典树分的特别细,例如这里的字典树叫 01-Trie 。

肯定不能两个循环去枚举,必定超时。

将每个数的二进制01序列存储在字典树中,然后进行贪心策略:从高到低找,对每个bit位,在所有存在的数中,尽可能去找和当前比特位不同的。然后根据查找的情况通过二进制操作将异或的结果更新 bit 位。这个算法的时间复杂度是 O ( n log ⁡ n ) \text{O}(n\log n) O(nlogn) ,系数 32 可忽略。

P10471 最大异或对 The XOR Largest Pair - 洛谷 参考程序如下。1472:【例题2】The XOR Largest Pair 因为题目给的运行内存只有 64 MB ,而 vector 和结构体因为对齐等原因,浪费很多内存,使得这个代码无法过。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
struct DictTreeNode { // 描述字典树的边和结点信息
    using cvi = const vi;
    using ci = const int;
    vi tree;
    int pss; // 当前节点一共经过了多少次
    int ed;  // 当前节点是多少个字符串的结尾
    DictTreeNode(cvi &_tree = vi(2, 0), int _pss = 0, int _ed = 0) {
        tree = _tree;
        pss = _pss;
        ed = _ed;
    }
    int &operator[](ci &aim) { // 字典树支持下标访问
        return tree[aim];
    }
    ci &operator[](ci &aim) const { // 字典树支持下标访问
        return tree[aim];
    }
};
struct dict_tree {
    using Dtn = DictTreeNode;
    using Vdtn = vector<Dtn>;
    using cst = const string;
    Vdtn dt; // 字典树本体

    dict_tree() {}

    void insert(int num) {
        if (dt.empty())
            dt.push_back(Dtn());
        int cur = 0;
        dt[cur].pss++;
        for (int i = 31; i >= 0; i--) {
            int path = (num >> i) & 1;
            if (dt[cur][path] == 0) {
                dt[cur][path] = dt.size();
                dt.push_back(Dtn());
            }
            cur = dt[cur][path];
            dt[cur].pss++;
        }
        dt[cur].ed++;
    }

    int find(int num) const {
        int cur = 0;
        int ans = 0;
        // 贪心策略:对每个bit位,在存在的数中,尽可能去找和当前比特位不同的
        for (int i = 31; i >= 0; i--) {             // 从第32位开始枚举
            if (dt[cur][((num >> i) & 1) ^ 1] == 0) // 找不到就继续往下
                cur = dt[cur][((num >> i) & 1)];
            else {
                ans |= (1 << i); // num的异或值
                cur = dt[cur][((num >> i) & 1) ^ 1];
            }
        }
        return ans;
    }
};

string calc(int x) { // 转换成二进制01序列
    string st;
    for (int i = 31; i >= 0; i--) {
        st += char(((x >> i) & 1) + '0');
    }
    return st;
}

int main() {
    int n, ans = 0;
    vi num;
    dict_tree dt;
    cin >> n;
    num.resize(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        cin >> num[i];
        dt.insert(num[i]);
    }
    for (int i = 1; i <= n; i++) {
        ans = max(ans, dt.find(num[i]));
    }
    cout << ans;
    return 0;
}

1472:【例题2】The XOR Largest Pair 建议是按照 C 语言的方式模拟实现,但字典树的核心插入、查找等逻辑保持不变。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 1e7 + 1000;
int tree[N][2], pss[N], ed[N], num[N / 100];
int idx, n, ans;

void insert(int x) {
    int cur = 0;
    for (int i = 31; i >= 0; i--) {
        int path = (x >> i) & 1;
        if (tree[cur][path] == 0)
            tree[cur][path] = ++idx;
        cur = tree[cur][path];
    }
}

int find(int x) {
    int cur = 0;
    int ans = 0;
    for (int i = 31; i >= 0; i--) {
        int path = (x >> i) & 1;
        if (tree[cur][path ^ 1] == 0)
            cur = tree[cur][path];
        else {
            ans |= (1 << i);
            cur = tree[cur][path ^ 1];
        }
    }
    return ans;
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> num[i];
        insert(num[i]);
    }
    for (int i = 1; i <= n; i++)
        ans = max(ans, find(num[i]));
    cout << ans;
    return 0;
}

OJ汇总

P8306 【模板】字典树 - 洛谷

P2580 于是他错误的点名开始了 - 洛谷

P10471 最大异或对 The XOR Largest Pair - 洛谷

1472:【例题2】The XOR Largest Pair

相关推荐
灰色小旋风2 小时前
力扣第11题C++盛最多水的容器
数据结构·算法·leetcode
一匹电信狗2 小时前
【LeetCode面试题17.04】消失的数字
c语言·开发语言·数据结构·c++·算法·leetcode·stl
j_xxx404_2 小时前
从 O(N) 到 O(log N):LCR 173 点名问题的五种解法与最优推导
开发语言·c++·算法
自信150413057592 小时前
数据结构之单链表OJ复盘
c语言·数据结构·算法
仰泳的熊猫2 小时前
题目2265:蓝桥杯2015年第六届真题-移动距离
开发语言·数据结构·c++·算法·蓝桥杯
十年编程老舅2 小时前
C++ 原子操作实战:实现无锁数据结构
linux·c++·c++11·原子操作·无锁队列
米码收割机2 小时前
【AI】OpenClaw问题排查
开发语言·数据库·c++·python
苦藤新鸡2 小时前
87.分割成两个等和数组 leetcode416
数据结构·算法·leetcode
Eward-an2 小时前
LeetCode 128. 最长连续序列(O(n)时间复杂度详解)
数据结构·算法·leetcode