zoukankan      html  css  js  c++  java
  • AC自动机

    简介:

      用于多模式串的在一个长文本串上的匹配问题。简单的说就是将KMP算法搬到了Trie树上。所以学习AC自动机前要由Trie树与KMP算法做前置知识。

    主要步骤:

      1、将所有模式串建成一个Trie树。

      2、对Trie树的每个节点都构造失配指针。

      AC自动机中失配指针(nxt数组)的运用与KMP算法几乎一模一样,即若能匹配下一位的话,当前的失配指针就向下转移一位;否则,当前的失配指针就要往回走一下,知道下一位能匹配上或失配指针不能再往回走为止。在实际应用中,对失配指针的额外操作会因题目的不同而有所差异。

    对于建立Trie树,可以看这篇博客。下面讲讲怎么建nxt数组。

      nxt[u]表示当主串s匹配到u节点时,设主串已经匹配到了第i位,若主串的下一位不能继续匹配,即u节点没有表示字符s[i+1]的边时,满足表示的字符串是u最长后缀的新的u节点(因为要保证u表示的字符串与s以第i位为结尾的长度为lu的子串匹配(lu为u节点表示的字符串的长度),因为一开始u就是最长的能匹配的某字符串的前缀,若想不漏掉所有情况地改变u且仍满足前缀与以s[i]结尾后缀的匹配关系,只能把u跳到是u表示的字符串的最长后缀的新的u)。

      对于nxt[u],设v一开始是u的父亲。看下v的失配指针对应的点nxt[v]是否有与从u的父亲到u的一样的边(即u的父亲和nxt[v]能否直接匹配下一位),若有,则nxt[u]就是nxt[v]通过那条边所到达的节点;否则将v变为nxt[v],再看下v的失配指针对应的点nxt[v]是否有与从u的父亲到u的一样的边……。为了方便,可以建一个节点0,节点0的表示每个字符的边都存在且指向u,那么v最差就是变为0,此时v一定会存在与从u的父亲到u的一样的边,且这条边指向节点1(表示空串),nxt[u]=1。

      实际写代码时,有个优化:如果当前的Trie树节点x表示某个字符num的边不存在,即tree[x][num]==0(建trie树时若某个节点的某条边的指针为0,就说明还没有进行赋值(因为建Trie树时节点从1开始计数;全局数组初始化默认为0)),就让tree[x][num]=tree[nxt[x][num]。即让它保存当v变为x时v仍没有从u的父亲到u的一样的边,v还要再等于nxt[v]…直到有从u的父亲到u的一样的边后返回的结果。这样的话求一个点y的儿子z的失配指针nxt[z]时,设从y到z的边为num,直接nxt[z]=tree[nxt[y]][num]就好了。

     1 queue<int> q;
     2 
     3 inline void bfs()//因为nxt[x]的深度一定比x的深度小,所以建nxt数组时可用bfs 
     4 {
     5     q.push(1);
     6     int head;
     7     while(!q.empty())
     8     {
     9         head=q.front();
    10         q.pop();
    11         for(int i=0;i<26;++i)
    12         {
    13             if(!tree[head][i])//如上文的优化 
    14                 tree[head][i]=tree[nxt[head]][i];
    15             else
    16             {
    17                 q.push(tree[head][i]);
    18                 nxt[tree[head][i]]=tree[nxt[head]][i];
    19             }
    20         }
    21     }
    22 }
    建立nxt数组的核心代码

      再讲一下查询。三种询问方式对应着三种查询(这也是AC自动机的应用)

    1、查询有多少模式串在文本串中出现过(例题):

     1 inline void insert(char *a)//建立Tire树。a为要插入的模式串 
     2 {
     3     int l=strlen(a+1),now=1,num;
     4     for(int i=1;i<=l;++i)
     5     {
     6         num=a[i]-'a';
     7         if(!tree[now][num])
     8         {
     9             tree[now][num]=++cnt;
    10             //memset(tree[cnt],0,sizeof tree[cnt]);//多组数据时要先清零 
    11         }
    12         now=tree[now][num];
    13     }
    14     ed[now]++;//*
    15 }
    16 
    17 inline void fin(char *a)//查询过程。a为文本串 
    18 {
    19     int l=strlen(a+1),now=1,num;
    20     for(int i=1;i<=l;++i)
    21     {
    22         num=a[i]-'a';
    23         now=tree[now][num];
    24         for(int k=now;k>1&&ed[k]!=-1;k=nxt[k])
    25             ans+=ed[k],ed[k]=-1;//** 
    26     }
    27 }
    代码实现

    每匹配到一个节点u,都要顺着从u开始的nxt指针看一下,防止漏掉长度短的字符串。

    讲一下标上**的那一行:对于匹配到的Trie树上的节点,查询的时候nxt数组肯定建完了。因为只要求是否出现的,那么若当前匹配到的Trie上的节点是u,那么下次再看到u时是不会对答案再产生变化了,故不看。

    时间复杂度O(n+m)(n为文本串的长度。m为**语句执行的次数,且最多不超过Trie树的节点数),碾压KMP。

    (蓝书中的实现代码的复杂度高达O(n*m),m为所有节点的平均深度。连洛谷模板题都跑不过,还是背上面的代码吧)

     2、查询模式串在文本串中出现过的次数(例题):

     1 inline void insert(char *a,int k)//要插入的字符串为a,它的编号为k 
     2 {
     3     int l=strlen(a+1),now=1,num;
     4     for(int i=1;i<=l;++i)
     5     {
     6         num=a[i]-'a';
     7         if(!tree[now][num])
     8             tree[now][num]=++cnt;
     9         now=tree[now][num];
    10     }
    11     ed[now].push_back(k);//可能有相同的字符串,也要记录。 
    12 }
    13 
    14 inline void fin(char *a)//a为要查询的文本串 
    15 {
    16     int l=strlen(a+1),now=1,num,k;
    17     for(int i=1;i<=l;++i)
    18     {
    19         num=a[i]-'a';
    20         now=tree[now][num];
    21         for(int k=now;k>=1;k=nxt[k])
    22         {
    23             if(ed[k].size())
    24             {
    25                 int ll=ed[k].size();
    26                 for(int j=0;j<ll;++j)
    27                     tot[ed[k][j]]++;
    28             }
    29         }
    30     }
    31 }
    代码实现

    这个没什么好说的,时间复杂度为O(n*m),m为所有节点的平均深度。如果用KMP做的话复杂度为O(n*k),k为模式串个数。这样看的话两种算法各有优劣,模式串个数多就用AC自动机,字符串都很长的话就用KMP。

    3、查询模式串在文本串中出现过的次数(强化版)(例题):

      1 #include<iostream>
      2 #include<cstdio>
      3 #include<cstring>
      4 #include<queue>
      5 
      6 using namespace std;
      7 
      8 const int LEN=2e5+5,L=2e6+5,N=2e5+5;
      9 
     10 int n,tree[LEN][26],cnt=1,nxt[LEN],tot[N],has[LEN],siz[LEN],lst[LEN],to[LEN],enxt[LEN],ecnt;
     11 
     12 vector<int> ed[LEN];
     13 
     14 char s[L];
     15 
     16 inline void insert(char *a,int k)
     17 {
     18     int l=strlen(a+1),now=1,num;
     19     for(int i=1;i<=l;++i)
     20     {
     21         num=a[i]-'a';
     22         if(!tree[now][num])
     23             tree[now][num]=++cnt;
     24         now=tree[now][num];
     25     }
     26     ed[now].push_back(k);
     27 }
     28 
     29 queue<int> q;
     30 
     31 inline void bfs()
     32 {
     33     q.push(1);
     34     int head;
     35     while(!q.empty())
     36     {
     37         head=q.front();
     38         q.pop();
     39         for(int i=0;i<26;++i)
     40         {
     41             if(!tree[head][i])
     42                 tree[head][i]=tree[nxt[head]][i];
     43             else
     44             {
     45                 q.push(tree[head][i]);
     46                 nxt[tree[head][i]]=tree[nxt[head]][i];
     47             }
     48         }
     49     }
     50 }
     51 
     52 inline void addedge(int u,int v)
     53 {
     54     enxt[++ecnt]=lst[u];
     55     lst[u]=ecnt;
     56     to[ecnt]=v;
     57 }
     58 
     59 void dfs(int u)
     60 {
     61     for(int e=lst[u];e;e=enxt[e])
     62     {
     63         dfs(to[e]);
     64         siz[u]+=siz[to[e]];
     65     }
     66     int k=ed[u].size();
     67     for(int i=0;i<k;++i)
     68         tot[ed[u][i]]=siz[u];
     69 }
     70 
     71 inline void fin(char *a)
     72 {
     73     int l=strlen(a+1),now=1,num,k;
     74     for(int i=1;i<=l;++i)
     75     {
     76         num=a[i]-'a';
     77         now=tree[now][num];
     78         siz[now]++;
     79     }
     80 }
     81 
     82 int main()
     83 {
     84     for(int i=0;i<26;++i)
     85         tree[0][i]=1;
     86     scanf("%d",&n);
     87     for(int i=1;i<=n;++i)
     88     {
     89         scanf("%s",s+1);
     90         insert(s,i);
     91     }
     92     bfs();
     93     for(int i=1;i<=cnt;++i)
     94         addedge(nxt[i],i);
     95     scanf("%s",s+1);
     96     fin(s);
     97     dfs(1);
     98     for(int i=1;i<=n;++i)
     99         printf("%d
    ",tot[i]);
    100     return 0;
    101 } 
    完整代码

    强化版更适合看完整的代码。插入和建Trie树与原版一样。多了一个求子树和的dfs,fin查询函数更简单了。

    我们发现原版的复杂度真是不尽人意,堂堂的省选知识AC自动机竟和普及组就学的KMP五五开,这怎么行?强化版用了一个建nxt树的思路。考虑每个节点的贡献来源,要么是当前节点被匹配到一次,使多了一个贡献,要么是某个节点被匹配到一次,通过nxt恰好能走到当前节点,于是又让当前节点的贡献加1。对于每个Trie树上的节点,发现它有且只有一个nxt(不考虑节点0)。那么从一个只有Trie树上的节点(不包含节点0)的新图上,将每个点的nxt向相应的点连一条边,一定会生成一个根为1的树。若每个点的权值为它被匹配到的次数的话,发现每个点对答案的贡献正是以它为根的子树的和。故可以通过建nxt树、最后求子树和的方式快速求出每个串的出现次数。

    (想不出来就要换的角度嘛。一个个找相应串加不行的话,看看贡献的来源与去向,搞个整体收集不就行了?)

    时间复杂度O(n+m),m为Trie树的节点个数(又一次碾压了KMP,看来AC自动机要处处碾压KMP了)

  • 相关阅读:
    asp.net url传参中使用javascript过滤中文乱码
    Javascript 操作select控件大全(新增、修改、删除、选中、清空、判断存在等)【转】
    element.firstChild
    NHibernate使用Criteria分页显示并返回记录总数 【转】
    动态生成select选项全接触Javascript技术【转】
    struts中ApplicationResources.properties支持中文
    博客园博客美化方法大全[附源码]
    fileinput模块
    python学习
    遗忘的果实
  • 原文地址:https://www.cnblogs.com/InductiveSorting-QYF/p/11811163.html
Copyright © 2011-2022 走看看