续了一个晚上终于搞懂后缀自动机的建法了。。
首先,SAM的线性复杂度构建方法是将所建字符串的字符从左至右一个个插入,设插入后自动机有n个点,那么插入会产生到1~n、2~n、3~n直至n~n这些点(都是由i~n-1转移来),而在插入过程中要分成三种情况讨论。
首先因为最长长度n的点之前不可能存在,所以必然要先开一个新点(np)。再从之前一个插入的点(记为p),沿着fa边往回跳,边回跳边将没有连向np字符的点连上,如果跳到的点已经存在了转移到np这个字符的边break-->2.情况 。。不然,如果直到跳完也不存在上述2情况则-->1情况
1.该情况说明插入的字符是当前自动机里没有的字符
显然,因为插入一个没有的字符那么在它之前不可能存在其他该字符的endpos,所以它的fa必然只能连到根,故从之前一个插入的字符位置沿着fa边一个个向该字符位置连边即可。
2.说明插入的字符是已有的
这种情况需再分两种。
设该点为p,沿np相同字符所达点为t
首先跳到p存在走到np相同字符的边那么肯定不能再连np否则不满足自动机结构,而因为是后缀自动机,那么这个点之前的点显然也已经存在走到t的边,这时候如果p的最长字符长度+1等于t的最长长度,那么说明t及之前的状态不会因加入np改变,将np连fa边到t就行。还有一种情况则是p最长字符+1!=t最长长度,那么很复杂,t里的一些状态会因为np的到来改变,这时就要新建一个点,将t中改变的状态连到这个新点nt,而nt这个点的转移状态因与t相同,所以memcpy复制过来,之后再将nt的fa连向当前t的fa,np的fa和t的fa都指向nt,再将p中通过np相同字符转移到t的连向nt,并重复更新p之前的点即可。
复杂度证明的话空间显然每次最多开两个点是O(n)的,时间嘛。。反正有大佬证明了是均摊O(1)的,总的也是O(n)。
代码:
struct Sam{
int nxt[N][26],mx[N],fa[N],las,tot;
Sam(){las=tot=1;}
void ins(int x)
{
int np,p;
p=las,np=++tot,las=np;
mx[np]=mx[p]+1;
while(p&&!nxt[p][x])nxt[p][x]=np,p=fa[p];
if(!p){fa[np]=1;return;}
int t=nxt[p][x];
if(mx[t]==mx[p]+1)fa[np]=t;
else
{
int nt=++tot;
mx[nt]=mx[p]+1;
memcpy(nxt[nt],nxt[t],sizeof nxt[t]);
fa[nt]=fa[t],fa[t]=fa[np]=nt;
for(;nxt[p][x]==t;p=fa[p])nxt[p][x]=nt;
}
}
//int cal(){return mx[las]-mx[fa[las]];}
}sam;