CLJ大神的后缀自动机,异常的强大,只不过也不太好理解。
只能勉强把构造过程弄懂,其中很多性质还不是特别明白……
char s[maxn]; //待构造字符串
int sz; //状态个数
struct state
{
state *pre; //前继结点,表示最近的添完上一字符后的终止状态
state *go[sigma]; //表示当前结点的出边
int val; //表示当前结点的最长路径长度
void clear() //初始化函数
{
pre=0;val=0;
memset(go,0,sizeof(go));
}
};
state *root,*last; //根结点和上一个添加的结点
state st[maxn];
void init() //初始化SAM
{
sz=0;
root=last=&st[sz++];
root->clear();
}
void extend(int w) //扩展结点
{
state *p=last; //上一个添加的结点
state *np=&st[sz++]; //新建结点
np->clear();
np->val=p->val+1;
while(p && p->go[w]==0) //对于没有到该字符的祖先结点添加到当前结点的边
{
p->go[w]=np;
p=p->pre;
}
if(p==0) np->pre=root; //若该字符第一次出现,添加到根结点的边
else
{
state *q=p->go[w];
if(q->val==p->val+1) //q为合法状态
np->pre=q;
else
{
state *nq=&st[sz++];//新建结点并将q的信息复制
nq->clear();
nq->val=p->val+1;
memcpy(nq->go,q->go,sizeof(q->go));
nq->pre=q->pre;
q->pre=nq; //将q,np的前继结点设为nq
np->pre=nq;
while(p && p->go[w]==q) //将指向q的p的祖先结点改为指向nq
{
p->go[w]=nq;
p=p->pre;
}
}
}
last=np;
}
要更好的利用后缀自动机,往往需要对其结点进行一遍拓扑排序,为了保持O(n)的复杂度,可以对结点按val值进行一遍桶排。
//t,r为辅助数组
int t[maxn];
state *r[maxn];
//l为字符串长度,sz为结点总数
//注意这里包含了root结点,root结点在r中的下标为1
for(int i=0;i<l;i++) t[i]=0;
for(int i=0;i<sz;i++) t[st[i].val]++;
for(int i=1;i<l;i++) t[i]+=t[i-1];
for(int i=0;i<sz;i++) r[t[st[i].val]--]=&st[i];