zoukankan      html  css  js  c++  java
  • 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
    ",cnt[idx[i]]);
    111     return 0;
    112 }
    View Code

    2015/11/22

  • 相关阅读:
    salmon 报错:ESC[00mException : [rapidjson internal assertion failure: IsObject()] salmon quant was invoked improperly.
    报错:RSEM can not recognize reference sequence name chr1!(基因组的bam不能直接用rsem进行表达值计算)
    R: 使用tapply根据相同ID合并指定列
    linux:去除特定列为空格的行
    知乎一答:程序员为什么要关注管理
    如何掌握一门编程语言的运用
    谈谈程序员这个职业及前景
    Oracle学习笔记(2)--Centos 7 下11gR2部署
    用flask写一个简单的接口
    iptables命令详解
  • 原文地址:https://www.cnblogs.com/frank-c1/p/4985105.html
Copyright © 2011-2022 走看看