1. 程式人生 > >後綴數組(SA)總結

後綴數組(SA)總結

all 元組 們的 logs 是否 void 次數 nlogn 答案

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

後綴數組(SA)總結