Tags:字符串php
博主第一次這麼詳細地講解算法,強烈建議看看hihocoder上的講解
注意弄清楚每一個數組的確切含義html
構建一個自動機,使得一個字符串的全部子串都可以被表示出來
並且從根出發的任意一條合法路徑都是該串中的一個子串
後綴自動機是一個DAG(\(AC\)自動機建成了\(Trie\)圖)node
時間空間都是\(O(n)\)(空間要開兩倍)ios
\(len[i]\)表示\(i\)點表示的字符串集合中最長字符串的長度算法
從\(hihocoder\)上能夠知道,一個節點表示的\(Endpos\)(位置集合)對應的字符串的集合中的字符串必定是長度連續的,即有一個\(longest\)和一個\(shortest\)
這裏的\(len\)表示的是\(longest\),當\(shortest\)減去其第一個字符,必定會成爲另外一個節點的\(longest\),那麼\(i\)節點的\(shortest\)即是某節點的\(longest+1\)數組
\(fa[i]\)表示\(i\)在\(Parent\)樹上的父親spa
那麼上文所說的\(i\)所對應另外一個節點\(j\),使得\(shortest(i)=longest(j)+1\),在\(parent\)樹上的關係就是\(fa[i]=j\)
能夠得出,\(parent\)樹必定是樹的結構,並且根是\(1\)號節點,表示的是空串3d
圖中不少邊都沒有畫出來
加入一個字符\(c\)時,把圖分爲\(A,B\)兩部分,\(A\)表示這一段存在兒子\(C\),\(B\)表示不存在兒子\(C\)
咱們把右邊那個\(C\)叫做\(p\)點,把下面的叫\(x\)好啦調試
Situation 1code
不存在\(A\)部分,那麼沿着\(parent\)樹一直會跳到\(0\)節點
這個時候實際上是沒有出現過字符\(c\),咱們須要把\(B\)中全部節點的\(c\)兒子置爲\(p\)號點
其實就是添加了全部以\(c\)結尾的子串的路徑
因此\(p\)節點表明的\(Endpos\)對應的字符串的\(shortest\)爲\(1\),\(parent\)樹上父親指向根節點即\(1\)節點
Situation 2
令\(A\)中最末端節點編號爲\(f\),當\(longest(f)+1=shortest(x)=longest(x)\)時,\(x\)表示的\(Endpos\)對應的字符串集只有一個元素
考慮是什麼實際狀況使得\(x\)的字符集中只有一個元素:
因爲自動機是一個DAG,因此此狀況當且僅當從根出發只有一條路徑可以到達\(x\)點,因此從\(1\)出發到達在\(x\)點以前的點也只有一條路徑,而這條路徑若是有不相同的字符,那麼不行(好比\(aab\),咱們須要在自動機上表示子串\(b\),因此必定有一條直接往\(b\)走的路徑,而\(aaa\)就能夠說最後一個\(a\)只有一條路徑可以到達,由於加入第三個\(a\)產生三個子串\(a,aa,aaa\),前兩個已經可以被表示出來了)因此只有開頭一段連續相同的字符知足這種狀況
把\(p\)的\(parent\)樹上父親指向\(x\)
其實這個是和第三種狀況同樣的,由於這裏x的入度只爲1,若是把這個點複製了一邊那麼原來的點就沒用了
原本覺得爲了節省空間就這樣作,而後發現這樣會\(WA\),和\(SYC\)討論了兩天最終得出結果:
1.這個點被複制,那麼複製出來的點實際上是表示空串,在Parent樹上和後綴自動機上都沒有意義
2.本覺得這樣是對的:從原來的點的\(Parent\)父親連向複製的點,這樣能夠貢獻\(1\)的\(siz\),使得點被徹底複製,原來的點被徹底拋棄,可是調試一天發現原來被丟棄的點是有可能被跳\(Parent\)樹訪問到的(如圖),而後它的\(fa\)和各項數據會被魔改,使得\(siz\)貢獻不上去
因此第二種狀況就是第二種,和第三種有本質區別
Situation 3
把\(x\)節點複製一遍給\(y\),全部前面連續的一段本應該連向\(x\)的都連向\(y\)(也就是說在\(A\)前面可能還有連向\(x\)的邊),把\(x\)的兒子memcpy給\(y\),把\(x\)和\(p\)的\(parent\)父親連向\(y\),把\(y\)的\(len\)設置爲\(len[f]+1\)
(Update2018.8.28:已更正@SSerxhs)
好比\(aabab\)在加入第二個\(b\)時子串\(ab\)已經存在,因此\(ab\)的\(Endpos\)集合變大了,這樣原來第一個\(b\)表示\(aab,ab,b\),它們的\(Endpos\)都是\(3\),而如今只能表示\(aab\)的\(Endpos\)是\(3\),\(ab,b\)的\(Endpos\)是\(3,5\),因此須要一個新的點來維護,同時這樣操做也保證了若是在後面加入\(c\),只會增長\(c,bc,abc\)而不會增長\(aabc\)
\(len[y]=len[f]+1\):在後面接一個字符,因此\(longest\)直接加\(1\)
一個想了好久的問題:爲何只連前面第一段?
感性理解一下,其實前面必定只有連續的一段連向這個點,因此其實加這句是保證複雜度的
\(siz[i]\)表示\(i\)號點表明的\(Endpos\)集合大小,也能夠說是\(i\)號點字符串集合在整個串中的出現次數
siz[i]=k 表示i節點對應的Endpos的字符串集合出現了k次 for(int i=node;i>=1;i--) siz[fa[A[i]]]+=siz[A[i]]; //A[i]表示len數組第從大到小第i位的節點 由於parent樹上父親的全部字符串是全部兒子的全部字符串的後綴,因此全部兒子出現的地方父親必定會出現,那麼siz[i]+=siz[son[i]]
\(sum[i]\)表示後綴自動機上通過\(i\)點的子串數量
sum[i]=k 表示字符串中通過i號節點的本質不一樣的子串有多少個 for(int i=2;i<=node;i++) sum[i]=siz[i]=1; for(int i=node;i>=1;i--) for(int k=0;k<26;k++) if(ch[A[i]][k]) sum[A[i]]+=sum[ch[A[i]][k]]; 這樣至關於在每個本質不一樣的子串的結尾打上1的標記,而後sum[i]表示的就是DAG上拓撲序在i以後的點的數量
建議完成這題:[TJOI2015]弦論
這是個人題解
int fa[N],ch[N][26],len[N],siz[N]; int lst=1,node=1,l;//1爲根,表示空串 void Extend(int c) { /* 2+2+2+3行,那麼多while可是複雜度是O(n) */ int f=lst,p=++node;lst=p; len[p]=len[f]+1;siz[p]=1; /* f爲以c結尾的前綴的倒數第二個節點,p爲倒數第一個(新建) len[i] 表示i節點的longest,不用記錄shortest(概念在hihocoder後綴自動機1上講得十分詳細) siz[i]表示i節點所表明的endpos的集合元素大小,即所對應的字符串集的longest出現的次數 不用擔憂複製後點的siz,在parent樹上覆制後的點的siz是它全部兒子siz之和,比1多 */ while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f]; if(!f) {fa[p]=1;return;} /* 把前面的一段沒有c兒子的節點的c兒子指向p Situation 1 若是跳到最前面的根的時候,那麼把p的parent樹上的父親置爲1 */ int x=ch[f][c],y=++node; if(len[f]+1==len[x]) {fa[p]=x;node--;return;} /* x表示從p一直跳parent樹獲得的第一個有c兒子的節點的c兒子 Situation 2 若是節點x表示的endpos所對應的字符串集合只有一個字符串,那麼把p的parent樹父親設置爲x */ len[y]=len[f]+1; fa[y]=fa[x]; fa[x]=fa[p]=y; memcpy(ch[y],ch[x],sizeof(ch[y])); while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f]; /* Situation 3 不然把x點複製一遍(parent樹父親、兒子),同時len要更新 (注意len[x]!=len[f]+1,由於經過加點會使x父親改變) 而後把x點和p點的父親指向複製點y,再將前面全部本連x的點連向y */ }
桶排序
按照\(len\)桶排序以後也就是\(Parent\)樹的\(BFS\)序/自動機的拓撲序
因此按照相似\(SA\)的桶排序方法能夠以下將\(Parent\)樹從葉子到根/自動機反拓撲序用\(A[1]..A[node]\)表示出來
for(int i=1;i<=node;i++) t[len[i]]++; for(int i=1;i<=node;i++) t[i]+=t[i-1]; for(int i=1;i<=node;i++) A[t[len[i]]--]=i;
SAM上每一條合法路徑都是字符串的一個子串
經過拓撲序DP能夠用來求本質不一樣的子串的問題
SAM上跑匹配
\(T\)表示查詢串,\(p\)表示匹配到自動機上\(p\)號節點,\(tt\)表示當前匹配長度爲\(tt\)
一共分三步
\(Step 1\) 一直跳\(parent\)父親,直到根或者下一位能夠匹配爲止,這一步很像\(kmp\)的\(next\)和\(AC\)自動機的\(fail\)
\(Step 2\) 若是匹配得上更新\(p\)和\(tt\),不然重置\(p\)和\(tt\)
\(Step 3\) 匹配完成則累加答案
for(int i=1;i<=l;i++) { int c=T[i]-'a'; while(p!=1&&!ch[p][c]) p=fa[p],tt=len[p]; ch[p][c]?(p=ch[p][c],tt++):(p=1,tt=0); if(tt==l) Ans+=siz[p]; }
大多數題確定沒有那麼裸,這時須要魔改中間步驟,使得符合題意(Example)
廣義後綴自動機
專題太大請見連接
附模板題[HN省隊集訓6.25T3]string題解
須要知道的幾個性質:
1.\(SAM\)上每條合法路徑都是原串中的一種子串
2.一個點能夠表示一個字符串集合
3.從一個點跳\(Parent\)樹父親一直到根,這些字符串集合表示的是全部後綴
新增一個節點\(p\),至關於在全部後綴後再加上一個字符
理所固然要一直跳\(Parent\)樹添\(c\)兒子
1.直接跳到了根,把新增點的\(Parent\)樹父親置爲\(1\),\(return\)
2.若是到某步有個點有\(c\)兒子,並且該點只能表示一個字符串,那麼\(fa[p]=ch[f][c]\)由於知足了性質\(3\),你接着從\(p\)出發跳父親獲得的字符串集合是全部後綴
3.若是該點\(x\)表示的不僅一個字符串,那麼把\(x\)複製一遍給\(y\),這時\(y\)只表示一個字符串,至關於\(x\)表示的字符串集合中有一個串的\(Endpos\)和其他的不一樣了因此要把這個狀態剝離給\(y\),同時在\(Parent\)樹上父親指向\(x\)也要指向\(y\)
洛谷 P3804 【模板】後綴自動機
// luogu-judger-enable-o2 #include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #define ll long long using namespace std; const int N=2010000; char s[N]; int fa[N],ch[N][26],len[N],siz[N]; int lst=1,node=1,l,t[N],A[N]; ll ans; void Extend(int c) { /* 若是是廣義後綴自動機的話,最好加上第一句,雖然不加不會錯 這句的意思是要加入的這個點已經存在(Trie樹上的同一個點再次被訪問) 不加就至關把這個點做爲中轉站,可是又和Situation2那個錯誤的中轉站不一樣,這個沒有被parent父親指,因此不會被魔改 */ if(ch[lst][c]&&len[ch[lst][c]]==len[lst]+1) {lst=ch[lst][c];siz[lst]++;return;} /* 2+2+2+3行,那麼多while可是複雜度是O(n) */ int f=lst,p=++node;lst=p; len[p]=len[f]+1;siz[p]=1; /* f爲以c結尾的前綴的倒數第二個節點,p爲倒數第一個(新建) len[i] 表示i節點的longest,不用記錄shortest(概念在hihocoder後綴自動機1上講得十分詳細) siz[i]表示以i所表明的endpos的集合元素大小,即所對應的字符串集出現的次數 不用擔憂複製後的siz,在parent樹上覆制後的點的siz是它全部兒子siz之和,比1多 */ while(f&&!ch[f][c]) ch[f][c]=p,f=fa[f]; if(!f) {fa[p]=1;return;} /* 把前面的一段沒有c兒子的節點的c兒子指向p Situation 1 若是跳到最前面的根的時候,那麼把p的parent樹上的父親置爲1 */ int x=ch[f][c],y=++node; if(len[f]+1==len[x]) {fa[p]=x;node--;return;} /* x表示從p一直跳parent樹獲得的第一個有c兒子的節點的c兒子 Situation 2 若是節點x表示的endpos所對應的字符串集合只有一個字符串,那麼把p的parent樹父親設置爲x */ len[y]=len[f]+1; fa[y]=fa[x]; fa[x]=fa[p]=y; memcpy(ch[y],ch[x],sizeof(ch[y])); while(f&&ch[f][c]==x) ch[f][c]=y,f=fa[f]; /* Situation 3 不然把x點複製一遍(parent樹父親、兒子),同時len要更新 (注意len[x]!=len[f]+1,由於經過加點會使x父親改變) 而後把x點和p點的父親指向複製點y,再將前面全部本連x的點連向y */ } int main() { //Part 1 創建後綴自動機 scanf("%s",s); l=strlen(s); for(int i=l;i>=1;i--) s[i]=s[i-1];s[0]=0; for(int i=1;i<=l;i++) Extend(s[i]-'a'); //Part 2 按len從大到小排序(和SA好像啊)後計算答案 for(int i=1;i<=node;i++) t[len[i]]++; for(int i=1;i<=node;i++) t[i]+=t[i-1]; for(int i=1;i<=node;i++) A[t[len[i]]--]=i; for(int i=node;i>=1;i--) {//從小到大枚舉,實際上在模擬parent樹的DFS int now=A[i];siz[fa[now]]+=siz[now]; if(siz[now]>1) ans=max(ans,1ll*siz[now]*len[now]); } printf("%lld\n",ans); return 0; }