后缀自动机的程序实现

    一个字符串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 10001
struct _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其实不用拷贝
}
//通过逐个插入字母建立SAM
void mkSAM(char ch){
	int sn = ch - 'a';

	//生成一个新节点
	_t* nn = Node + toUsed ++;
	mkNode(nn);
	//该节点的step是最后节点的step加1
	nn->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的SAM
		for(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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值