后綴數組(SA)總結


后綴數組(SA)總結

這個東西鴿了好久了,今天補一下

概念

后綴數組\(SA\)是什么東西?

它是記錄一個字符串每個后綴的字典序的數組

\(sa[i]\):表示排名為\(i\)的后綴是哪一個。

\(rnk[i]\):可以理解為\(SA\)數組的逆,記錄后綴\(i\)的排名是多少,\(rnk[SA[i]]=i\)

\(lcp[i]\):別人一般叫\(height\),表示后綴\(SA[i]\)\(SA[i-1]\)的最長公共前綴的長度。

后綴排序

求出后綴數組的算法,模板題

代碼

先上代碼,便於理解

#define cmp(i, j, k) (y[i] == y[j] && y[i + k] == y[j + k])
void Get_SA() {
    static int x[MAX_N], y[MAX_N], bln[MAX_N];
	int M = 122; 
	for (int i = 1; i <= N; i++) bln[x[i] = a[i]]++; 
	for (int i = 1; i <= M; i++) bln[i] += bln[i - 1]; 
	for (int i = N; i >= 1; i--) sa[bln[x[i]]--] = i; 
	for (int k = 1; k <= N; k <<= 1) { 
		int p = 0; 
		for (int i = 0; i <= M; i++) y[i] = 0; 
		for (int i = N - k + 1; i <= N; i++) y[++p] = i; 
		for (int i = 1; i <= N; i++) if (sa[i] > k) y[++p] = sa[i] - k;
		for (int i = 0; i <= M; i++) bln[i] = 0; 
		for (int i = 1; i <= N; i++) bln[x[y[i]]]++; 
		for (int i = 1; i <= M; i++) bln[i] += bln[i - 1]; 
		for (int i = N; i >= 1; i--) sa[bln[x[y[i]]]--] = y[i]; 
	    swap(x, y); x[sa[1]] = p = 1;
		for (int i = 2; i <= N; i++) x[sa[i]] = cmp(sa[i], sa[i - 1], k) ? p : ++p;
		if (p >= N) break;
		M = p; 
	} 
} 

算法流程

\(sa\)的算法有倍增法和\(DC3\),因為后者有碼量大、常數大、我不會等種種缺點,

這里只介紹倍增算法。

我們如果對於每個倍增完的二元組,每個都\(sort\)一下,復雜度是\(O(nlog^2)\)的。

那么將基數排序應用到其中去,就可以做到\(O(nlogn)\),具體做法:

我們考慮一下普通的基數排序是怎么排二元組

先將第二位丟進桶里,然后按照第一維的次序取出。

那么這個字符串怎么排呢?

首先當\(k=0\)時,我們直接桶排一下就行了。

但是我們還要接着排啊,

還記得吧,基排序是先按照第二維從小往大排

那么,我們就先把第二維的順序搞出來

首先最小的一定就是沒有第二維的東西

所以我們先把這些數直接丟進數組里面

接下來就是有第二維的東西啦

\(i\)位的第二維是啥?\(rnk[i+k]\)

所以,從小到達枚舉\(sa\),這樣保證第二維從小往大

那么,只要\(sa[i]>k\)

就證明它是一個東西的第二維

所以,把\(sa[i]−k\)

丟到數組里面去就好啦

這樣的話,按照第二維就排好啦

再來依次按照第一維丟到桶里面去

做一遍基數排序就好啦

這樣就能夠求出\(sa\)

看起來很簡單誒。。

只是數組不要搞混了

一定搞清楚每個數組是干啥的

比如我的代碼

\(sa\)是后綴數組,\(sa[i]\)表示排名為i的串是哪一個

\(rnk[i]\)相當於排名,\(rnk[i]\)表示第i個串的排名

\(x,y\)兩個數組是記錄順序的

分別記錄第一維和第二維的排序的順序

\(bln\)是桶。

如果實在理解不了,就背吧,反正也沒有多長

那么\(lcp\)數組怎么求呢?

\(\forall i<j\),不妨設\(rnk[j]<rnk[k]\),那么以\(j\)開頭的后綴和\(k\)開頭的后綴的最長公共前綴就是\(\min _{i=rnk[j]+1}^{rnk[k]} lcp[i]\)

有一個引理:

定義\(h[i]=lcp[rnk[i]]\),那么,\(h[i]\geq h[i-1]-1\)

證明:設\(s[k...]\)為排在\(s[i-1...]\)的前一名的后綴,其最長公共前綴為\(h[i-1]\),則\(s[k+1...]\)\(s[i...]\)的最長公共前綴顯然大於等於\(h[i-1]-1\),原結論得證。

然后這樣求就可以了:

    for (int i = 1; i <= N; i++) rnk[sa[i]] = i; 
    for (int i = 1, j = 0; i <= n; i++) { 
        if (j) j--; 
        while (a[i + j] == a[sa[rnk[i] - 1] + j]) ++j; 
        lcp[rnk[i]] = j; 
    }

一些trick

總結了一些食用SA時的\(trick\)

一、對於可重復的最長重復子串問題(若子串\(s\)重復出現次數大於等於二,則稱重復子串)\(Ans=\max_{i=1}^nlcp_i\)

二、對於不可重疊的最長重復子串問題,二分,將問題轉化為是否有兩個長度為\(k\)的子串是相同的,且不重疊。將\(lcp\)數組分組,最長公共前綴不小於\(k\)的為一組其中如果有一組\(sa[i]\)之差大於\(k\)時,則成
立。

三、對於可重疊的重復\(k\)次最長重復子串,與上一種方法思路相似,二分,問題轉化為判斷是否存在\(k\)個長度為\(l\)的子串是相同的,將最長公共子串大於\(l\)的后綴分為一組,查看每一組內后綴個數是否大於\(k\)

四、對於多個字符串的問題,通常用一個原串中不會出現的字符將兩個字符串連接為一個。對於最長公共子串問題,首先將兩個字符串用一個未出現過的字符連接起來,然后求出它們的最長公共前綴,解時注意判斷是否在間隔符兩邊。

五、求取長度不小於\(k\)的公共子串個數時,將兩個字符串按照上述方法連接,中間用一個未曾出現過的字符隔開,計算所有后綴之間最長公共前綴的長度,用單調棧維護最長公共前綴的長度。

六、對於在多個字符串中,出現不小於\(k\)個字符串的最長公共子串。按照上述方法連接多個字符串后,使用二分法。對於給定的長度,先分組,判斷每組字符串后綴是否出現在不同的\(k\)個字符串中。

七、對於在每個字符串中至少出現兩次且不重疊的最長公共子串時,按照上述方法連接多個字符串,使用二分法。對於給定的長度,先分組,判斷是否有一組包含每個字符串中的兩個不重疊答案。

一些后話

我還不太熟悉,題目暫未整理出來。

以后會提供每個\(trick\)的例題及一些題單。

如有錯漏之處,請聯系作者。

參考文章:
yyb的博客 https://www.cnblogs.com/cjyyb/p/8335194.html
清華大學出版社《ACM/ICPC算法基礎訓練教程》第8章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM