AC自动机和Fail树

一直对AC自动机有种执念,觉得有了它就能自动AC,是一种特别神奇的算法,今天终于见识了。

实际上,学习了AC自动机以后,才发现并没有那么玄乎。

一、Trie(字典树/前缀树)

学习AC自动机前,需要学会字典树。字典树,顾名思义当然是一棵树。特殊地,这棵树上的边是字符。那么对于任意一条从起点到某一结点的路径,都对应唯一的字符串。这样我们就可以用字典树上的结点来表示字符串。

如图,路径0-1经过“a",那么结点1就对应字符串“a";路径0-1-2-3依次经过“a","b","c",那么结点3就对应字符串"abc";路径0-1-4-5依次经过“a","a","a",那么结点5就对应字符串"aaa"……

从中我们可以发现,某一结点对应的字符串其实就是从结点0到该结点路径上的字符顺次连接而成的串。

在字典树上我们可以方便地实现字符串的插入、查找。

对于树上的结点,我们可以保存一些附加信息。如果我们规定结点的权值,用非零值表示单词结点,我们就可以用一棵字典树表示出一个由字符串组成的集合,并完成一些有用的工作,比如词频统计、前缀匹配等。

 1 struct Trie {
 2     static const int maxn=(int)(1e5)+5;
 3     static const int sigma_size=26;
 4     int ch[maxn][sigma_size]; //儿子结点
 5     int val[maxn]; //结点权值
 6     int siz;
 7     Trie():siz(0) {
 8         memset(ch,0,sizeof(ch));
 9         memset(val,0,sizeof(val));
10     }
11     int ord(char c) {
12         return c-'a';
13     }
14     //插入
15     void insert(char* s,int v) {
16         int u=0,n=strlen(s);
17         for(int i=0; i<n; i++) {
18             int c=ord(s[i]);
19             if(!ch[u][c]) ch[u][c]=++siz;
20             u=ch[u][c];
21         }
22         val[u]=v;
23     }
24     //查找
25     int find(char* s) {
26         int u=0,n=strlen(s);
27         for(int i=0; i<n; i++) {
28             int c=ord(s[i]);
29             if(!ch[u][c]) return 0;
30             u=ch[u][c];
31         }
32         return val[u];
33     }
34 };
View Code

二、KMP算法(单模式匹配)

KMP算法是一个单模式匹配算法,由三个发现者D.E.Knuth,J.H.Morris和V.R.Pratt的名字命名。

如果真的要用一篇文章来叙述KMP算法的思想,估计10多页的论文都说不完。这真是一个十分优美而又简洁的算法,虽然有点难于理解,但若是深入了解它的工作过程,一定会惊叹于它的精妙绝伦。

朴素算法进行单模式匹配,最坏情况下复杂度是O(nm);而KMP算法是O(n+m)。是什么大大加速了匹配过程?核心之处在于失配指针。朴素的方法之所以复杂度较高,在于它做了许多没有必要的比较。试想一下,如果模板串“abbababb"已经匹配到“abbabab"了,只有最后一个字符不一样,那我们要全部重新来过吗?

不用,既然已经匹配了“abbabab",那么前面两个字符一定是“ab",只要继续匹配模板串的第三个字符就好了。

如图是失配时的状态转移图,其中虚线箭头就是所谓的失配指针。当匹配某一字符失败时,只要转移到失配指针指向处继续匹配即可。具体的算法过程不再赘述,附上代码一份:

 1 struct KMP {
 2     static const int maxn=(int)(1e5)+5;
 3     int f[maxn]; //失配指针
 4     bool ok[maxn]; //是否成功匹配
 5     int n,m;
 6     //计算失配指针
 7     void getFail(char* P) {
 8         f[0]=0;
 9         for(int i=1,j=0; i<m; i++) {
10             while(j && P[i]!=P[j]) j=f[j-1];
11             if(P[i]==P[j]) j++;
12             f[i]=j;
13         }
14     }
15     //单模式匹配
16     void Find(char* T,char* P) {
17         n=strlen(T);
18         m=strlen(P);
19         getFail(P);
20         for(int i=0,j=0; i<n; i++) {
21             while(j && T[i]!=P[j]) j=f[j-1];
22             if(T[i]==P[j]) j++;
23             if(j==m) ok[i]=true;
24         }
25     }
26 };
View Code

三、AC自动机(多模式匹配)

 观察下图,如果我们把虚线箭头全部去掉,其实就是第一部分中的Trie;而那些虚线箭头,貌似是第二部分中的失配指针?

Trie+失配指针,就基本上组成了传说中的AC自动机。AC自动机,由其发现者Aho Corasick的名字命名,用于解决多模式匹配的问题。

可以说,AC自动机就是KMP算法以Trie为基础的实现。其中失配指针的作用也和KMP算法中的一样,写法也类似。唯一有些不同的是,在单模式匹配中,由于只有一个模板串,成功匹配也就是一个串;但是多模式匹配时,有可能在成功匹配一个模板串时,也同时成功匹配了另一个模板串。这个问题可以用后缀链接进行处理。

 1 struct AC_AutoMaton {
 2     static const int maxn=(int)(1e5)+5;
 3     static const int sigma_size=26;
 4     int ch[maxn][sigma_size];
 5     int f[maxn],last[maxn];
 6     int cnt[maxn],val[maxn];
 7     int siz;
 8 
 9     AC_AutoMaton():siz(0) {
10         memset(ch,0,sizeof(ch));
11         memset(val,0,sizeof(val));
12     }
13 
14     int ord(char c) {
15         return c-'a';
16     }
17     //插入 
18     void insert(char* s,int v) {
19         int u=0,n=strlen(s);
20         for(int i=0; i<n; i++) {
21             int c=ord(s[i]);
22             if(!ch[u][c]) ch[u][c]=++siz;
23             u=ch[u][c];
24         }
25         val[u]=v;
26     }
27     //计算失配指针 
28     int getFail() {
29         queue<int> Q;
30         f[0]=0;
31         for(int c=0; c<sigma_size; c++) {
32             int u=ch[0][c];
33             if(u) {
34                 f[u]=last[u]=0;
35                 Q.push(u);
36             }
37         }
38         while(!Q.empty()) {
39             int k=Q.front();
40             Q.pop();
41             for(int c=0; c<sigma_size; c++) {
42                 int u=ch[k][c];
43                 if(!u) continue;
44                 Q.push(u);
45                 int v=f[k];
46                 while(v && !ch[v][c]) v=f[v];
47                 f[u]=ch[v][c];
48                 last[u]=val[f[u]] ? f[u] : last[f[u]];
49             }
50         }
51     }
52     //统计 
53     void add(int u) {
54         for(; u; u=last[u]) cnt[val[u]]++;
55     }
56     //多模式匹配 
57     void Find(char* s) {
58         int u=0,n=strlen(s);
59         memset(cnt,0,sizeof(cnt));
60         for(int i=0; i<n; i++) {
61             int c=ord(s[i]);
62             while(u && !ch[u][c]) u=f[u];
63             u=ch[u][c];
64             if(val[u]) add(u);
65             else if(last[u]) add(last[u]);
66         }
67     }
68 };
View Code

四、优化:Trie图

注意到,在AC自动机匹配时,每一步可能有多次回溯(直到找到一个可以继续匹配的结点)。那么我们是否可以在之前就完成一些工作,使每步只需要转移一次呢?

我们引入Trie图的概念。Trie图是一个确定性有限状态自动机(DFA),比起AC自动机,增加了确定性的属性。即在任一位置,输入任一字符集中的字符,都可以转移到一个确定的结点。

实现方法很简单,就是把getFail()中的 if(!u) continue; 改成 if(!u) {ch[k][c]=ch[f[k]][c];continue;} ,然后就可以把Find()中 while(u && !ch[u][c]) u=f[u]; 删去。实质上就是通过添加有向边,使所有的转移统一化。这样做的直接好处就是在AC自动机上动规时转移比较方便。

五、拓展:Fail树

例1[TJOI2013] 单词 http://www.lydsy.com/JudgeOnline/problem.php?id=3172

刚学会AC自动机时,我很兴奋,马上到网上找了一道题来做(就是这道题)。当我兴奋地打完、上交,然后就TLE了……QAQ

这个题的朴素算法就是建立所有串的AC自动机,然后每个串上去跑一遍,然后统计答案。当然是TLE的。正解是用Fail树,利用树的性质来减少重复的计算。

首先根据AC自动机中失配指针的定义,每一个结点有且仅有一个失配指针指向另一结点,即所有结点出度为1。而对于某一结点u,它一定是由它的前趋结点或者失配指针指向u的结点转移过来的。那么考虑把所有的失配指针反向,然后把反向后的失配指针当作边,我们就得到了一棵以结点0为根的树(因为所有结点入度为1)。这样利用树的性质,我们dfs一遍就可以求出答案了。

解法:设结点i出现在cnt[i]个单词中,插入单词时所有经过的结点cnt[u]++,构造出Fail树后dfs一遍,答案是单词末结点子树的cnt值总和。代码如下:

  1 #include <set>
  2 #include <map>
  3 #include <queue>
  4 #include <ctime>
  5 #include <cmath>
  6 #include <cstdio>
  7 #include <vector>
  8 #include <string>
  9 #include <cctype>
 10 #include <bitset>
 11 #include <cstring>
 12 #include <cstdlib>
 13 #include <utility>
 14 #include <iostream>
 15 #include <algorithm>
 16 #define lowbit(x) (x)&(-x)
 17 #define REP(i,a,b) for(int i=(a);i<=(b);i++)
 18 #define PER(i,a,b) for(int i=(a);i>=(b);i--)
 19 #define RVC(i,S) for(int i=0;i<(S).size();i++)
 20 using namespace std;
 21 typedef long long LL;
 22 typedef pair<int,int> pii;
 23 
 24 template<class T> inline
 25 void read(T& num) {
 26     bool start=false,neg=false;
 27     char c;
 28     num=0;
 29     while((c=getchar())!=EOF) {
 30         if(c=='-') start=neg=true;
 31         else if(c>='0' && c<='9') {
 32             start=true;
 33             num=num*10+c-'0';
 34         } else if(start) break;
 35     }
 36     if(neg) num=-num;
 37 }
 38 /*============ Header Template ============*/
 39 
 40 const int maxn=(int)(1e6)+5;
 41 const int sigma_size=26;
 42 vector<int> G[maxn];
 43 queue<int> Q;
 44 int ch[maxn][sigma_size];
 45 int f[maxn];
 46 int cnt[maxn],idx[205];
 47 int siz;
 48 
 49 int ord(char c) {
 50     return c-'a';
 51 }
 52 int insert(char* s) {
 53     int u=0,n=strlen(s);
 54     for(int i=0; i<n; i++) {
 55         int c=ord(s[i]);
 56         if(!ch[u][c]) ch[u][c]=++siz;
 57         u=ch[u][c];
 58         cnt[u]++;
 59     }
 60     return u;
 61 }
 62 
 63 void getFail() {
 64     REP(i,0,siz) G[i].clear();
 65     f[0]=0;
 66     for(int c=0; c<sigma_size; c++) {
 67         int u=ch[0][c];
 68         if(!u) continue;
 69         f[u]=0;
 70         G[0].push_back(u);
 71         Q.push(u);
 72     }
 73     while(!Q.empty()) {
 74         int k=Q.front();
 75         Q.pop();
 76         for(int c=0; c<sigma_size; c++) {
 77             int u=ch[k][c];
 78             if(!u) continue;
 79             Q.push(u);
 80             int v=f[k];
 81             while(v && !ch[v][c]) v=f[v];
 82             f[u]=ch[v][c];
 83             G[f[u]].push_back(u);
 84         }
 85     }
 86 }
 87 
 88 void dfs(int u,int fa) {
 89     RVC(i,G[u]) {
 90         int v=G[u][i];
 91         if(v==fa) continue;
 92         dfs(v,u);
 93         cnt[u]+=cnt[v];
 94     }
 95 }
 96 
 97 char buf[maxn];
 98 int main() {
 99     siz=0;
100     memset(ch,0,sizeof(ch));
101     memset(cnt,0,sizeof(cnt));
102     int n;
103     read(n);
104     REP(i,1,n) {
105         scanf("%s",buf);
106         idx[i]=insert(buf);
107     }
108     getFail();
109     dfs(0,-1);
110     REP(i,1,n) printf("%d\n",cnt[idx[i]]);
111     return 0;
112 }
View Code

2015/11/22

 

posted @ 2015-11-22 14:42  frank_c1  阅读(827)  评论(0编辑  收藏  举报