AC自动机

Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。

解决什么问题呢?

KMP是给你一个模式串和一个文本串,要求求出模式串的匹配位置。

而AC自动机是给你一个文本串和一堆模式串,问你能匹配上多少模式串。

容易想到我们可以在Trie上跑KMP,结合二者的优点就是我们的AC自动机了。

想学AC自动机,先要明白Trie树的实现方法和KMP的思想。

AC自动机大概就长这个样,它是在字典树的基础上多了个失配指针,告诉你失配了该往哪里跑,就相当于KMP中的next数组。

比如我们在K处失配

显然next记录的位置是在k的父亲的next下的s[k]的编号

我们的next是按照从根节点开始的BFS序求解,就是这样。

 

 1 void get_next()
 2 {
 3     for(int i=0;i<26;i++)
 4     {
 5         if(trie[0][i])
 6         {
 7             next[trie[0][i]]=0;
 8             q.push(trie[0][i]);
 9             //因为按照BFS序找next嘛,可以理解为什么按BFS序吧 
10         }
11     }//先预处理深度为1的点的next,显然他父亲没有与i相同的兄弟,所以直接赋0 
12     while(!q.empty())
13     {
14         int cnt=q.front(); q.pop();//取队首,找他儿子的next 
15         for(int i=0;i<26;i++)
16         {
17             if(trie[cnt][i])
18             {
19                 next[trie[cnt][i]]=trie[next[cnt]][i];//显然的转移 
20                 q.push(trie[cnt][i]);
21             }
22             else
23             trie[cnt][i]=trie[next[cnt]][i];//既然不存在,我们查找它时就应该找到上面那个i的位置
24             //由于是按BFS序找的,所以 trie[next[cnt]][i]一定找过 
25         }
26     }
27 }

这就是next的求法。

 对了,忘了说了,建AC自动机的方法就是建一颗Trie:

 1 void _insert(string s)
 2 {
 3     int now=0;
 4     int size=s.size();
 5     for(int i=0;i<size;i++)
 6     {
 7         int x=s[i]-'a';
 8         if(!trie[now][x])
 9         trie[now][x]=++total;
10         now=trie[now][x];
11     }
12     _word[now]++;//我们令_word记录以now结尾有几个单词,用于解题的。 
13 }

如果看官实在看不懂鄙人丑陋的码风,可以转开始的链接-Trie

 

好,接下来就是查找了:

给我们一个文本串S,问在AC自动机上有多少单词出现在了S上(多模式串匹配)

我们的_word还是很有用的,就是我们每找到一个结尾k,那么就可以保证结尾为k的所有单词都被访问过一遍,所以我们的答案加上_word[k],之后为避免重复,将_word[k]清零。

查找嘛,正常情况就按照查找字典树的办法查找,失配的话就跳到next,同时我们要保证你找到的一定是单词结尾(显然若不存在,程序结束)。

 1 int find(string s)
 2 {
 3     int size=s.size();
 4     int now=0,ans=0;
 5     for(int i=0;i<size;i++)
 6     {
 7         int x=s[i]-'a';
 8         now=trie[now][x];
 9         for(int k=now;k&&_word[k];k=next[k])
10         {
11             ans+=_word[k];
12             _word[k]=0;
13         }
14     }
15     return ans;
16 }

于是就结束了。

//洛谷3808

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<queue>
using namespace std;
int n,trie[1000010][30],next[1000010],total,_word[10000010];
queue<int>q;
void _insert(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
        int x=s[i]-'a';
        if(!trie[now][x])
        trie[now][x]=++total;
        now=trie[now][x];
    }
    _word[now]++;
}
void get_next()
{
    for(int i=0;i<26;i++)
    {
        if(trie[0][i])
        {
            next[trie[0][i]]=0;
            q.push(trie[0][i]);
        }
    }
    while(!q.empty())
    {
        int cnt=q.front(); q.pop();
        for(int i=0;i<26;i++)
        {
            if(trie[cnt][i])
            {
                next[trie[cnt][i]]=trie[next[cnt]][i];
                q.push(trie[cnt][i]);
            }
            else
            trie[cnt][i]=trie[next[cnt]][i];
        }
    }
}
int find(string s)
{
    int size=s.size();
    int now=0,ans=0;
    for(int i=0;i<size;i++)
    {
        int x=s[i]-'a';
        now=trie[now][x];
        for(int k=now;k&&-(_word[k]+1);k=next[k])//“k&&_word[k]”走到头再找next ,不卡时间过不去 
        {
            ans+=_word[k];
            _word[k]=-1;
        }
    }
    return ans;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        string s;
        cin>>s;
        _insert(s);
    }
    get_next();
    string s;
    cin>>s;
    cout<<find(s);
}

进阶版

不同的是,他让你求哪个串出现的次数最多,而且可能有许多串出现的次数一样多。

因为输入的特殊性,我们在输入时赋予每个串的结尾一个映射belong[i],记录这个串是第几个输入进去的(显然纵然有重复模式串也不影响答案)

然后对于每次查找,我们另数组_time[k]表示k这个字符串出现了几次,在查找时每找到一个点k,我们令_time[belong[k]]++.

最后顺序统计答案。

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<algorithm>
 4 #include<map>
 5 #include<string>
 6 #include<queue>
 7 #include<cstring>
 8 using namespace std;
 9 int trie[100010][30],next[100010],n,t,total,_time[100010],belong[100010];
10 queue<int>q;
11 string s[100010];
12 void _insert(string s1,int j)
13 {
14     int size=s1.size(),now=0;
15     for(int i=0;i<size;i++)
16     {
17         int x=s1[i]-'a';
18         if(!trie[now][x])
19         trie[now][x]=++total;
20         now=trie[now][x];
21     }
22     belong[now]=j;
23 }
24 void get_next()
25 {
26     for(int i=0;i<26;i++)
27     if(trie[0][i])
28     q.push(trie[0][i]);
29     while(!q.empty())
30     {
31         int cnt=q.front(); q.pop();
32         for(int i=0;i<26;i++)
33         {
34             if(trie[cnt][i])
35             {
36                 next[trie[cnt][i]]=trie[next[cnt]][i];
37                 q.push(trie[cnt][i]);
38             }
39             else
40             trie[cnt][i]=trie[next[cnt]][i];
41         }
42     }
43 }
44 int find(string s1)
45 {
46     int size=s1.size();
47     int now=0,ans=0;
48     for(int i=0;i<size;i++)
49     {
50         int x=s1[i]-'a';
51         now=trie[now][x];
52         for(int k=now;k;k=next[k])
53         _time[belong[k]]++;            
54     }
55     for(int i=1;i<=n;i++)
56     ans=max(ans,_time[i]);
57     return ans;
58 }
59 int main()
60 {
61     while(scanf("%d",&n)!=EOF)
62     {
63         if(n==0)break;
64         memset(next,0,sizeof(next));
65         memset(_time,0,sizeof(_time));
66         memset(trie,0,sizeof(trie));
67         memset(belong,0,sizeof(belong));
68         total=0;
69         for(int i=1;i<=n;i++)
70         {
71             cin>>s[i];
72             _insert(s[i],i);    
73         }
74         get_next();
75         string s1;
76         cin>>s1;
77         int num=find(s1);
78         printf("%d\n",num);
79         for(int i=1;i<=n;i++)
80         {
81             if(_time[i]==num)
82             cout<<s[i]<<endl;
83         }
84     }    
85 }

AC自动机是一种优秀的多模式串匹配算法,应用广泛,希望大家能深入理解。

posted @ 2018-05-10 20:38  _ZZH  阅读(506)  评论(0编辑  收藏  举报