AC自动机

AC自动机就是在Trie树上加入一些失败指针(fail,类似KMP中的next),使得它在某个结点失配的时候能够转移到该结点失败指针指向的结点继续匹配,从而实现多串匹配(单主串多子串)。

【1】Trie树的结构:
首先是结点类型:
struct  node {
    
int  mul, ch[SZ], fail;
} T[MAXN];
其中SZ是字符集的大小,比如小写字母集SZ=26,数字集SZ=10等。
另外这个mul表示的是该结点的重复次数(和平衡树中的比较像),就是这个结点所对应的字符串(从根到该结点路径上的所有边上的字符组成的字符串)出现了几次。
(不过这种表示法MS不是很好……如果有卡空间的,比如结点总数和SZ之积达到了100M以上,而真正的边又很少的时候……就囧掉了……而如果用指针的话在Linux下面又会CE掉……各位神犇来说一下怎么解决啊囧……)
然后,Trie树的0号结点(T[0])是空结点(用来代替指针中的NULL),因此真正结点下标从1开始。1号结点(T[1])一般都作为根结点,所以下面直接写#define root 1。
还有(这句话是除草用的……)Trie树的字符全部都在边上而不在点上,因此T[x].ch[c]其实指的是“结点x引出的字符为c的边所指向的结点”,简称结点x的c子结点。

【2】自动机的建立:
首先要建立Trie树(也就是在空的Trie树上插入所有要匹配的子串)。这个随便搞一下就傻掉了,因此直接上代码:
void  ins()
{
    
int  len  =  s0.length(), x  =  root, c;
    re(i, len) {
        c 
=  s0[i]  -   97 ;
        
if  ( ! T[x].ch[c]) {T[x].ch[c]  =   ++ N; T[N].mul  =   0 ; re(j, SZ) T[N].ch[j]  =   0 ;}
        x 
=  T[x].ch[c];
    }
    T[x].mul
++ ;
}
这里string s0是待插入的字符串,定义成全局变量,因为在参数中出现string有可能爆掉。

然后就是建立自动机了。
这一过程其实是用BFS遍历来实现的。首先,T[root].fail=0(root也是整棵树中唯一的fail为0的结点)并将root入队。下面按照BFS的顺序依次取出队所有的点,对于结点i,若它存在k子结点j(也就是j=T[i].ch[k],且j≠0),则结点j入队,并计算j的失败指针fail,方法:从T[i].fail(不是i)开始不断上溯,直到找到一个存在k子结点的结点或者到root都木有找到(结点下标为0,即NULL)。若找到了一个存在k子结点的结点x,则将T[j].fail置为T[x].ch[k],否则(直到root都木有找到),T[j].fail=root。

到这里失败指针的用处就显现了:如果匹配到结点x的时候失配(即接下来的一个字符是c而T[x].ch[c]=0),可以立刻转到T[x].fail进行匹配,因为T[x].fail有以下三个特征:
<1>其深度严格小于x的深度;
<2>其代表的字符串一定是x代表的字符串的后缀;
<3>该结点一定是满足条件<1><2>的所有结点中深度最小的结点;

代码:
void  mkf()
{
    Q[
0 =  root; T[root].fail  =   0 ;
    
int  i, j, x;
    
for  ( int  front = 0 , rear = 0 ; front <= rear; front ++ ) {
        i 
=  Q[front];
        re(k, SZ) 
if  (j  =  T[i].ch[k]) {
            x 
=  T[i].fail;
            
while  (x  &&   ! T[x].ch[k]) x  =  T[x].fail;
            
if  (x) T[j].fail  =  T[x].ch[k];  else  T[j].fail  =  root;
            Q[
++ rear]  =  j;
        }
    }
}

【3】匹配过程:
在有了失败指针时,其匹配过程就和KMP差不多了。
设主串为A(代码中同),依次扫描A[0..A.length()-1]进行匹配。设目前匹配到了第i位,A[i]=c,当前结点为x。匹配过程中将进行以下操作:
<1>成功匹配(T[x]有c子结点),则进入T[x]的c子结点;
<2>失配(T[x]无c子结点),则从T[x].fail开始,沿着失败指针上溯,直到找到一个有c子结点的结点为止。如果到了root都木有找到这样的结点,则停止在root处;
<3>设立结点y,从当前的结点x开始不断上溯到root(注意root也要算,因为子串中可能有空串),将中间所有结点的mul值累加进最终结果(表示这些字符串在主串中出现了,统计次数),如果题目中要求统计不重复的子串个数(如HDU2222),则在累加之后将它们全部置为0,防止下次再次累加。这一步操作实质上就是统计A中所有以A[i]为右端点的子串个数。

这样,整个过程就傻掉了。

代码:
void  solve()
{
    
int  len  =  A.length(), x  =  root, y, c; res  =   0 ;
    re(i, len) {
        c 
=  A[i]  -   97 ;
        
while  (x  &&   ! T[x].ch[c]) x  =  T[x].fail;
        
if  ( ! x) x  =  root;  else  x  =  T[x].ch[c];
        y 
=  x;
        
while  (y) {res  +=  T[y].mul; T[y].mul  =   0 ; y  =  T[y].fail;}
    }
}

有关例题:
【1】 HDU2222
裸的多串匹配问题,模板题;
有关该题的详细资料(包括易疵点)见 这里。
【2】 HDU2896
比2222稍难一些,但还是模板题。注意这题的子串木有重复的,因此mul可以设为bool。
【3】 HDU3065
统计每个子串出现的次数(可以重复),也是模板题。
以上三题均不卡空间。


Mato_No1 2011-10-19 19:03 发表评论

你可能感兴趣的:(ac,自动机)