這個東西鴿了很久了,今天補一下html
後綴數組\(SA\)是什麼東西?算法
它是記錄一個字符串每一個後綴的字典序的數組數組
\(sa[i]\):表示排名爲\(i\)的後綴是哪個。spa
\(rnk[i]\):能夠理解爲\(SA\)數組的逆,記錄後綴\(i\)的排名是多少,\(rnk[SA[i]]=i\)。code
\(lcp[i]\):別人通常叫\(height\),表示後綴\(SA[i]\)與\(SA[i-1]\)的最長公共前綴的長度。htm
求出後綴數組的算法,模板題blog
先上代碼,便於理解排序
#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; }
總結蒯了一些食用SA時的\(trick\):
1、對於可重複的最長重複子串問題(若子串\(s\)重複出現次數大於等於二,則稱重複子串)\(Ans=\max_{i=1}^nlcp_i\)。
2、對於不可重疊的最長重複子串問題,二分,將問題轉化爲是否有兩個長度爲\(k\)的子串是相同的,且不重疊。將\(lcp\)數組分組,最長公共前綴不小於\(k\)的爲一組其中若是有一組\(sa[i]\)之差大於\(k\)時,則成
立。
3、對於可重疊的重複\(k\)次最長重複子串,與上一種方法思路類似,二分,問題轉化爲判斷是否存在\(k\)個長度爲\(l\)的子串是相同的,將最長公共子串大於\(l\)的後綴分爲一組,查看每一組內後綴個數是否大於\(k\)。
4、對於多個字符串的問題,一般用一個原串中不會出現的字符將兩個字符串鏈接爲一個。對於最長公共子串問題,首先將兩個字符串用一個未出現過的字符鏈接起來,而後求出它們的最長公共前綴,解時注意判斷是否在間隔符兩邊。
5、求取長度不小於\(k\)的公共子串個數時,將兩個字符串按照上述方法鏈接,中間用一個不曾出現過的字符隔開,計算全部後綴之間最長公共前綴的長度,用單調棧維護最長公共前綴的長度。
6、對於在多個字符串中,出現不小於\(k\)個字符串的最長公共子串。按照上述方法鏈接多個字符串後,使用二分法。對於給定的長度,先分組,判斷每組字符串後綴是否出如今不一樣的\(k\)個字符串中。
7、對於在每一個字符串中至少出現兩次且不重疊的最長公共子串時,按照上述方法鏈接多個字符串,使用二分法。對於給定的長度,先分組,判斷是否有一組包含每一個字符串中的兩個不重疊答案。
我還不太熟悉,題目暫未整理出來。
之後會提供每一個\(trick\)的例題及一些題單。
若有錯漏之處,請聯繫做者。
參考文章:
yyb的博客 http://www.javashuo.com/article/p-tfxihtst-bo.html 清華大學出版社《ACM/ICPC算法基礎訓練教程》第8章