AC自动机的fail数组
我们知道KMP算法就是两个字符串在进行模式匹配,而AC自动机则是一堆字符串和一个字符串进行的模式匹配。由于一堆字符串比较多,我们需要使用trie树来存储这些字符串。
AC自动机的 f a i l fail fail数组,对于节点 x x x而言, f a i l x fail_x failx表示以 x x x结尾的后缀,和以根节点开头的前缀,最大的公共部分长度是多少。可KMP一样,我们可以使用递推的方法来进行求解。
对于当前的节点 u u u,以及在 t r i e trie trie数上的子节点 v v v而言:
- 如果存在子节点为 v v v,可以将 u u u的 f a i l fail fail的子节点与v进行匹配,即令 v v v和 c [ f a i l u ] [ v ] c[fail_u][v] c[failu][v]进行匹配。此时我们思考 w = c [ f a i l u ] [ v ] w=c[fail_u][v] w=c[failu][v]的取值,如果存在,那肯定能匹配上;如果不存在,则我们可以让它指向一个 c [ f a i l w ] [ v ] c[fail_w][v] c[failw][v],则这样保证了不断跳fail一定匹配上或者指向根节点了。
- 不存在,直接令 c [ u ] [ v ] = c [ f a i l u ] [ v ] . c[u][v]=c[fail_u][v]. c[u][v]=c[failu][v].理由在上面已经提到。
具体的实现,我们需要利用到广度优先遍历。
代码如下:
void build(string s,int num)
{
int t = 0, len = s.size();
for (int i=0;i<len;++i)
{
if (!c[t][s[i]-'a'])
c[t][s[i]-'a'] = ++Cnt;
t = c[t][s[i]-'a'];
}
return;
}
//在trie数中插入字符串
void work(void)
{
queue <int> q;
for (int i=0;i<26;++i)
if (c[0][i]) {
q.push(c[0][i]);
fail[c[0][i]] = 0;
}
while (q.size())
{
int t = q.front(); q.pop();
for (int i=0;i<26;++i) {
if (c[t][i]) fail[c[t][i]] = c[fail[t]][i], q.push(c[t][i]);
else c[t][i] = c[fail[t]][i];
}
}
return;
}
//广度优先遍历求解fail数组
模板1
给定 n n n个模式串和 1 1 1个文本串,求有多少个模式串在文本串里出现过。
我们每一次在插入操作结束以后用一个 e n d end end记录当前节点有几个结尾的。
在跳 f a i l fail fail的时候,每一次 f a i fai fail只能跳一遍,因为问的是由多少个模板串出现过。
注意跳 f a i l fail fail的实现方式。
#include <bits/stdc++.h>
using namespace std;
const int N = 2000000;
int n, cnt = 0;
int c[N][26], end[N], fail[N];
void build(string s)
{
int t = 0 ,len = s.size();
for (int i=0;i<len;++i)
{
if (c[t][s[i]-'a'] == 0)
c[t][s[i]-'a'] = ++cnt;
t = c[t][s[i]-'a'];
}
end[t] ++;
return;
}
void work(void)
{
queue <int> q;
for (int i=0;i<26;++i)
if (c[0][i] > 0)
q.push(c[0][i]), fail[c[0][i]] = 0;
while (q.size())
{
int t = q.front(); q.pop();
for (int i=0;i<26;++i) {
if (c[t][i] > 0)
q.push(c[t][i]), fail[c[t][i]] = c[fail[t]][i];
else c[t][i] = c[fail[t]][i];
}
}
return;
}
int ask(string s)
{
int now = 0, ans = 0, len = s.size();
for (int i=0;i<len;++i)
{
now = c[now][s[i]-'a'];
for (int t = now; t > 0 && end[t] ^ -1; t = fail[t])
ans += end[t], end[t] = -1;
}
return ans;
}
int main(void)
{
cin>>n;
for (int i=1;i<=n;++i) {
string S; cin>>S;
build(S);
}
work();
string S; cin>>S;
cout<<ask(S)<<endl;
return 0;
}
模板2
我们每一次统计每一个数字的出现次数,在统计这个东东的时候不用将 e n d end end赋值为 1 1 1,因为统计的是出现次数而不是由多少个字符串。
我们用 e n d end end记录字符串标号,在跳 f a i l fail fail的使用用 c n t [ e n d [ t ] ] + + cnt[end[t]]++ cnt[end[t]]++即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 20000;
int n, Cnt = 0;
string s[N];
int c[N][26], fail[N], end[N], cnt[N];
void Clear(void)
{
n = Cnt = 0;
memset(c,0,sizeof c);
memset(end,0,sizeof end);
memset(cnt,0,sizeof cnt);
memset(fail,0,sizeof fail);
}
void build(string s,int num)
{
int t = 0, len = s.size();
for (int i=0;i<len;++i)
{
if (!c[t][s[i]-'a'])
c[t][s[i]-'a'] = ++Cnt;
t = c[t][s[i]-'a'];
}
end[t] = num;
return;
}
void work(void)
{
queue <int> q;
for (int i=0;i<26;++i)
if (c[0][i]) {
q.push(c[0][i]);
fail[c[0][i]] = 0;
}
while (q.size())
{
int t = q.front(); q.pop();
for (int i=0;i<26;++i) {
if (c[t][i]) fail[c[t][i]] = c[fail[t]][i], q.push(c[t][i]);
else c[t][i] = c[fail[t]][i];
}
}
return;
}
void ask(string s)
{
int now = 0, len = s.size();
for (int i=0;i<len;++i)
{
now = c[now][s[i]-'a'];
for (int t=now;t;t=fail[t])
cnt[end[t]] ++;
}
return;
}
void get_ans(void)
{
int Max = 0;
for (int i=1;i<=n;++i)
Max = max(Max,cnt[i]);
cout<<Max<<endl;
for (int i=1;i<=n;++i)
if (cnt[i] == Max)
cout<<s[i]<<endl;
return;
}
void solve(void)
{
cin>>n;
if (n == 0) exit(0);
for (int i=1;i<=n;++i) cin>>s[i];
for (int i=1;i<=n;++i) build(s[i],i);
work();
cin>>s[0]; ask(s[0]);
get_ans();
return;
}
int main(void)
{
while (1) Clear(), solve();
return 0;
}