把所有模式串插入字典树,并在每个前缀节点记录经过它的模式串数量;查询串走到终点后的计数就是答案。
OJ: luogu
题目 ID: P8306
难度:普及/提高-
标签:字典树字符串模板题
日期: 2026-06-21 01:26
题意
给出若干模式串,再给出若干查询串。
对每个查询串 t,要求统计有多少个模式串 s 满足:
t是s的前缀
思路
先看一个可以直接验证想法的朴素解:
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:
- 全部查询:
空间复杂度:
总结
这题是前缀统计类 Trie 模板。
和“判断字符串是否存在”不同,这里必须在每个经过节点上累计计数,而不是只在结尾节点打标记。