日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

[学习笔记]后缀自动机

發布時間:2023/12/20 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [学习笔记]后缀自动机 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

解決大部分字符串問題的大殺器

給一下clj課件:戳我

SAM是一個博大精深的算法。雖然沒有像網絡流家族,Tarjan家族一樣拉幫結派,但是自身包含很多方法。

?

一、前言

字符串常見問題:各種匹配

1.LCP

2.子串匹配

3.模式串和主串匹配

4.回文串?也算匹配吧。。。

?

經典例子:

給兩個串,求最長公共子串長度。

用hash表+二分,可以做到nlogn

?

后綴自動機可以做到O(n)

?

二、概念

我們需要一個可以識別某個串是否為某個主串的子串、處理主串的子串信息、串和串之間進行匹配的問題。

樸素的做法是,對每個子串建立一個節點,然后連邊,O(n^2)

?

考慮的優化是,發現一些子串會出現很多次,一些子串出現所有的出現位置的集合是同一個集合!

對這個進行優化

1.Right集合

對于某個串s,所有出現的位置的集合是:{r1,r2,...,rm}

那么,這個集合就是Right(s)

由于許多的串si,可能Right集合都是Right(s)

如果我們給定集合Right(t)

再給定屬于這個集合的字符串的長度len

隨便選擇一個r,那么這個字符串就已經唯一確定了。

一個Right集合會包含若干個字符串

那么,其len的范圍一定是一個區間[max,min]

?

那么,為了處理方便,我們會把這些串壓縮在一起。

保留這個Right集合的max

?

我們就會定義Right(s)為一個“狀態”

這個“狀態”,包含了所有的可能匹配到的節點末尾的位置ri,以及可能已經匹配出來的字符串集合si

?

*性質:兩個Right集合要么不相交,要么一個是另一個的真子集

證明:

首先易證,一個字符串屬于且僅屬于一個Right集合。

設兩個集合s1,s2相交,r是交集的某個元素

那么對于一個屬于s1的字符串string1

一個屬于s2的字符串string2

由于兩個字符串都可以以r作為末尾出現一次,由于一個字符串屬于且僅屬于一個集合

所以,string1!=string2

不妨設len(string1)=l1<len(string2)=l2

那么,顯然地,string2出現的位置,string1必然也會出現。

所以,s2中有的元素ri,s1中必然也有

并且,必然存在一個元素r0,滿足s1中有,但是s2中沒有(否則s1,s2其實是一個集合)

那么s2就是s1的真子集。

證畢。

?

2.Parent樹

發現,Right集合的兩個性質非常給力。

真子集的包含關系可以看做父子關系

Right集合之間的不相交關系可以看做非祖宗關系。

圖示:from clj

可以發現,對于集合字符串長度,有mx[fa[i]]+1=min[i]

進而可以這樣理解:

father集合的結束位置更多,所以滿足的字符串長度必然更短。

而且,有mx[fa[i]]+1=min[i]

葉子節點只有一個r

那么,如果從某個葉子節點往上找,那么所遍歷的長度,就是以r結尾的所有子串。

?

三、SAM的構造

?

數組:(節點及$Right$集合,即所謂的“狀態”)

$fa[i]$——節點$parent$樹上的$father$

$ch[i][∑]$——節點的出邊(類比$trie$樹),指向添加字符$x$后到達的下一個節點($s[ri+1]==x$的會繼續匹配上,這些$ri$就構成了目標的節點。如果沒有,那么就指向$NULL$(實際不會操作,即$0$號節點就是$NULL$節點))。$∑$是字符集大小

$len[i]$——某個節點的所有字符串中最長的長度。

$nd$——當前匹配的最后一個節點$p$。若當前是$L$,則$Right(p)={L}$

$cnt$——節點個數。


我令$rt$節點為$1$

開始$nd=rt$

增量法構造,即每次插入一個字符$x$。設當前已經插入的字符串長度是$L$

我們要做的,就是新加入這個節點之后,維護好所有的后綴(使得后綴都能走出來)。

需要維護$fa$指針,和$ch[∑]$指針。


(以下祖先指$parent$樹上的祖先)

建立一個$np$節點,其實有$Right(np)={L+1}$

賦值:$len[np]=L+1$

對于$p$的祖先,$p_1,p_2,...p_k$

存在一段$p_1,p_2,...p_{v-1}$

滿足都沒有$ch[*][x]$的邊(也即都指向$NULL$)

對于$p_v$存在一個指向$x$的邊

設$q=ch[p_v][x]$,

存在兩種情況

$I$

$len[q]==len[p_v]+1$

那么意味著,之前所有的狀態$q$代表的位置都可以加一個$x$匹配上將要加入的字符

那么可以直接把$q$當做$np$的$father$

(這個時候,其實$q$的$Right$集合發生了變化,已經加入了一個位置$L+1$)

就可以結束這次插入。


$II$

$len[q]>len[p_v]+1$

這是什么意思?

圖示:(第一個串有一個B錯了)

?


那么意味著,$q$拋棄了一些$p_v$的存在位置,并且使得長度增加了。

那么,$q$一定拋棄了以$L$結尾的位置。

否則,會存在一個離p更近的祖先,滿足存在指向$x$的邊。

所以,這樣的$q$匹配上$x$之后,一定不會包含$L+1$位置。


那么,這個$q$,一定不能作為$np$的$father$(因為不包含$L+1$這個位置)

所以,我們新建一個節點$nq$

這個$nq$的$Right$集合,其實是:$Right(q)∪\{L+1\}$

我們還要對$nq$進行一些處理。

$fa[nq]=fa[q]$

$fa[q]=nq$

$fa[np]=nq$

$len[nq]=len[p_v]+1$

對于$nq$,所有$q$的出邊,都是$nq$的出邊

對于$ch[p_i][x]==q$的$p$的祖先(一定是連續一段),把出邊$ch[p_i][x]$都設置為$nq$

?

(這里并不會存在這種情況:p的某個祖先$p'$

使得$len[ch[p'][x]]>len[p']+1$

因為,處理$q$的時候,就相當于在那個時候,加入一個$x$,$p'$會是當時的$p_v$

在那個時候會考慮到的。


然后就可以結束了。

?

?



代碼:

luoguP3804 【模板】后綴自動機

(注意,點數是2倍)

#include<bits/stdc++.h> #define reg register int #define il inline #define numb (ch^'0') using namespace std; typedef long long ll; il void rd(int &x){char ch;x=0;bool fl=false;while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true);for(x=numb;isdigit(ch=getchar());x=x*10+numb);(fl==true)&&(x=-x); } namespace Miracle{ const int N=3e6+6; int n,m; ll ans; char s[N]; struct SAM{int ch[N][30],cnt;int fa[N],nd;int sz[N];int hd[N],tot;int len[N];struct node{int nxt,to;}e[2*N];void init(){nd=1;cnt=1;}void add(int x,int y){e[++tot].nxt=hd[x];e[tot].to=y;hd[x]=tot;}void ins(int c,int pos){int p=nd;sz[nd=++cnt]=1;len[nd]=pos;for(;p&&ch[p][c]==0;p=fa[p]) ch[p][c]=nd;if(p==0) {fa[nd]=1;return;}int q=ch[p][c];if(len[q]==len[p]+1){fa[nd]=q;return;}len[++cnt]=len[p]+1;for(reg i=1;i<=26;++i) ch[cnt][i]=ch[q][i];fa[cnt]=fa[q];fa[q]=cnt;fa[nd]=cnt;for(;p&&ch[p][c]==q;p=fa[p]) ch[p][c]=cnt;}void build(){for(reg i=2;i<=cnt;++i) add(fa[i],i);}void dfs(int x){for(reg i=hd[x];i;i=e[i].nxt){dfs(e[i].to);sz[x]+=sz[e[i].to];}if(sz[x]!=1) ans=max(ans,(ll)sz[x]*len[x]);} }sam; int main(){scanf("%s",s+1);n=strlen(s+1);sam.init();for(reg i=1;i<=n;++i) sam.ins(s[i]-'a'+1,i);sam.build();sam.dfs(1);printf("%lld",ans);return 0; }} int main(){Miracle::main();return 0; }/*Author: *Miracle*Date: 2018/11/16 21:54:31 */

?

?

?

四、構造正確性證明(理解)

?

其實就是實時維護好fa,len,ch[∑]

這樣就可以保證Right集合,和Parent樹的結構和定義。

?

觀察步驟,我們可以保證,從某個狀態出發,加一個字符,一定能走到新的字符串的集合的節點;

從某個狀態出發,跳father,一定可以找到一個包含當前所有匹配位置的超集的集合。從而尋求更多的可能出邊。

?

維護了所有的后綴,所以維護了所有的子串。

并且Parent指針類似于AC自動機的fail指針,轉到了可能可以繼續匹配的某個出現過的、更短的字符串。

跳一次Parent,匹配長度至少-1

所以,能失配后,快速利用保留信息,所以也可以支持匹配。

?

五、應用

1.匹配

SP1811 LCS - Longest Common Substring

暴力匹配。

對A建SAM。

B在上面跑。

如果可以匹配,那么就匹配。len++

否則,不斷跳parent,直到有當前字符的出邊。本質上是通過減少匹配長度,從而增加匹配位置,從而有機會進行匹配。

然后,len=max[new]+1

隨時更新最長匹配長度,即答案。

?

B串的匹配情況,實際上類似于尺取法

?

A串放在SAM上,實際上是從多個位置一起嘗試匹配。(因為某個點是一個集合)

?

2.Parent樹

P3804 【模板】后綴自動機

Parent樹上DP size。

size相同的點選擇最長的。

本質上考慮到了所有的子串,每個子串會在自己出現所有出現位置的Right處被考慮到。

?

SP8222 NSUBSTR - Substrings

利用Parent樹處理出長度為某個長度的最大出現次數。

兒子的出現次數,縮短之后,必然可以作為father的出現次數。

所以,對于全局的數組,最后從后往前,ans[i]=max(ans[i],ans[i+1])處理即可

?

本質上,考慮Parent樹上的每個節點,就相當于考慮到了每個狀態,也就考慮到了每個字符串。

開始每個節點只保留最長的子串

最后倒序取max,就相當于考慮到了更小的子串。

?

?

P4248 [AHOI2013]差異

化簡式子之后,就是兩兩后綴LCP長度之和*2

?

后綴數組+單調棧/分治

在SA序列上考慮。直接+單調棧/分治即可

詳見:[HAOI2016]找相同字符

?

SAM+Parent樹上DP

把原串反過來。后綴的最長公共前綴,就變成了前綴的最長公共后綴

兩個前綴的最長公共后綴長度,即Parent樹上LCA點的len長度。

所以,只要統計Parent樹上,某個點是多少個葉子節點的LCA,然后乘上2*len即可。

直接樹形DP即可處理。

正確性考慮每個前綴,顯然和所有的前綴在LCA處正確統計了貢獻。

(其實不建反串也沒有問題,其實長度本質是對應的。雖然串本身有所不同)

差異

?

?

3.第k大子串

P1368 工藝

循環同構

SAM或者最小表示法

?

SAM:

把串復制一倍加進去。

要選擇一個長度為n的子串,使得字典序最小。

從起始位置隨便走n步,一定不會停止。

從起始位置走n步的所有方案數,對應所有長度為n的子串。

每次貪心選擇字典序最小的走即可。

(字符集比較大,用map存出邊)

?

?

最小表示法:

開始,i=0,j=1,k=0

不斷往后匹配,如果s[i+k]<s[j+k],那么,j=j+k+1,k=0

如果s[i+k]>s[j+k],那么,i=i+k+1,k=0

如果相同,++k

i,j跳的原因是,可以直接跳過一段不可能成為最優解開頭的位置。

最后,因為i,j兩個點之一一定保留了最優解。另一個超過了n

所以min(i,j)就是答案。

#include<bits/stdc++.h> #define reg register int #define il inline #define numb (ch^'0') using namespace std; typedef long long ll; il void rd(int &x){char ch;x=0;bool fl=false;while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true);for(x=numb;isdigit(ch=getchar());x=x*10+numb);(fl==true)&&(x=-x); } namespace Miracle{ const int N=300000+5; int s[2*N]; int n; int wrk(){int i=0,j=1,k=0;while(i<n&&j<n&&k<n){int t=s[(i+k)%n]-s[(j+k)%n];if(t==0) ++k;else{if(t<0) j+=k+1;else i+=k+1;if(i==j) ++j;k=0;}} return min(i,j); } int main(){scanf("%d",&n);for(reg i=0;i<n;++i) scanf("%d",&s[i]),s[i+n]=s[i];int st=wrk();for(reg i=st;i<=st+n-1;++i) printf("%d ",s[i]);return 0; }} int main(){Miracle::main();return 0; }/*Author: *Miracle*Date: 2018/11/17 10:20:49 */ 最小表示法

?

?

SP7258 SUBLEX - Lexicographical Substring Search

本質不同的第k大串

(后綴數組可以O(nlogn)解決)

(SAM)

DAG上,從起點出發的路徑條數就是本質不同子串個數。

所以,如果走到一個狀態節點,那么從這個節點出發的路徑條數就是固定的。

把這些串接到之前走出來的串后面,就構成了一個完整子串。

所以,可以反向建圖,topo+DP

就可以得到從某個點出發,往后走,走出來的本質不同子串個數了。

注意,可以在每個點停止一次。所以DP值還要特殊+1

?

然后,從起點開始走,選擇可以走的出邊即可。

走的時候,要減去每個點的1.如果是起始點,則不能減(因為認為空串不是一個串)

(當然,如果嫌每次枚舉26條出邊太慢,可以考慮每次進行二分,降低常數)

?

P3975 [TJOI2015]弦論

重復子串可以算多個的第K大。

區別就是,可以在每個點多“結束”幾次。(原來是1次)

換句話說,可以創造出多個相同的字符串,接到原來的路徑上。

這個次數,就是節點Right集合的大小。

Parent樹dfs處理大小。

然后直接類似上面一個題走即可。

#include<bits/stdc++.h> #define il inline #define reg register int #define numb (ch^'0') using namespace std; typedef long long ll; namespace Miracle{ const int N=500000+5; int n,t; char s[N]; struct SAM{int fa[2*N],len[2*N],sz[2*N],dp[2*N];int ch[2*N][28];int cnt,nd;int du[2*N];struct node{int nxt,to;}e[3*N],bian[2*N];int hd[2*N],ee;int pre[2*N],bb;SAM(){cnt=nd=1;}void upda(int x,int y){bian[++bb].nxt=pre[x];bian[bb].to=y;pre[x]=bb;}void add(int x,int y){e[++ee].nxt=hd[x];e[ee].to=y;hd[x]=ee;}void ins(int c,int pos){int p=nd;len[nd=++cnt]=pos;sz[cnt]=1;for(;p&&ch[p][c]==0;p=fa[p]) ch[p][c]=cnt;if(p==0){fa[cnt]=1;return;}int q=ch[p][c];if(len[q]==len[p]+1){fa[cnt]=q;return;}len[++cnt]=len[p]+1;for(reg i=1;i<=26;++i) ch[cnt][i]=ch[q][i];fa[cnt]=fa[q];fa[q]=fa[nd]=cnt;for(;p&&ch[p][c]==q;p=fa[p]) ch[p][c]=cnt;}void tree(){for(reg i=2;i<=cnt;++i) upda(fa[i],i);}void dfs(int x){for(reg i=pre[x];i;i=bian[i].nxt){int y=bian[i].to;dfs(y);sz[x]+=sz[y];}}void DAG(){for(reg i=1;i<=cnt;++i){for(reg j=1;j<=26;++j){if(ch[i][j]){add(ch[i][j],i);++du[i];}}}}void topo(){if(!t){for(reg i=2;i<=cnt;++i)sz[i]=1;}sz[1]=0;queue<int>q;for(reg i=1;i<=cnt;++i){if(du[i]==0) q.push(i);}while(!q.empty()){int x=q.front();q.pop();dp[x]+=sz[x];for(reg i=hd[x];i;i=e[i].nxt){int y=e[i].to;dp[y]+=dp[x];du[y]--;if(du[y]==0) q.push(y);}}}void query(int k){int now=1;//cout<<" dp "<<dp[now]<<endl;if(k>dp[now]) {printf("-1");return;}while(1){//cout<<" jhaahah "<<now<<" "<<sz[now]<<endl;if(k<=sz[now]) break;k-=sz[now];for(reg i=1;i<=26;++i){if(ch[now][i]){if(k>dp[ch[now][i]]) k-=dp[ch[now][i]];else {putchar('a'+i-1);now=ch[now][i];break;}}}}} }sam; int main(){scanf("%s",s+1);n=strlen(s+1);int k;scanf("%d%d",&t,&k);for(reg i=1;i<=n;++i){sam.ins(s[i]-'a'+1,i);}if(t){sam.tree();sam.dfs(1);}sam.DAG();sam.topo();sam.query(k);return 0; }} int main(){Miracle::main();return 0; }/*Author: *Miracle*Date: 2018/11/18 9:06:05 */ 弦論

?

4.與其他算法結合

?P4022 [CTSC2012]熟悉的文章

二分+DP+單調隊列+SAM

?

二分L

對于分段dp,可以直接列出式子:

dp[i]=max(dp[j]+i-j,dp[i-1]) (i-maxlen[i]<=j<=i-L)

其中,maxlen[i],表示,到了i位置,最長往前匹配的子串長度,使得這個子串是某個模板串的子串。

把模板串用'2'接起來,然后建SAM。跑出每個位置的maxlen

發現,maxlen[i]每次最多+1,i每次加1,所以,i-maxlen[i]單調不減。

單調隊列優化即可。

判斷dp[n]>=0.9*n即可。

O(nlogn)

?

?

P3649 [APIO2014]回文串

本質不同O(n)個回文串,求出所有本質不同的回文串,然后找每個串的出現次數。

?

(SA+二分+manacher

每一個串,通過SA數組二分左右邊界。然后出現次數就是(R-L+1)

?

SAM+倍增+manacher

每一個串,parent樹上,從葉子開始,倍增找符合min[i]<=len<=max[i]的節點i。i的Right集合大小就是該回文串出現次數。

?

六、廣義后綴自動機

?多個主串匹配。

建造方法一般有兩個:

法一:

把每個主串依次放進SAM

不同主串之間,強制使得從root開始構造。

法二:

對所有的主串依次插入建造出一個trie

trie上建SAM,可以dfs,或者bfs建。

只要記錄樹上x的father的SAM上的節點編號p,從這個p作為nd開始建即可。

?

點數O(2*總長度)

Parent樹,DAG圖,仍然可以使用。

Parent樹,仍然可以統計size等等。Right集合size的意義,是后x個主串的某些子串的出現個數和。。。。(對于第i個串,x應該是n-i+1,,,n是主串個數)

利用Parent樹,可以統計某些子串在所有串的分別出現次數(用size[p][i]統計),或者,可以判斷一個字符串是否僅屬于某個(某幾個)模式串

?

發現:

畫圖發現,

如果對于多個主串,可能某些節點不能從根節點出發到達。那么,這樣的節點的路徑一定可以從其他的節點表示。

這樣的節點其實不具備實際意義(一般不予統計答案,但是作為一個葉子,也就是信息的存儲點,一定從樹上要往上傳信息。)

因為,一定有len[i]==len[fa[i]],否則,這個點就有了串,但是不能從根節點到達。(假裝這么證是對的)

如果忽視這樣的點,那么剩下的點的Right集合就有意義。

它一定代表著,這些串在所有的主串的所有出現位置的集合

所以,我們可以利用Parent樹,處理某個、某些字符串在個個主串的出現情況。

?

(當然,如果廣義SAM要處理Right集合的大小的話,要根據建造的方法處理

1.所有的串依次從rt=1開始插入

葉子sz賦值為1,然后Parent樹上往上push。注意,不管節點是否有實際意義,都要push上去。因為代表一個出現的位置

2.trie樹(其實這個就是把前綴合并在了一起,理論上更節省SAM的空間)

先要找到trie樹某個點的sz[x],然后插入的時候sz就是這個sz[x]

?

例題:

P4081 [USACO17DEC]Standing Out from the Herd

建立廣義SAM

每個葉子節點,保留所屬主串的id

Parent樹上DP,統計某個節點所代表的那些字符串對某個主串答案的貢獻(顯然同一個節點,最多為一個主串貢獻答案)

當id只有一個的時候,ans[id[x]]+=len[id[x]]-len[fa[x]]

代碼:

#include<bits/stdc++.h> #define reg register int #define il inline #define numb (ch^'0') using namespace std; typedef long long ll; il void rd(int &x){char ch;x=0;bool fl=false;while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true);for(x=numb;isdigit(ch=getchar());x=x*10+numb);(fl==true)&&(x=-x); } namespace Miracle{ const int N=1e5+5; int n,m; char s[N]; ll ans[N]; struct SAM{int ch[2*N][27];int nd,cnt;int len[2*N],fa[2*N];int id[2*N];SAM(){nd=cnt=1;}struct node{int nxt,to;}e[2*N];int hd[2*N],tot;void add(int x,int y){e[++tot].nxt=hd[x];e[tot].to=y;hd[x]=tot;}void ins(int c,int pos,int lp){int p=nd;len[nd=++cnt]=pos;id[cnt]=lp;for(;p&&ch[p][c]==0;p=fa[p]) ch[p][c]=cnt;if(p==0){fa[cnt]=1;return;}int q=ch[p][c];if(len[q]==len[p]+1){fa[cnt]=q;return;}len[++cnt]=len[p]+1;for(reg i=1;i<=26;++i) ch[cnt][i]=ch[q][i];fa[cnt]=fa[q];fa[q]=fa[nd]=cnt;for(;p&&ch[p][c]==q;p=fa[p]) ch[p][c]=cnt;}void build(){//cout<<" cnt "<<cnt<<endl;for(reg i=2;i<=cnt;++i) add(fa[i],i);}void dfs(int x){for(reg i=hd[x];i;i=e[i].nxt){int y=e[i].to;dfs(y);if(id[x]==-1) continue;if(id[y]){if(id[y]==-1) id[x]=-1;else if(id[x]){if(id[x]!=id[y]) id[x]=-1;}else id[x]=id[y];}}if(id[x]!=-1) ans[id[x]]+=len[x]-len[fa[x]];//cout<<x<<" : "<<id[x]<<endl; } }sam; int main(){rd(m);for(reg i=1;i<=m;++i){sam.nd=1;scanf("%s",s+1);n=strlen(s+1);for(reg j=1;j<=n;++j){sam.ins(s[j]-'a'+1,j,i);} }sam.build();sam.dfs(1);for(reg i=1;i<=m;++i){printf("%lld\n",ans[i]);}return 0; }} int main(){Miracle::main();return 0; }/*Author: *Miracle*Date: 2018/11/19 17:09:46 */ [USACO17DEC]Standing Out from the Herd

?

?

七、理解

1.和AC自動機比較

都是通過類似失配指針的思想,在失配的時候,盡量小地減少匹配長度,找到能夠匹配的位點。

SAM還是完爆AC自動機的。畢竟AC自動機只能處理前綴問題。。不能處理子串匹配。

?

2.SAM的用法:

SAM雖然就是一個圖但是其實博大精深!!

有一棵樹,一張DAG,方法自然多了很多。

?

①Parent樹(子串整體考慮)

樹形DP以及信息轉移——>可以賦予每個節點的Right以實際意義。

同樣,father關系可以推出某個狀態的最短長度min

?

②出邊(trans)的DAG(處理匹配以及單個子串)

支持topo排序DP。通常用于考慮路徑條數——對應子串個數

支持字符串匹配。

?

③與其他算法結合(一起用)

與SA:倒不算是結合,,大部分時候,SA能做的,SAM都能做。SA不能做的,SAM也能做。

SA和SAM的思路不太一樣。

畢竟,SA:區間、區間取min、二分、ST表、單調隊列、單調棧、分治

SAM:樹形結構,DAG(路徑問題),topo排序,DP,貪心。。

用法就要看題了。

?

3.小結論/小觀察:

①某個節點i所管轄的字符串長度為[mini,maxi]的,有且僅有這些字符串在SAM上跑,會在i點結束匹配。

②跳father就是跳fail,通過減少匹配長度(當然,這個減少后的長度必須之前跑出過。(這也是為什么要建立nq節點。否則跑出來的串之前都沒有存在23333)),進而增加出現位置,從而尋找更多的可能有當前字符x出邊的位置。從而進行狀態的轉移。

由于max[fa[i]]+1=min[fa[i]],所以,其實減少匹配長度時,本質上考慮到了所有的長度。(只不過匹配位置不增加的我們不特殊考慮)

SAM和思想核心在于,對于本質相同的字符串進行壓縮,對所有結尾出現位置相同的字符串再進行壓縮。一個點的集合,其實可以是很多個字符串。

我們相當于,對一個串的所有子串,按照它們的所有結束位置分類,一起來考慮。

我們不但節省了時間和空間、考慮了所有的子串,

并且,一個子串的出現次數,出現位置,以及不同子串的LCP,大小,出現位置比較,匹配,都可以比較輕松地處理。

?

4.Parent樹和出邊DAG的聯系

DAG為Parent樹提供構造基礎。

Parent樹為DAG的點賦予實際意義。(例如Right集合實際大小)

?

轉載于:https://www.cnblogs.com/Miracevin/p/9974743.html

總結

以上是生活随笔為你收集整理的[学习笔记]后缀自动机的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。