• 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了)

  • 相关阅读:
    spring cloud 和 阿里微服务spring cloud Alibaba
    为WPF中的ContentControl设置背景色
    java RSA 解密
    java OA系统 自定义表单 流程审批 电子印章 手写文字识别 电子签名 即时通讯
    Hystrix 配置参数全解析
    spring cloud 2020 gateway 报错503
    Spring Boot 配置 Quartz 定时任务
    Mybatis 整合 ehcache缓存
    Springboot 整合阿里数据库连接池 druid
    java OA系统 自定义表单 流程审批 电子印章 手写文字识别 电子签名 即时通讯
  • 原文地址:https://www.cnblogs.com/InductiveSorting-QYF/p/11811163.html
Copyright © 2020-2023  润新知