后綴自動機是個好東西,代碼短還很快。
這是因為根到一個節點的不同路徑表示所有有某種相同性質的不同字符串。
假設對於字符串S建后綴自動機,以下名詞的意思是:
right(S的子串s):s在S中出現的位置的右端點的集合;
狀態:有同樣right集合的子串,某狀態的right集合是指該狀態的所有字符串的right集合;
ancestor(狀態k):表示一個集合,狀態k的right集合是此集合中所有狀態的right集合的真子集;
parent(狀態k):ancestor(k)中right集合最小的那一個;
字符邊:就是和trie樹中的邊一樣的那種邊,別人都叫它狀態的轉移;
dis(狀態k):根到某個狀態的最長路徑。
這樣會使得它們有一些神奇的性質:
1.對於某個狀態,其中所有字符串的長度在一個區間內。
想象一下,對於字符串S,right(s)={x1,x2,x3}。
那么隨着s的長度增加,三個位置前面可能會出現不一樣的,right(s)就不是x1,x2,x3了;
隨着s的長度減小,可能會在除了x1,x2,x3以外的地方有s出現,同上。
根據這條性質,就可以用根到狀態的不同字符邊路徑表示狀態中包含的不同字符串,dis(x)則表示該狀態長度區間的上邊界。那么下邊界該怎么得來呢?剛剛說過,隨着長度減少,right集合會更大。因為ancestor(x)的right集合都大於x,所以它們的字符串長度取值范圍都小於x。那么其中最長的一個的長度應該剛好是x的下邊界-1。因為對應的字符串長度最長,所以right集合最小,那么就是parent(x)了。也就是說,點x的下邊界是dis(parent(x))+1,可以直接求出,沒必要額外存一個。
2.對於任意兩個狀態,要么它們的right集合沒有交集,要么一個是另一個的真子集。
考慮反證法。
假設存在兩個狀態k1,k2的right集合有交集但其中一個不是另一個的真子集。
從k1,k2中分別取s1,s2。
因為s1,s2的right集合有交集,所以必然有一個是另一個的后綴。假設s1是s2的后綴。
對於right(s2),也就是s2出現的所有位置,它的后綴必然都出現了,所以right(s2)是right(s1)的真子集,假設不成立。
根據這條性質,對於所有狀態A的right集合是狀態B的right集合的真子集,且不存在狀態C,使得狀態A的right集合是狀態C的right集合的真子集,狀態C的right集合是狀態B的right集合的真子集,都可以得出parent(A)=B,也就形成了傳說中的parent樹。
3.parent(k)中的所有子串都是k的所有子串的后綴。
這是性質2的推論。
根據這條性質,parent邊可以當AC自動機的失配邊來用。
4.parent樹的根對應的狀態是空串。
因為每個位置都有空串。
根據這條性質,可以把起點和parent樹的根直接稱為“根”。
5.並沒有第五條。
根據這條性質,就會發現這並不是對勁的博客。
那么該如何構建呢?考慮每次加一個字符s[x],新建節點np,大概想一想方法。
新串肯定包含原串的所有子串。相比於原串,新串多出的是新串的所有后綴。
那么將原串的所有后綴(包括空串)對應的狀態都連一條值為s[x]的字符邊到np上。
原串的后綴很好找,從上一次加入的點開始,沿着的parent邊走到根就行了。
“大概”想完后,似乎一切都解決了,然而並不。
如果原串的所有后綴的狀態都沒有連過值為s[x]的字符邊,那么s[x]這個字符在字符串中第一次出現。這時先照着“大概”做,再將np的parent連向根。因為s[x]這個字符從未出現過,只有空串是它的后綴。
如果在沿着parent邊走的過程中走到一個狀態p已經連了一條值為s[x]的字符邊到q,也就是說s[x]這個字符不是第一次出現,該怎么辦?
為了保證時間復雜度,不能再連一條值為s[x]的字符邊。為了保證正確性,也不能將p到q的字符邊直接接到np上。
情況一:如果dis(q)==dis(p)+1,直接令parent(np)=q就行了。能走到p說明p中所有字符串是舊串的后綴。dis(q)==dis(p)+1,說明p與q的差距就在於q中所有字符串是p中所有字符串末尾加s[x],q中所有字符串是新串的后綴。至於為什么ancestor(p)不用向np連字符邊,是因為q和ancestor(q)中所有字符串已經包括了長度不超過dis(q)的所有的新串的后綴(←聽上去好繞)。
情況二:如果dis(q)!=dis(p)+1,也就是說q中的字符串不全是新串的后綴,而是新串的后綴前面再加上些什么東西,情況就有些復雜了。這時考慮再新建一個節點nq,將q的所有信息(包括parent)都復制過去,將ancestor(p)中所有連向q的字符邊都改為連向nq,再將q,np的parent都改為nq。這是因為q中有的字符串是新串的后綴前面再加上些什么東西,它不在新串的末尾出現。這就相當於q不僅僅有來自ancestor(p)的連邊。這時就需要新建一個nq節點使得nq中所有字符串是新串的后綴且是q的后綴(←這就是將q,np的parent設為nq的原因),也就是將p的祖先的s[x]邊接到nq上。
由於每次在末尾加一個字符,至多新增兩個節點,所以空間復雜度是o(n)的。至於為什么時間復雜度是均攤o(n),並不對勁的人並不知道。
看上去很復雜,但是代碼很簡單。

#include<iostream> #include<iomanip> #include<cstdio> #include<cstring> #include<cstdlib> #include<cmath> #include<algorithm> #define maxn 250010 using namespace std; int ans,len,p; int read(){ int f=1,x=0;char ch=getchar(); while(isdigit(ch)==0 && ch!='-')ch=getchar(); if(ch=='-')f=-1,ch=getchar(); while(isdigit(ch))x=x*10+ch-'0',ch=getchar(); return x*f; } void write(int x){ int ff=0;char ch[15]; while(x)ch[++ff]=(x%10)+'0',x/=10; if(ff==0)putchar('0'); while(ff)putchar(ch[ff--]); putchar(' '); } typedef struct node{ int to[30],dis,fa; }spot; struct SAM{ spot x[maxn*2]; int cnt,rt,lst; char s[maxn]; void start(){ lst=rt=++cnt; scanf("%s",s+1); int ls=strlen(s+1); for(int i=1;i<=ls;i++) extend(i); } void extend(int pos){ int val=s[pos]-'a',p=lst,np=++cnt; lst=np,x[np].dis=pos; for(;p&&x[p].to[val]==0;p=x[p].fa)x[p].to[val]=np; if(p==0)x[np].fa=rt; else{ int q=x[p].to[val]; if(x[q].dis==x[p].dis+1)x[np].fa=q; else{ int nq=++cnt; x[nq].dis=x[p].dis+1; memcpy(x[nq].to,x[q].to,sizeof(x[q].to)); x[nq].fa=x[q].fa,x[np].fa=x[q].fa=nq; for(;x[p].to[val]==q;p=x[p].fa)x[p].to[val]=nq; } } } }t; int main(){ char s2[maxn]; t.start(); scanf("%s",s2+1); int ls2=strlen(s2+1); p=t.rt; for(int i=1;i<=ls2;i++){ int val=s2[i]-'a'; if(t.x[p].to[val])len++,p=t.x[p].to[val]; else{ while(p&&t.x[p].to[val]==0)p=t.x[p].fa; if(p==0)p=t.rt,len=0; else len=t.x[p].dis+1,p=t.x[p].to[val]; } ans=max(ans,len); } write(ans); return 0; }
其實是對勁的后綴自動機
根據以上的介紹,可以發現后綴自動機是由拓撲圖和樹組成的,也就是說樹上和拓撲圖上的dp好像就……
宣傳一波電教,歡迎加入。