单词频率 / 单词
题目链接:ybt高效进阶2-5-2 / luogu P3966
题目大意
有 n 个单词,然后问你每个单词在这些单词中出现了多少次。
思路
那看到匹配单词,然后多个匹配多个,我们会想到 AC 自动机。
但是 AC 自动机是在多个单词在一个单词中出现了多少次,你总不能直接枚举过去。
那其实也好搞,你就在插入的时候无论插入了前面多少个,都给他加出现的次数。
这样子你每次 fail 边跑到这个点,就代表这个位置到前面一段距离一定是匹配的串。
那我们为了保险,用逆拓扑序求 fail 边树上前缀和,就可以得到答案。
输出的答案的位置你只要在插入单词的时候记录一下这个单词的末尾位置即可。
代码
#include<queue>
#include<cstdio>
#include<cstring>
using namespace std;
struct Trie {
int son[31], num, fail;
}tree[1000301];
int n, tot, dy[201];
int tp[1000301], tmp;
char c[1000001];
queue <int> q;
void insert(int op) {
//建 Trie 树
int size = strlen(c);
int now = 0;
for (int i = 0; i < size; i++) {
int nxt = c[i] - 'a';
if (!tree[now].son[nxt]) tree[now].son[nxt] = ++tot;
tree[now].num++;
now = tree[now].son[nxt];
}
tree[now].num++;
dy[op] = now;//记录答案的位置
}
void make_fail() {
//建 AC 自动机的 fail 边
for (int i = 0; i < 26; i++)
if (tree[0].son[i]) {
q.push(tree[0].son[i]);
tp[++tmp] = tree[0].son[i];
tree[tree[0].son[i]].fail = 0;
}
while (!q.empty()) {
int now = q.front();
q.pop();
for (int i = 0; i < 26; i++)
if (tree[now].son[i]) {
q.push(tree[now].son[i]);
tp[++tmp] = tree[now].son[i];
tree[tree[now].son[i]].fail = tree[tree[now].fail].son[i];
}
else tree[now].son[i] = tree[tree[now].fail].son[i];
}
}
void work_tp() {
//直接跑逆序拓扑
for (int i = tmp; i >= 1; i--) {
tree[tree[tp[i]].fail].num += tree[tp[i]].num;
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%s", &c);
insert(i);
}
make_fail();
work_tp();
for (int i = 1; i <= n; i++)
printf("%d\n", tree[dy[i]].num);
return 0;
}