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\n",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了)

posted @ 2019-11-07 15:20  千叶繁华  阅读(172)  评论(0)    收藏  举报