思路來源
https://www.luogu.com.cn/blog/Kesdiael3/hou-zhui-zi-dong-ji-yang-xie 首推這一篇
https://www.cnblogs.com/zjp-shadow/p/9218214.html 也看了這一篇,也不錯
http://hihocoder.com/problemset/problem/1441 hihocoder板子題
https://www.luogu.com.cn/problem/solution/P3804 洛谷板子題
前置知識
自動機的知識(編譯原理那一套,比如有限狀態自動機云云,知道能在其上沿邊轉移感覺就可)
AC自動機、Trie樹、後綴數組
死記硬背環節
證明和心得(後附)感覺用處不大,直接計入死記硬背和抄板子做題環節
後綴自動機(SAM),是一個最小的確定有限狀態自動機(DFA),接受且只接受S的後綴
parent樹和自動機節點共用,時間複雜度O(n),空間複雜度O(n)
parent(也稱fa,後綴鏈接link):當一個節點的串,在變爲其後綴,且endpos發生了擴大時,最長的那個後綴對應的節點
endpos(也稱right):每個節點對應的串的endpos集合是一致的,每個子串唯一出現在某個節點裏
len:一個節點內代表的串的最大長度
minlen:一個節點內代表的串的最小長度,由於a回跳到父親fa(a)的時候len(fa)+1=minlen(a),故一般不存
ch[0-26](後綴轉移transfer):自動機上對應字母的轉移
配合兩張圖食用,便於記憶這些概念
圖1:abcd的後綴自動機,
紅色括號是endpos集合,字母是自動機轉移
圖二:aaba的後綴自動機,
紅色數字是節點對應的最長子串的長度len,藍邊是其parent樹上的父親fa
板子
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct NODE
{
int ch[26];
int len,fa;
NODE(){memset(ch,0,sizeof(ch));len=0;}
}dian[N<<1];
int las=1,tot=1;
void add(int c)
{
int p=las;int np=las=++tot;
dian[np].len=dian[p].len+1;
for(;p&&!dian[p].ch[c];p=dian[p].fa)dian[p].ch[c]=np;
if(!p)dian[np].fa=1;//以上爲case 1
else
{
int q=dian[p].ch[c];
if(dian[q].len==dian[p].len+1)dian[np].fa=q;//以上爲case 2
else
{
int nq=++tot;dian[nq]=dian[q];
dian[nq].len=dian[p].len+1;
dian[q].fa=dian[np].fa=nq;
for(;p&&dian[p].ch[c]==q;p=dian[p].fa)dian[p].ch[c]=nq;//以上爲case 3
}
}
}
char s[N];int len;
int main()
{
scanf("%s",s);len=strlen(s);
for(int i=0;i<len;i++)add(s[i]-'a');
}
性質
①endpos:一個子串在原串中所有出現的地方,其末位置下標的集合
如串abcab,endpos(ab)={2,5}
②endpos相同,一個必爲另一個後綴,
考慮abcdbcd,當abcd變成後綴bcd的時候,endpos增加,bcd變成cd的時候,endpos不變
③根據②,任意兩個子串的endpos集合,要麼一個是另一個子集,要麼二者相交爲空,
只要有一個相交的位置,說明是後綴,後綴就是含於的情況
④根據②,在子串縮短爲其後綴的過程中,要麼endpos不變,要麼增加,把endpos相同的視爲一個等價類節點
在SAM中,一個子串只屬於一個節點,這個節點內的子串的endpos都相同
⑤endpos等價類(節點)個數爲O(n),
這個沒看懂曾經看懂過,可以參考原博客
⑥parent樹、自動機
後綴自動機的parent樹和自動機的節點是共用的,
parent樹往祖先跳的時候,endpos會變大,實際上是當前串變爲其後綴串,在節點中用fa定義
自動機就是讀進這個字符轉移到哪個點,定義了一種轉移關係,在節點中用ch[0-26]定義,類似trie
⑦由②,不難發現,endpos不變的一段子串是連續的,
分別記minlen和len爲這個點對應的子串的最小長度和最大長度
記點a覆蓋的串的長度爲[minlen(a),len(a)],則從minlen(a)再縮短一個長度的時候,endpos擴充,跳到父親
所以有len(fa(a))+1=minlen(a),fa(a)是a在parent樹上的父親
⑧後綴自動機的邊數爲O(n)
這個沒看懂曾經看懂過,可以參考原博客
⑨SAM的構造過程
設當前字母爲c,從上一個點las轉移過來,當前新開一個點np,endpos多出一個{n}來
先考慮轉移,爲了最大程度的利用節點,
只對las的祖先節點中,計當前祖先節點爲p,
若不存在字母c的轉移的點,對其進行轉移到np的操作,
並令p回跳到p的父親,最終p停留在了某個節點
以下分三種情況,實際上是(1)(2)兩種情況,其中(2)分爲兩種子情況
(1)c是沒出現過的字符,這樣回跳的時候一定會回到根,這裏計根爲1號節點
根代表了空串,其endpos集合爲{1,2,...,n},
因此,c所在節點np構成了一個新的endpos集合{n},直接令fa(np)=1
(2)停留在了中途某個點,此時點p存在字母c的轉移,所以停下了,
這其中分成兩種子情況,計q爲p通過字母c能轉移到的點,len[q]是q中最長串的長度
①若len[q]=len[p]+1,由於p是las的祖先,p的串一定是las串的後綴,
np比las多了一個字母c,q比p多了一個字母c,且q最長的串是p最長的串的長度+1,
這表明,在p的串後面補一個字母c,其仍然是 在las串後面補一個字母c 的後綴,
則q的串也是np串的後綴,q的endpos一定是np的endpos的超集,令fa(np)=q即可
②len[q]>len[p]+1,由於len[q]>=len[p]+1(是由p的串補了一個字母c而來),既然不等於,就一定大於
說明q中的串分成了兩部分,長度=len[p]+1的那些串x,由①,是np串的後綴,其endpos多出了{n}
長度>len[p]+1的那些串y,不是np串的後綴,其endpos沒有變化,
而q是np在往上回跳的過程中,第一個包含了np的所有endpos的集合的點,
既然不能表示,就考慮將q拆成兩部分,
一部分是x串集合構成的點,是新開的點,記爲nq,有len[nq]=len[p]+1
另一部分是y串集合構成的點,保持原來的q不變,
起先,令nq的所有自動機轉移關係(所有ch節點)等於原先的q,這樣做可行的原因是:
nq是q的後綴,後續在加入新的字符z時,設其跳到狀態nr,
由於nq和q只有碰到字母c時有區別,而z不等於c(設z=c,則nr是第一個包含了np的所有endpos的點,與nq是第一個包含了np的所有endpos集合的點矛盾),
故沒有受到新字母c的影響,所以二者唯一的區別endpos{n},沒有給後續的nr的狀態帶來影響
然後考慮fa的關係,nq是舊q的一部分,fa(nq)=fa(q),新q比nq長,可以令fa(q)=nq
構建這個nq節點的意義在於表示np,所以令fa(np)=nq
舊q的兒子(轉移關係ch)原封不動保留到了新q和nq中,考慮fa的改變
原先有一些節點指向q,但現在nq成爲了q和np的fa,這些點應該指向nq,
所以應從p往其祖先回跳,若其碰到字母c的轉移爲q,就應將其改爲nq,
對於沒有通過c轉移到q的那些祖先節點,其一定指向了nq的祖先,故不用修改
心得
感覺自己死摳知識點的證明,不做題,沒啥大的意義,
有些知識點感性理解一下就好了……
但總之思路來源的博主寫的很好,
滿足了我這個zz對於原理的一切幻想……
去年8月、11月自學過SAM的原理,但是後來沒做題,就漸漸忘了,
現在想想,SA前年當時花了四五天摳原理,但現在也只記得rank、sa、height數組是幹啥的了,
實際上,SAM的情況就分三種,代碼也很短,掌握了性質就可以了……
證明什麼的,除了O(n)的點數和邊數的證明略複雜以外,剩下的還好……
沒必要每次做題前都複習一下證明,更何況這玩意比SA做題直接很多……