[学习笔记]后缀自动机
解決大部分字符串問題的大殺器
給一下clj課件:戳我
SAM是一個博大精深的算法。雖然沒有像網(wǎng)絡(luò)流家族,Tarjan家族一樣拉幫結(jié)派,但是自身包含很多方法。
?
一、前言
字符串常見問題:各種匹配
1.LCP
2.子串匹配
3.模式串和主串匹配
4.回文串?也算匹配吧。。。
?
經(jīng)典例子:
給兩個串,求最長公共子串長度。
用hash表+二分,可以做到nlogn
?
后綴自動機(jī)可以做到O(n)
?
二、概念
我們需要一個可以識別某個串是否為某個主串的子串、處理主串的子串信息、串和串之間進(jìn)行匹配的問題。
樸素的做法是,對每個子串建立一個節(jié)點,然后連邊,O(n^2)
?
考慮的優(yōu)化是,發(fā)現(xiàn)一些子串會出現(xiàn)很多次,一些子串出現(xiàn)所有的出現(xiàn)位置的集合是同一個集合!
對這個進(jìn)行優(yōu)化
1.Right集合
對于某個串s,所有出現(xiàn)的位置的集合是:{r1,r2,...,rm}
那么,這個集合就是Right(s)
由于許多的串si,可能Right集合都是Right(s)
如果我們給定集合Right(t)
再給定屬于這個集合的字符串的長度len
隨便選擇一個r,那么這個字符串就已經(jīng)唯一確定了。
一個Right集合會包含若干個字符串
那么,其len的范圍一定是一個區(qū)間[max,min]
?
那么,為了處理方便,我們會把這些串壓縮在一起。
保留這個Right集合的max
?
我們就會定義Right(s)為一個“狀態(tài)”
這個“狀態(tài)”,包含了所有的可能匹配到的節(jié)點末尾的位置ri,以及可能已經(jīng)匹配出來的字符串集合si
?
*性質(zhì):兩個Right集合要么不相交,要么一個是另一個的真子集
證明:
首先易證,一個字符串屬于且僅屬于一個Right集合。
設(shè)兩個集合s1,s2相交,r是交集的某個元素
那么對于一個屬于s1的字符串string1
一個屬于s2的字符串string2
由于兩個字符串都可以以r作為末尾出現(xiàn)一次,由于一個字符串屬于且僅屬于一個集合
所以,string1!=string2
不妨設(shè)len(string1)=l1<len(string2)=l2
那么,顯然地,string2出現(xiàn)的位置,string1必然也會出現(xiàn)。
所以,s2中有的元素ri,s1中必然也有
并且,必然存在一個元素r0,滿足s1中有,但是s2中沒有(否則s1,s2其實是一個集合)
那么s2就是s1的真子集。
證畢。
?
2.Parent樹
發(fā)現(xiàn),Right集合的兩個性質(zhì)非常給力。
真子集的包含關(guān)系可以看做父子關(guān)系
Right集合之間的不相交關(guān)系可以看做非祖宗關(guān)系。
圖示:from clj
可以發(fā)現(xiàn),對于集合字符串長度,有mx[fa[i]]+1=min[i]
進(jìn)而可以這樣理解:
father集合的結(jié)束位置更多,所以滿足的字符串長度必然更短。
而且,有mx[fa[i]]+1=min[i]
葉子節(jié)點只有一個r
那么,如果從某個葉子節(jié)點往上找,那么所遍歷的長度,就是以r結(jié)尾的所有子串。
?
三、SAM的構(gòu)造
?
數(shù)組:(節(jié)點及$Right$集合,即所謂的“狀態(tài)”)
$fa[i]$——節(jié)點$parent$樹上的$father$
$ch[i][∑]$——節(jié)點的出邊(類比$trie$樹),指向添加字符$x$后到達(dá)的下一個節(jié)點($s[ri+1]==x$的會繼續(xù)匹配上,這些$ri$就構(gòu)成了目標(biāo)的節(jié)點。如果沒有,那么就指向$NULL$(實際不會操作,即$0$號節(jié)點就是$NULL$節(jié)點))。$∑$是字符集大小
$len[i]$——某個節(jié)點的所有字符串中最長的長度。
$nd$——當(dāng)前匹配的最后一個節(jié)點$p$。若當(dāng)前是$L$,則$Right(p)={L}$
$cnt$——節(jié)點個數(shù)。
我令$rt$節(jié)點為$1$
開始$nd=rt$
增量法構(gòu)造,即每次插入一個字符$x$。設(shè)當(dāng)前已經(jīng)插入的字符串長度是$L$
我們要做的,就是新加入這個節(jié)點之后,維護(hù)好所有的后綴(使得后綴都能走出來)。
需要維護(hù)$fa$指針,和$ch[∑]$指針。
(以下祖先指$parent$樹上的祖先)
建立一個$np$節(jié)點,其實有$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$的邊
設(shè)$q=ch[p_v][x]$,
存在兩種情況
$I$
$len[q]==len[p_v]+1$
那么意味著,之前所有的狀態(tài)$q$代表的位置都可以加一個$x$匹配上將要加入的字符
那么可以直接把$q$當(dāng)做$np$的$father$
(這個時候,其實$q$的$Right$集合發(fā)生了變化,已經(jīng)加入了一個位置$L+1$)
就可以結(jié)束這次插入。
$II$
$len[q]>len[p_v]+1$
這是什么意思?
圖示:(第一個串有一個B錯了)
?
那么意味著,$q$拋棄了一些$p_v$的存在位置,并且使得長度增加了。
那么,$q$一定拋棄了以$L$結(jié)尾的位置。
否則,會存在一個離p更近的祖先,滿足存在指向$x$的邊。
所以,這樣的$q$匹配上$x$之后,一定不會包含$L+1$位置。
那么,這個$q$,一定不能作為$np$的$father$(因為不包含$L+1$這個位置)
所以,我們新建一個節(jié)點$nq$
這個$nq$的$Right$集合,其實是:$Right(q)∪\{L+1\}$
我們還要對$nq$進(jìn)行一些處理。
$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$的祖先(一定是連續(xù)一段),把出邊$ch[p_i][x]$都設(shè)置為$nq$
?
(這里并不會存在這種情況:p的某個祖先$p'$
使得$len[ch[p'][x]]>len[p']+1$
因為,處理$q$的時候,就相當(dāng)于在那個時候,加入一個$x$,$p'$會是當(dāng)時的$p_v$
在那個時候會考慮到的。
)
然后就可以結(jié)束了。
?
?
代碼:
luoguP3804 【模板】后綴自動機(jī)
(注意,點數(shù)是2倍)
?
?
?
四、構(gòu)造正確性證明(理解)
?
其實就是實時維護(hù)好fa,len,ch[∑]
這樣就可以保證Right集合,和Parent樹的結(jié)構(gòu)和定義。
?
觀察步驟,我們可以保證,從某個狀態(tài)出發(fā),加一個字符,一定能走到新的字符串的集合的節(jié)點;
從某個狀態(tài)出發(fā),跳father,一定可以找到一個包含當(dāng)前所有匹配位置的超集的集合。從而尋求更多的可能出邊。
?
維護(hù)了所有的后綴,所以維護(hù)了所有的子串。
并且Parent指針類似于AC自動機(jī)的fail指針,轉(zhuǎn)到了可能可以繼續(xù)匹配的某個出現(xiàn)過的、更短的字符串。
跳一次Parent,匹配長度至少-1
所以,能失配后,快速利用保留信息,所以也可以支持匹配。
?
五、應(yīng)用
1.匹配
SP1811 LCS - Longest Common Substring
暴力匹配。
對A建SAM。
B在上面跑。
如果可以匹配,那么就匹配。len++
否則,不斷跳parent,直到有當(dāng)前字符的出邊。本質(zhì)上是通過減少匹配長度,從而增加匹配位置,從而有機(jī)會進(jìn)行匹配。
然后,len=max[new]+1
隨時更新最長匹配長度,即答案。
?
B串的匹配情況,實際上類似于尺取法
?
A串放在SAM上,實際上是從多個位置一起嘗試匹配。(因為某個點是一個集合)
?
2.Parent樹
P3804 【模板】后綴自動機(jī)
Parent樹上DP size。
size相同的點選擇最長的。
本質(zhì)上考慮到了所有的子串,每個子串會在自己出現(xiàn)所有出現(xiàn)位置的Right處被考慮到。
?
SP8222 NSUBSTR - Substrings
利用Parent樹處理出長度為某個長度的最大出現(xiàn)次數(shù)。
兒子的出現(xiàn)次數(shù),縮短之后,必然可以作為father的出現(xiàn)次數(shù)。
所以,對于全局的數(shù)組,最后從后往前,ans[i]=max(ans[i],ans[i+1])處理即可
?
本質(zhì)上,考慮Parent樹上的每個節(jié)點,就相當(dāng)于考慮到了每個狀態(tài),也就考慮到了每個字符串。
開始每個節(jié)點只保留最長的子串
最后倒序取max,就相當(dāng)于考慮到了更小的子串。
?
?
P4248 [AHOI2013]差異
化簡式子之后,就是兩兩后綴LCP長度之和*2
?
后綴數(shù)組+單調(diào)棧/分治
在SA序列上考慮。直接+單調(diào)棧/分治即可
詳見:[HAOI2016]找相同字符
?
SAM+Parent樹上DP
把原串反過來。后綴的最長公共前綴,就變成了前綴的最長公共后綴
兩個前綴的最長公共后綴長度,即Parent樹上LCA點的len長度。
所以,只要統(tǒng)計Parent樹上,某個點是多少個葉子節(jié)點的LCA,然后乘上2*len即可。
直接樹形DP即可處理。
正確性考慮每個前綴,顯然和所有的前綴在LCA處正確統(tǒng)計了貢獻(xiàn)。
(其實不建反串也沒有問題,其實長度本質(zhì)是對應(yīng)的。雖然串本身有所不同)
差異?
?
3.第k大子串
P1368 工藝
循環(huán)同構(gòu)
SAM或者最小表示法
?
SAM:
把串復(fù)制一倍加進(jìn)去。
要選擇一個長度為n的子串,使得字典序最小。
從起始位置隨便走n步,一定不會停止。
從起始位置走n步的所有方案數(shù),對應(yīng)所有長度為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跳的原因是,可以直接跳過一段不可能成為最優(yōu)解開頭的位置。
最后,因為i,j兩個點之一一定保留了最優(yōu)解。另一個超過了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
本質(zhì)不同的第k大串
(后綴數(shù)組可以O(shè)(nlogn)解決)
(SAM)
DAG上,從起點出發(fā)的路徑條數(shù)就是本質(zhì)不同子串個數(shù)。
所以,如果走到一個狀態(tài)節(jié)點,那么從這個節(jié)點出發(fā)的路徑條數(shù)就是固定的。
把這些串接到之前走出來的串后面,就構(gòu)成了一個完整子串。
所以,可以反向建圖,topo+DP
就可以得到從某個點出發(fā),往后走,走出來的本質(zhì)不同子串個數(shù)了。
注意,可以在每個點停止一次。所以DP值還要特殊+1
?
然后,從起點開始走,選擇可以走的出邊即可。
走的時候,要減去每個點的1.如果是起始點,則不能減(因為認(rèn)為空串不是一個串)
(當(dāng)然,如果嫌每次枚舉26條出邊太慢,可以考慮每次進(jìn)行二分,降低常數(shù))
?
P3975 [TJOI2015]弦論
重復(fù)子串可以算多個的第K大。
區(qū)別就是,可以在每個點多“結(jié)束”幾次。(原來是1次)
換句話說,可以創(chuàng)造出多個相同的字符串,接到原來的路徑上。
這個次數(shù),就是節(jié)點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.與其他算法結(jié)合
?P4022 [CTSC2012]熟悉的文章
二分+DP+單調(diào)隊列+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
發(fā)現(xiàn),maxlen[i]每次最多+1,i每次加1,所以,i-maxlen[i]單調(diào)不減。
單調(diào)隊列優(yōu)化即可。
判斷dp[n]>=0.9*n即可。
O(nlogn)
?
?
P3649 [APIO2014]回文串
本質(zhì)不同O(n)個回文串,求出所有本質(zhì)不同的回文串,然后找每個串的出現(xiàn)次數(shù)。
?
(SA+二分+manacher
每一個串,通過SA數(shù)組二分左右邊界。然后出現(xiàn)次數(shù)就是(R-L+1)
?
SAM+倍增+manacher
每一個串,parent樹上,從葉子開始,倍增找符合min[i]<=len<=max[i]的節(jié)點i。i的Right集合大小就是該回文串出現(xiàn)次數(shù)。
?
六、廣義后綴自動機(jī)
?多個主串匹配。
建造方法一般有兩個:
法一:
把每個主串依次放進(jìn)SAM
不同主串之間,強(qiáng)制使得從root開始構(gòu)造。
法二:
對所有的主串依次插入建造出一個trie
trie上建SAM,可以dfs,或者bfs建。
只要記錄樹上x的father的SAM上的節(jié)點編號p,從這個p作為nd開始建即可。
?
點數(shù)O(2*總長度)
Parent樹,DAG圖,仍然可以使用。
Parent樹,仍然可以統(tǒng)計size等等。Right集合size的意義,是后x個主串的某些子串的出現(xiàn)個數(shù)和。。。。(對于第i個串,x應(yīng)該是n-i+1,,,n是主串個數(shù))
利用Parent樹,可以統(tǒng)計某些子串在所有串的分別出現(xiàn)次數(shù)(用size[p][i]統(tǒng)計),或者,可以判斷一個字符串是否僅屬于某個(某幾個)模式串
?
發(fā)現(xiàn):
畫圖發(fā)現(xiàn),
如果對于多個主串,可能某些節(jié)點不能從根節(jié)點出發(fā)到達(dá)。那么,這樣的節(jié)點的路徑一定可以從其他的節(jié)點表示。
這樣的節(jié)點其實不具備實際意義(一般不予統(tǒng)計答案,但是作為一個葉子,也就是信息的存儲點,一定從樹上要往上傳信息。)
因為,一定有l(wèi)en[i]==len[fa[i]],否則,這個點就有了串,但是不能從根節(jié)點到達(dá)。(假裝這么證是對的)
如果忽視這樣的點,那么剩下的點的Right集合就有意義。
它一定代表著,這些串在所有的主串的所有出現(xiàn)位置的集合
所以,我們可以利用Parent樹,處理某個、某些字符串在個個主串的出現(xiàn)情況。
?
(當(dāng)然,如果廣義SAM要處理Right集合的大小的話,要根據(jù)建造的方法處理
1.所有的串依次從rt=1開始插入
葉子sz賦值為1,然后Parent樹上往上push。注意,不管節(jié)點是否有實際意義,都要push上去。因為代表一個出現(xiàn)的位置
2.trie樹(其實這個就是把前綴合并在了一起,理論上更節(jié)省SAM的空間)
先要找到trie樹某個點的sz[x],然后插入的時候sz就是這個sz[x]
)
?
例題:
P4081 [USACO17DEC]Standing Out from the Herd
建立廣義SAM
每個葉子節(jié)點,保留所屬主串的id
Parent樹上DP,統(tǒng)計某個節(jié)點所代表的那些字符串對某個主串答案的貢獻(xiàn)(顯然同一個節(jié)點,最多為一個主串貢獻(xiàn)答案)
當(dāng)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自動機(jī)比較
都是通過類似失配指針的思想,在失配的時候,盡量小地減少匹配長度,找到能夠匹配的位點。
SAM還是完爆AC自動機(jī)的。畢竟AC自動機(jī)只能處理前綴問題。。不能處理子串匹配。
?
2.SAM的用法:
SAM雖然就是一個圖但是其實博大精深!!
有一棵樹,一張DAG,方法自然多了很多。
?
①Parent樹(子串整體考慮)
樹形DP以及信息轉(zhuǎn)移——>可以賦予每個節(jié)點的Right以實際意義。
同樣,father關(guān)系可以推出某個狀態(tài)的最短長度min
?
②出邊(trans)的DAG(處理匹配以及單個子串)
支持topo排序DP。通常用于考慮路徑條數(shù)——對應(yīng)子串個數(shù)
支持字符串匹配。
?
③與其他算法結(jié)合(一起用)
與SA:倒不算是結(jié)合,,大部分時候,SA能做的,SAM都能做。SA不能做的,SAM也能做。
SA和SAM的思路不太一樣。
畢竟,SA:區(qū)間、區(qū)間取min、二分、ST表、單調(diào)隊列、單調(diào)棧、分治
SAM:樹形結(jié)構(gòu),DAG(路徑問題),topo排序,DP,貪心。。
用法就要看題了。
?
3.小結(jié)論/小觀察:
①某個節(jié)點i所管轄的字符串長度為[mini,maxi]的,有且僅有這些字符串在SAM上跑,會在i點結(jié)束匹配。
②跳father就是跳fail,通過減少匹配長度(當(dāng)然,這個減少后的長度必須之前跑出過。(這也是為什么要建立nq節(jié)點。否則跑出來的串之前都沒有存在23333)),進(jìn)而增加出現(xiàn)位置,從而尋找更多的可能有當(dāng)前字符x出邊的位置。從而進(jìn)行狀態(tài)的轉(zhuǎn)移。
由于max[fa[i]]+1=min[fa[i]],所以,其實減少匹配長度時,本質(zhì)上考慮到了所有的長度。(只不過匹配位置不增加的我們不特殊考慮)
③SAM和思想核心在于,對于本質(zhì)相同的字符串進(jìn)行壓縮,對所有結(jié)尾出現(xiàn)位置相同的字符串再進(jìn)行壓縮。一個點的集合,其實可以是很多個字符串。
我們相當(dāng)于,對一個串的所有子串,按照它們的所有結(jié)束位置分類,一起來考慮。
我們不但節(jié)省了時間和空間、考慮了所有的子串,
并且,一個子串的出現(xiàn)次數(shù),出現(xiàn)位置,以及不同子串的LCP,大小,出現(xiàn)位置比較,匹配,都可以比較輕松地處理。
?
4.Parent樹和出邊DAG的聯(lián)系
DAG為Parent樹提供構(gòu)造基礎(chǔ)。
Parent樹為DAG的點賦予實際意義。(例如Right集合實際大小)
?
轉(zhuǎn)載于:https://www.cnblogs.com/Miracevin/p/9974743.html
總結(jié)
以上是生活随笔為你收集整理的[学习笔记]后缀自动机的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 技术 | Python从零开始系列连载(
- 下一篇: oracle备份与恢复--闪回技术