後綴數組詳解

基本概念

什麼是後綴

假如你有一個字符串如c++

"gzyorz"算法

它的後綴是ubuntu

"gzyorz","zyorz","yorz","orz","rz","z"數組

很簡單。
\(suff[i]\)表示以第\(i\)位爲開頭的後綴。學習

大小比較

給兩個字符串,讓你比較大小,從頭開始,一位一位的比,若是不相等,就比較兩個字符的那個字典序比較大,若是一個串已經到結尾了,它們仍是相等,那長的那個大。
好比spa

"aab"和"aac"
第一位'a'='a',第二位'a'='a',第三位'b'<'c',因此"aab"<"aac"。

"aab"和"aabc"
第一二三位均相等,但"aabc"比"aab"長,因此"aab"<"aabc"。code

後綴數組和名次數組

拿網上一張十分直觀的圖
5c8756bf13571.png
後綴數組\(sa[i]\):表示全部後綴在排完序後,排名爲\(i\)的後綴在原串中的位置。
名次數組\(rank[i]\):表示全部後綴在排序完後,原字符串中第\(i\)名如今的排名。
總結一下
sa表示「排名第幾的是誰」,rank表示"排名第幾"
這裏sa存的是排名第i後綴的開頭的位置
這二者是能夠在\(O(n)\)的時間內互推出來的。blog

rnak[sa[i]] = i;
sa[rank[i]] = i;

顯然,\(x\)的排名是\(y\),那排名是\(y\)的就是\(x\)排序

求後綴數組

構造sa數組的方法通常有兩種:圖片

  1. 倍增算法:\(O(nlogn)\)
  2. DC3算法:\(O(n)\)
    這裏只講一下倍增算法。

對於一個後綴\(suff[i]\),直接求\(rank\)比較困難,咱們用倍增的思想,成倍的兩兩合併出全部的後綴,用第\(k-1\)輪的\(rank\)推出第\(k\)輪的\(rank\)
咱們第\(k\)輪的\(s[i...i+2^k]\)能夠看作是\(s[i...i+2^{k-1}]\)\(s[i+2^{k-1}+1...2^k]\)拼起來的,而這兩個長度爲\(2^{k-1}\)的字符串是上一輪處理出來的,咱們知道他們的\(rank\),這就至關於兩組數字(關鍵字)比較大小,這樣,咱們就得到了第\(k\)\(s[i...i+2^k]\)\(rank\)
若是\(i\)位置後沒有\(2^{k-1}\)個字符,就是\(s[i...2^{k-1}]\)不能由上面兩個字符串拼起來,代表\(i+2{k-1}\)大於等於\(len\),也就是\(suff[i]\)這個字符串,直接補0。

因此,咱們獲得\("aabaaaab"\)\(rank\)的過程大概就是這樣。

怎麼比較大小呢
舉個栗子:
未命名.bmp
如圖,咱們要比較\(str1\)\(str2\)的大小,顯然咱們只須要比較\(f1\)\(f2\)的大小(第一關鍵 字),\(g1\)\(g2\)的大小就能夠判斷\(str1\)\(str2\)的大小(第二關鍵字)。
顯然這樣作的複雜度是\(O(log(len))\)

基數排序

咱們每次把子串合併後都要排一次序,若是直接上快排的話,\(O(len log^2 (len))\),顯然不行啊。

這就用到了\(O(len)\)的基數排序。
所謂基數排序,就是從最低位開始,先按個位排,再排十位,再排百位……
這裏給張圖感性理解一下,建議仍是深度的學習一下,對下文的代碼也好理解。
161837176365265.jpg

代碼

代碼仍是頗有必要解釋一下的
若是學了基數排序的話仍是基本很好理解的。

int fir[N], sce[N], t[N], sa[N];
//fir第一關鍵字(rank)
//sec第二關鍵字(sa)
//排名爲i的串出現了多少次(桶)
for (int i = 1; i <= len; ++i) ++t[fir[i] = s[i]];		//把每一個字符放入桶內 
for (int i = 1; i <= num; ++i) t[i] += t[i - 1];		//前綴和一下求當前字符的排名
for (int i = len; i >= 1; --i) sa[t[fir[i]]--] = i;	
	/*	這裏枚舉到i位置時,s[i] (fir[i])的排名是t[fir[i]],那排名爲t[fir[i]]的字符串開頭的位置顯然爲i 
		->  sa[rank[i]] = i
	*/

就是第一輪在沒有第二關鍵字的時候把全部的字母排一遍序。
利用前綴和能夠快速的定位出每一個位置應有的排名。
這裏稍微模擬一下應該很好理解。

for (int i = len - k + 1; i <= n; ++i) sec[++cnt] = i;		
for (int i = 1; i <= len; ++i) if (sa[i] > k) sec[++cnt] = sa[i] - k;

第一行:由於這一部分的長度小於\(k\),因此沒有第二關鍵字,直接排到最前面好了,\(sec[i]\)記錄的是排名第\(cnt\)的後綴的開頭在\(i\)位置。

第二行:看排名爲\(i\)的後綴的位置是否大於\(k\),位置要大於\(k\),當前找的字符串是由兩個長度爲\(k\)的子串拼起來的,若是\(i\)位置小於\(k\),這個後綴就不能做爲第二關鍵字了。
而後直接把上一輪的\(sa\)拿過來用就能夠了,同時減去一個數後相對排名不變,必定要時刻記住\(sec\)存的是排名爲\(cnt\)的後綴的位置,咱們知道第二關鍵字排名第\(i\)的後綴的位置,這樣就獲得了以第二關鍵字的排名。

for (int i = 1; i <= num; ++i) t[i] = 0;
for (int i = 1; i <= len; ++i) ++t[fir[i]];
for (int i = 1; i <= num; ++i) t[i] += t[i - 1];
for (int i = len; i >= 1; --i) sa[t[fir[sec[i]]]--] = sec[i], sec[i] = 0;

這個是把第一二關鍵字總的排名弄出來。
\(fir\)數組中存的是上次關鍵字的\(rank\),即第一關鍵字,對\(fir\)排序就是對第一關鍵字排序,那第二關鍵字呢。
由於第一關鍵字可能對應不少第二關鍵字(由於有的串可能能是後綴,有的是長度爲\(2^{k-1}\)的串,可能相同),咱們要在第一關鍵字相同的狀況下排第二關鍵字,由於第二關鍵字已經排好,越大的確定越靠後。
好比\(sec[1]=3\),\(sec[2]=4\)那4位置開始的後綴要比3位置開始的後綴靠後
\(sec[i]\)是第二關鍵字排名爲\(i\)的後綴(sa數組定義)。
\(fir[sec[i]]\)就是排名爲\(i\)的第二關鍵字對應的第一關鍵字。
\(t[fir[sec[i]]]\)就表示當第一關鍵字相同時,第二關鍵字較大的這個後綴的排名是多少。
理同上面的基數排序,\(sa[t[fir[sec[i]]]--] = sec[i]\)

swap(fir, sec);
fir[sa[1]] = 1, cnt = 1;
for (int i = 2; i <= n; ++i) 
	fir[sa[i]] = (sec[sa[i]] == sec[sa[i - 1]] && sec[sa[i] + k] == sec[sa[i - 1] + k]) ? cnt : ++cnt;
if (cnt == len) break;
num = cnt;

這裏,在下面更新\(fir\)的時候\(sec\)是沒有用的,因此swap一下直接把\(fir\)的值賦值給\(sec\),這時\(sec\)存的就是\(fir\)了。
\(sa[1]\)的排名必定是1,而後定義一個值,表示串的"值"。
若是兩個字符串的兩個關鍵字徹底相等,則新的"值"也相等。
若是全部的值都不同,就說明排好序了。
關鍵字的取值範圍就發生了變化,變爲了\(cnt\)

完整代碼:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int num = 122, len;
int fir[N], sec[N], t[N], sa[N];
char s[N];
inline void SA() {
    for (int i = 1; i <= num; ++i) t[i] = 0; 
	for (int i = 1; i <= len; ++i) ++t[fir[i] = s[i]];
	for (int i = 1; i <= num; ++i) t[i] += t[i - 1];
	for (int i = len; i >= 1; --i) sa[t[fir[i]]--] = i;	
	for (int k = 1; k <= len; k <<= 1) {
		int cnt = 0;
		for (int i = len - k + 1; i <= len; ++i) sec[++cnt] = i;
		for (int i = 1; i <= len; ++i) if (sa[i] > k) sec[++cnt] = sa[i] - k;
		for (int i = 1; i <= num; ++i) t[i] = 0;
		for (int i = 1; i <= len; ++i) ++t[fir[i]];
		for (int i = 1; i <= num; ++i) t[i] += t[i - 1];
		for (int i = len; i >= 1; --i) sa[t[fir[sec[i]]]--] = sec[i], sec[i] = 0;
		swap(fir, sec);
		fir[sa[1]] = 1, cnt = 1;
		for (int i = 2; i <= len; ++i) 
			fir[sa[i]] = (sec[sa[i]] == sec[sa[i - 1]] && sec[sa[i] + k] == sec[sa[i - 1] + k]) ? cnt : ++cnt;
		if (cnt == len) break;
		num = cnt;
	}
}
int main() {
	scanf("%s", s + 1);
	len = strlen(s + 1);
	SA();
	for (int i = 1; i <= len; ++i) printf("%d ", sa[i]);
	return 0;
}

最長公共前綴——LCP

定義

height[i]:表示\(suff[sa[i]]\)\(suff[sa[i-1]]\)的最大公共前綴,也就是排名完後兩個相鄰的後綴的最長公共前綴。
h[i]:等於\(height[rank[i]]\)\(suff[i]\)和排序後在它前一名的後綴的最長公共前綴。
5c8756bf13571.png

height

性質\(h[i]\geq h[i-1]-1\)
證實:
\(suff[k]\)是排在\(suff[i - 1]\)前一名的後綴,則它們的最長公共前綴是\(h[i - 1]\)
未命名.PNG
5c8f7ee29d55e.png
在沒有公共前綴的時候\(h[i]\)\(0\)
若是\(h[i - 1] \leq 1\),那麼\(h[i] \geq 0\)顯然成立。
都去掉第一個字符,就變成\(suff[k + 1]\)\(suff[i]\)(兩個後綴長度均不爲0)。
2327e453-39e2-465c-8ff7-552eab3a0590.png
顯然,都去掉一個字符後\(suff[k+1]\)\(suff[i]\)的最長公共前綴是\(h[i-1]-1\)
因此\(suff[i]\)和在它前一名的後綴的最長公共前綴至少是\(h[i - 1] - 1\)

代碼

void Getheight() {
    int j, k = 0;   //目前height數組計算到k
    for (int i = 1; i <= len; i++) {
        if(k) k--;  //由性質得height至少爲k-1
        int j = sa[fir[i] - 1];   //排在i前一位的是誰
        while(s[i + k] == s[j + k]) k++;
        height[fir[i]] = k;
    }
}

對於一個字符串
定義\(LCP(i,j)=lcp(suff(sa[i]),suff(sa[j])\)
1.對任意\(1\leq i<j<k\leq n,LCP(I,k)=min\{LCP(I,j),LCP(j,k)\}\)
2.設\(i<j\)\(LCP(i,j)=min\{LCP(k-1,k)|i+1<=k<=j\}\)
而兩個排名不相鄰的最長公共前綴爲排名在它們之間的height的最小值
圖片1.png
一道求LCP的題
[AHOI2013]差別
在求出\(height\)數組以後,利用單調棧維護一個上升序列,獲得該位置到的左右端點的長度,兩長度相乘就是整個區間的長度,這個長度再乘上\(height[i]\)就是\(height[i]\)的貢獻。
代碼

相關文章
相關標籤/搜索