後綴自動機學習筆記

後綴自動機(SAM)

抱歉,圖牀掛了,博主並無存圖,待修改,暫留坑

Tags:字符串php

做業部落

評論地址


1、SAM詳解

博主第一次這麼詳細地講解算法,強烈建議看看hihocoder上的講解
注意弄清楚每一個數組的確切含義html

一、幹嗎用

構建一個自動機,使得一個字符串的全部子串都可以被表示出來
並且從根出發的任意一條合法路徑都是該串中的一個子串
後綴自動機是一個DAG\(AC\)自動機建成了\(Trie\)圖)node

二、複雜度

時間空間都是\(O(n)\)(空間要開兩倍)ios

三、\(len\)

\(len[i]\)表示\(i\)點表示的字符串集合中最長字符串的長度算法

\(hihocoder\)上能夠知道,一個節點表示的\(Endpos\)(位置集合)對應的字符串的集合中的字符串必定是長度連續的,即有一個\(longest\)和一個\(shortest\)
這裏的\(len\)表示的是\(longest\),當\(shortest\)減去其第一個字符,必定會成爲另外一個節點的\(longest\),那麼\(i\)節點的\(shortest\)即是某節點的\(longest+1\)數組

四、\(Parent\)

\(fa[i]\)表示\(i\)\(Parent\)樹上的父親spa

那麼上文所說的\(i\)所對應另外一個節點\(j\),使得\(shortest(i)=longest(j)+1\),在\(parent\)樹上的關係就是\(fa[i]=j\)
能夠得出,\(parent\)樹必定是樹的結構,並且根是\(1\)號節點,表示的是空串3d

五、添加一個字符

CjoDPS.png
圖中不少邊都沒有畫出來
加入一個字符\(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\)貢獻不上去PKUSC買了個草稿本~

因此第二種狀況就是第二種,和第三種有本質區別

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/sum

\(siz[i]\)表示\(i\)號點表明的\(Endpos\)集合大小,也能夠說是\(i\)號點字符串集合在整個串中的出現次數

  • 從parent樹上累加
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
     */
}

八、構建圖示

\(aab\)

CvyQiR.png

\(aaba\)

CvylJ1.png

\(aabab\)

Cvy3z6.png

九、性質及應用

桶排序

按照\(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\)

2、題單

  • [x] [hihocoder1441]後綴自動機一·基本概念 http://hihocoder.com/problemset/problem/1441
  • [x] [hihocoder1445]後綴自動機二·重複旋律5 http://hihocoder.com/problemset/problem/1445
  • [x] [hihocoder1449]後綴自動機三·重複旋律6 http://hihocoder.com/problemset/problem/1449
  • [x] [hihocoder1457]後綴自動機四·重複旋律7 http://hihocoder.com/problemset/problem/1457
  • [x] [hihocoder1465]後綴自動機五·重複旋律8 http://hihocoder.com/problemset/problem/1465
  • [ ] [HihoCoder1413]Rikka with String
  • [x] [Luogu3804]【模板】後綴自動機 https://www.luogu.org/problemnew/show/P3804
  • [ ] [BZOJ4516][SDOI2016]生成魔咒
  • [x] [BZOJ3998][TJOI2015]弦論 https://www.luogu.org/problemnew/show/P3975
  • [x] [BZOJ3277]串 https://www.lydsy.com/JudgeOnline/problem.php?id=3277
  • [ ] [BZOJ3926][ZJOI2015]諸神眷顧的幻想鄉
  • [x] [BZOJ2806][CTSC2012]熟悉的文章(Cheat) https://www.luogu.org/problemnew/show/P4022
  • [ ] [BZOJ1396&2865]識別子串
  • [ ] [HEOI2015]最短不公共子串 https://www.luogu.org/problemnew/show/P4112
  • [ ] [BZOJ2555]SubString
  • [ ] [BZOJ5137][Usaco2017 Dec]Standing Out from the Herd
  • [x] [BZOJ2780][SPOJ8093]Sevenk Love Oimaster https://www.luogu.org/problemnew/show/SP8093
  • [ ] [CF700E]Cool Slogans https://www.luogu.org/problemnew/show/CF700E
  • [ ] [CF666E]Forensic Examination
  • [x] [BZOJ5408][HN省隊集訓6.25T3]string https://www.lydsy.com/JudgeOnline/problem.php?id=5408

3、一句話題解

4、模板

洛谷 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;
}
相關文章
相關標籤/搜索