洛谷P3966 [TJOI2013]单词 单词 (ac自动机 fail树的应用)
洛谷P3966 [TJOI2013]单词 单词 (ac自动机 fail树的应用)
概述:
ac自动机的fail指针构建成了一颗树。如果fail[p]指向q。那么我们假设根到p的字符串为str, 根到q的字符串为ttr. 那么str的后缀就是ttr的前缀, 某字符串在自动机上出现了。
对于这题, 每个在每个单词插入字典树的时候一路加加,代表这个单词的这个前缀出现过。那么整个自动机就是我们的文章。还有些情况我们需要统计就是单词作为某些后缀出现的情况。cnt[ fail[x] ] += cnt[x] 这样我们按深度由深到浅计算贡献即可(避免重复计算)。同时我们发现整个贡献计算就是队列的逆顺序。所以我存到了一个栈中。
参考代码
ps:自动机风格是看着kuangbin的学的
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 7;
char buf[MAXN];
int pos[MAXN], n;
stack<int>st;
struct ACauto {
static const int MAXN = 1e6 + 7;
int fail[MAXN], ch[MAXN][30], cnt[MAXN], tot, root;
int new_node() {
for(int i = 0; i < 26; i++ ) {
ch[tot][i] = -1;
}
cnt[tot++] = 0;
return tot - 1;
}
void init() {
tot = 0;
root = new_node();
}
void insert(char *str, int len, int id) {
int now = root;
for(int i = 0; i < len; i++ ) {
if(ch[now][ str[i] - 'a' ] == -1) {
ch[now][ str[i] - 'a' ] = new_node();
}
now = ch[now][ str[i] - 'a' ];
cnt[now]++;
}
pos[id] = now;
}
void get_fail() {
queue<int>que;
fail[root] = root;
for(int i = 0; i < 26; i++ ) {
if(ch[root][i] == -1) {
ch[root][i] = root;
} else {
fail[ ch[root][i] ] = root;
que.push(ch[root][i]);
}
}
while(!que.empty()) {
int now = que.front();
que.pop();
st.push(now);
for(int i = 0; i < 26; i++ ) {
if(ch[now][i] == -1) {
ch[now][i] = ch[ fail[now] ][i];
} else {
fail[ ch[now][i] ] = ch[ fail[now] ][i];
que.push(ch[now][i]);
}
}
}
}
int query(char str[], int len) {
int now = root;
int res = 0;
for(int i = 0; i < len; i++ ) {
now = ch[now][ str[i] - 'a' ];
int pos = now;
while(pos != root) {
res += cnt[pos];
cnt[pos] = 0;
pos = fail[pos];
}
}
return res;
}
};
ACauto ac;
int main() {
scanf("%d", &n);
ac.init();
for(int i = 1; i <= n; i++ ) {
scanf("%s", buf);
ac.insert(buf, strlen(buf), i);
}
ac.get_fail();
while(!st.empty()) {
ac.cnt[ ac.fail[st.top()] ] += ac.cnt[ st.top() ];
st.pop();
}
for(int i = 1; i <= n; i++ ) {
printf("%d\n", ac.cnt[pos[i]]);
}
return 0;
}