后綴數組(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章
