【模板】字典树 / Trie

GitHub跳转原题关系图返回列表

把所有模式串插入字典树,并在每个前缀节点记录经过它的模式串数量;查询串走到终点后的计数就是答案。

OJ: luogu

题目 ID: P8306

难度:普及/提高-

标签:字典树字符串模板题

日期: 2026-06-21 01:26

题意

给出若干模式串,再给出若干查询串。

对每个查询串 t,要求统计有多少个模式串 s 满足:

  • ts 的前缀

思路

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

// brute.cpp:直接保存所有模式串。
// 每次查询时暴力统计有多少个模式串以查询串作为前缀。

bool is_prefix(const string &pre, const string &s) {
    if ((int) pre.size() > (int) s.size()) {
        return false;
    }
    for (int i = 0; i < (int) pre.size(); i++) {
        if (pre[i] != s[i]) {
            return false;
        }
    }
    return true;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;

    while (T--) {
        int n, q;
        cin >> n >> q;

        vector<string> a(n);
        for (int i = 0; i < n; i++) {
            cin >> a[i];
        }

        for (int i = 1; i <= q; i++) {
            string s;
            cin >> s;
            int ans = 0;
            for (int j = 0; j < n; j++) {
                if (is_prefix(s, a[j])) {
                    ans++;
                }
            }
            cout << ans << '\n';
        }
    }

    return 0;
}

brute.cpp 对每个查询串暴力检查全部模式串,看有多少个模式串以它开头。

这个方法只适合很小的数据。

更好的做法是使用字典树。

把每个模式串插入 Trie 时,沿着路径前进,并让每个经过节点的 cnt 加一。

这样 cnt[u] 表示:

  • 有多少个模式串拥有“根到 u 这段路径对应的前缀”

那么查询某个字符串 t 时:

  • 如果 Trie 里不存在这条路径,答案是 0
  • 如果路径存在,走到终点节点后的 cnt 就是答案

这题还有一个实现细节:

  • 字符集大小是 62
  • 所有字符串总长度可达 3 * 10^6

如果直接开 trie[node][62],内存压力较大。

所以这里采用稀疏实现:每个节点的出边用链式邻接表保存,只为真正存在的字符转移开边。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXNODE = 3200005;

int head[MAXNODE];       // head[u] 表示节点 u 的第一条出边
int to[MAXNODE];         // 边的终点
int nxt[MAXNODE];        // 邻接表下一条边
unsigned char ch[MAXNODE]; // 这条边对应哪个字符编号
int cnt[MAXNODE];        // 经过这个节点的模式串数量

int node_cnt = 0;
int edge_cnt = 0;

int new_node() {
    ++node_cnt;
    return node_cnt;
}

// 把字符映射到 0..61
int trans(char c) {
    if (c >= '0' && c <= '9') {
        return c - '0';
    }
    if (c >= 'A' && c <= 'Z') {
        return c - 'A' + 10;
    }
    return c - 'a' + 36;
}

// 在节点 u 的所有出边里寻找字符 code 对应的儿子。
int find_child(int u, int code) {
    for (int i = head[u]; i != 0; i = nxt[i]) {
        if ((int) ch[i] == code) {
            return to[i];
        }
    }
    return 0;
}

int add_child(int u, int code) {
    int v = new_node();
    ++edge_cnt;
    to[edge_cnt] = v;
    ch[edge_cnt] = (unsigned char) code;
    nxt[edge_cnt] = head[u];
    head[u] = edge_cnt;
    return v;
}

void insert_string(int root, const string &s) {
    int p = root;
    for (int i = 0; i < (int) s.size(); i++) {
        int code = trans(s[i]);
        int v = find_child(p, code);
        if (v == 0) {
            v = add_child(p, code);
        }
        p = v;
        cnt[p]++; // 经过这个节点的字符串数量,也就是这个前缀的出现次数
    }
}

int query_prefix_count(int root, const string &s) {
    int p = root;
    for (int i = 0; i < (int) s.size(); i++) {
        int code = trans(s[i]);
        p = find_child(p, code);
        if (p == 0) {
            return 0;
        }
    }
    return cnt[p];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;

    while (T--) {
        int n, q;
        cin >> n >> q;

        int root = new_node();

        for (int i = 1; i <= n; i++) {
            string s;
            cin >> s;
            insert_string(root, s);
        }

        for (int i = 1; i <= q; i++) {
            string s;
            cin >> s;
            cout << query_prefix_count(root, s) << '\n';
        }
    }

    return 0;
}

复杂度

设所有字符串总长度为 L

  • 建 Trie:O(L)O(L)
  • 全部查询:O(L)O(L)

空间复杂度:

O(L)O(L)

总结

这题是前缀统计类 Trie 模板。

和“判断字符串是否存在”不同,这里必须在每个经过节点上累计计数,而不是只在结尾节点打标记。