淺談Suffix Automaton(後綴自動機)

這是一個強大的automaton——Suffix Automaton==>我學過最強大,最牛犇,最難理解的自動機

現在給你一個問題:

給定一個字符串,要求這個字符串所有子串出現的次數分別是多少

樸素算法

①枚舉左端點,枚舉右端點,用hash記錄一下,統計個數。(注意最好雙hash,保證正確率)
預計時間複雜度:O(n2)O(n^2)
②可以直接開trie記錄,以所有後綴建,就好像這樣:aabbabd
這裏寫圖片描述
每次建的時候就記錄每個地方的次數,因爲有n2n^2個點,那麼時間複雜度也就是O(n2)O(n^2)

n2n^2複雜度很優秀,但是…

n<=100000啊啊啊啊啊…

這樣做不了,怎麼辦?!

這時我們引進Suffix Automaton(後綴自動機)

(集中精神,前方高能)

後綴自動機定義

一個串SS的後綴自動機(SAM)(SAM)是一個有限狀態自動機(DFA)(DFA),它能且只能接受所有SS的後綴(當然,它能做的事情遠遠不僅限於後綴).

後綴自動機其實是一個DAGDAG(有向無環圖),其中頂點是狀態,邊代表了狀態之間的轉移.

某一狀態SS被稱爲初始狀態,由它能夠到達其餘所有狀態.

自動機的所有轉移,都是一條有向邊,且都被某種符號標記,從某一狀態出發的所有轉移必須擁有不同的標記.

一個狀態被稱爲終止狀態,表示如果我們從初始狀態SS經任意一條路徑走到某一終止狀態,並順序寫出經過邊的標記,得到的字符串必定是原串的後綴.

在符合上述條件的所有自動機中,後綴自動機擁有最少的狀態與轉移,並且後綴自動機的狀態數以及轉移數都是O(S)O(|S|)的.

Pre的定義

可以說,pre是後綴自動機中最核心的東西,也是最難懂的部分。所以認真看!
pre[x]表示字符串[Sx]的最長後綴[Spre[x]]的右端點,例如:
這裏寫圖片描述
綠色的虛線表示pre邊。很顯然我們可以發現3的pre邊連向的是[S3]的最長後綴[S1]的右端點1。

Pre的連邊

對於剛剛加入的now點,尋找las的pre邊,如果沒有一條與當前新加的這條邊相同的邊就加上一條這樣的邊,直到找到一個有與當前新加的這條邊相同的邊爲止。然後分爲兩種情況:我們設當前找到的這個點爲p,它連向的兒子爲q。
這裏寫圖片描述
①如果p,q兩點距離爲1,那麼就可以直接把當前新節點的pre邊指向q。
②如果p,q兩點距離大於1,我們會發現如果把當前新節點的pre邊指向q,不符合[Sq]爲[Snow]的後綴(如下圖)
這裏寫圖片描述
(ab並不是abb的後綴)
我們分析這樣做錯誤的原因,因爲我們把原串中的ab當成了b(就是連在下面的那條邊),所以把ab誤認爲成了abb的後綴,那麼我們想,我們呢原來是想連b爲後綴的,所以這是我們需要進行加點操作。

加點操作

我們在p以後加入一個點,與p及以前所有與q用當前符號爲邊相連的點,與新加入的點連上一條與當前符號相同的邊,例如上圖是b,因爲這個點在原串中是不存在的,它只是q的一個分身,所以q指出去的所有邊它也都要連,然後新加的點的pre邊自然就變成了q原來的pre邊,然後q和now的pre邊也就指向了當前新加節點:
這裏寫圖片描述
對於“aabbabd”來說它的自動機長這樣:
這裏寫圖片描述

時間複雜度

時間複雜度是:O(n)O(n).
最大的狀態數是2n-1,因爲除了前面三個點,後面的所有點都可以加點。
#應用

1.給定字符串T,每次詢問一個p,問p是否爲T的子串.

  • 對T建一個後綴自動機.
  • 每次詢問就從初始狀態ss開始走,然後沿着詢問的字符串走.
  • 時間複雜度O(T+p)O(T+p)O(|T|+∑ |p|)O(|T|+∑ |p| )

2.給定字符串S,問它有多少不同的子串.

  • 還是先建一個後綴自動機.
  • 那麼對於後綴自動機中的任何一條路徑,都是一個不同的子串.
  • 所以答案就是從S開始出發的不同路徑數.

3.給定字符串S,每次詢問S的所有不同子串中字典序第k小.

  • 和前兩問類似,我們只需要處理從一個狀態開始,每種字符的路徑條數.
  • 然後從起點S一直找就好了.

4.給定字符串S,找到和它循環同構的字典序最小的字符串.

  • 我們對字符串S+S做一個後綴自動機,然後貪心的找字典序最小就好啦~

5.給定多個字符串,求它們的最長公共子串.

  • 思考…

模板Code

#include<cstdio>
#include<iostream>
#include<cstring>
#define maxN 2000010
using namespace std;
int son[maxN][27],pre[maxN],len[maxN],sum[maxN];
int sz,las,lens,now,q,p;
char s[maxN];
void add(int x)
{
	len[++sz]=len[las]+1,sum[sz]=1,now=sz;
	for (p=las;p&&!son[p][x];p=pre[p]) son[p][x]=now;
	if(p)
	{
		q=son[p][x];
		if(len[q]>1+len[p])
		{
			len[++sz]=len[p]+1;
			memcpy(son[sz],son[q],sizeof(son[q]));
			pre[sz]=pre[q];pre[q]=pre[now]=sz;
			for (;son[p][x]==q;p=pre[p]) son[p][x]=sz;
		}
		else pre[now]=q;
	}
	else pre[now]=1;
	las=now;
}
int main()
{
	scanf("%s",s+1);
	lens=strlen(s+1);sz=las=1;
	for (int i=1;i<=lens;++i) add(s[i]-96);
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章