後綴數組(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]\)
\(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章
後綴數組(SA)總結