后缀自动机的程序实现

来源:互联网 发布:linux 根据端口号查询 编辑:程序博客网 时间:2024/05/10 19:37

    一个字符串T的后缀自动机SAM(T)是指这样一个确定的有穷自动机,该自动机接受且仅接受T的所有后缀,包括ε;而且该自动机是最小状态的。
    给定任意字符串T,求其SAM有2个关键。其一,SAM(T)并不是直接生成的,而是需要依次求出其所有前缀的SAM。换一种说法,如果要求SAM(Tx),其中T是一个字符串,x是一个字母,那么必须首先求出SAM(T),然后在其基础上通过添加新的节点和边才能求出SAM(Tx)。其二,如何通过SAM(T)求出SAM(Tx),其关键是理解pre链(有些资料或者代码命名为father就混淆了其实际涵义,当然命名为pre也没有明确多少)。
    在一个SAM上,所有终态都会有一个pre指针依次指向其前一个终态,构成一个pre链。这里又有2点,第一,所谓前一个是指插入的顺序,因为SAM的所有节点都是依次插入的,所以必然能够分辨出前一个、前二个、……。第二,非终态也存在pre链,因为任何一个SAM都是依次建成的,所以任何一个节点必定会在之前的某个特定时刻作为终态存在,此时该节点就有pre指针,也一定会位于某个pre链之上。
    所以对于任何T的SAM(T),其是一个有穷自动机,且每个状态都会有一个pre指针。为了实现,我们还需为自动机的每个节点添加一个数据域称之为step,该域实际上记录了该节点插入到自动机的顺序。
    为了求出SAM(Tx),首先新建一个节点命名为nn,然后从SAM(T)的最后插入的那个节点开始,遍历其pre链,直到链上的某个节点有x儿子或者到达了初态为止(所有pre链必然结束于初态)。遍历的同时,将所有节点的x儿子指向nn。
    然后根据判断的情况,分为3种可能性:
1、整个pre链都没有x儿子,则将nn的pre指向初态,这是第一种情况。
    如果沿着pre链找到了一个节点p,该节点有x儿子,则令这个x儿子节点为q,则有第2、3种可能性。
2、如果q.step==p.step+1,也就是q恰好是在p后面插入的,则将nn的pre指向q。
3、如果q.step > p.step+1,此种情况下,必然存在这样一类节点称之为w类,w类节点经过x也能到达q,且w不在当前pre链上;与之对比的是p节点代表另外一类节点,p类节点通过x也能到达q,但是p类节点全部都在当前pre链上。此时需要将q节点一分为二,一个专门针对w类,一个专门针对p类。所以新建一个节点命名为nq,其入边就是p类入边,而原q节点只保留w类入边,同时n的pre指向nq,q的pre指向nq,nq的pre指向p,且nq.step = p.step + 1。
    根据这3种情况依次插入节点,设置数据域的值,即可建立起SAM。最开始的SAM只有一个状态,既是初态也是终态,对应空字符串ε。该流程的相关理论分析可以参考clj的论文。
    下列源代码是POJ1509求字符串的最小表示,此处使用SAM完成。当然这道题本身用循环同构的最小表示要比用SAM简单一些。

//典型的最小表示,此处用SAM完成//求出SAM(TT),在自动机上走length(T)步为结果//每次都走最小的儿子#include <cstdio>#include <algorithm>using namespace std;#define  SIZE 10001struct _t{_t* son[26];_t* pre;     //指向上一个可接收的int step;    //插入的秩序}Node[SIZE*2];int toUsed = 0;_t * const Init = Node;//初态始终指向Node[0],实际上就用Node_t * Last = Node; //最后状态在初始时为Node//初始化一个新节点inline void mkNode(_t* newNode){newNode->pre = NULL;newNode->step = 0;fill(newNode->son,newNode->son+26,(_t*)0);}//拷贝构造一个新节点inline void mkNode(_t const*src,_t*dest){dest->pre = src->pre;copy(src->son,src->son+26,dest->son);dest->step = src->step;//step其实不用拷贝}//通过逐个插入字母建立SAMvoid mkSAM(char ch){int sn = ch - 'a';//生成一个新节点_t* nn = Node + toUsed ++;mkNode(nn);//该节点的step是最后节点的step加1nn->step = Last->step + 1;//从最后节点遍历可接收链_t* p = Last;Last = nn;//更新最后节点while( p && NULL == p->son[sn] ){p->son[sn] = nn;p = p->pre;}        //第1种情况if ( NULL == p ){nn->pre = Node;return;}//找到了一个节点具有ch的出边_t* q = p->son[sn];        //第2种情况if ( p->step + 1 == q->step ){nn->pre = q;return;}        //第3种情况//新建一个节点,复制q_t* nq = Node + toUsed ++;mkNode(q,nq);nq->step = p->step + 1;q->pre = nn->pre = nq;while( p && q == p->son[sn] ){p->son[sn] = nq;p = p->pre;}return;}//初始化全局变量inline void init(){mkNode(Node);toUsed=1;Last = Node;}char A[SIZE];int main(){int nofkase;scanf("%d",&nofkase);while(nofkase--){init();scanf("%s",A);int len = 0;//输入字符串为A,实际上要构造AA的SAMfor(char*p=A;*p;++p,++len){mkSAM(*p);}for(char*p=A;*p;++p){mkSAM(*p);}//找到AA中长度为len的最小子串_t* loc = Node;for(int i=0;i<len;++i){for(int j=0;j<26;++j){if ( loc->son[j] ){loc = loc->son[j];break;}}}//看看这个字母是第几个插入的printf("%d\n",loc->step+1-len);}return 0;}


0 0