后綴數組是一個思路較為清晰,代碼十分玄學的操作,建議大家按照代碼模擬一下樣例,理解每一步操作的意義
后綴數組的作用是將長度為N的字符串的N個后綴來進行排序
我們直接排序的復雜度是\(O(N^2logN)\)
后綴數組常用方法是倍增+基數排序算法:
1.基數排序
我們先來看一下代碼:(默認升序排列)
//rep(i, a, b)是正序從a-b枚舉
//drep(i, a, b)是倒序從a-b枚舉
//a數組為基數排序輔助數組,即為一個桶
//rk數組為基數排序的第一關鍵字
//tp第二關鍵字中,排名為i的數的位置
//sa數組可以在此處理解為排完序后排名為i的數對的位置
il void Qsort() {
rep(i, 1, m) a[i] = 0;//
rep(i, 1, n) ++ a[rk[i]];
rep(i, 1, m) a[i] += a[i - 1];
//記錄前綴和以后,a[i]的意義是rak為i的數最大可以到哪里
drep(i, 1, n) sa[a[rk[tp[i]]] --] = tp[i];
//-- a的意義是減少了一個位置,所以a最大可以到的位置往前移一個
}
我們來模擬一下,假設我們要對一個數組進行基數排序
其中第一關鍵字為:\(1\ 3\ 2\ 1\ 4\ 3\ 1\ 2\)
對應第二關鍵字為:\(3\ 2\ 1\ 2\ 3\ 3\ 1\ 3\)
所以對應\(tp\)數組為:\(3\ 7\ 2\ 4\ 1\ 5\ 6\ 8\)
第一步
我們清空桶
第二步
我們將第一關鍵字壓入桶中,得到a數組:\(3\ 2\ 2\ 1\)
第三步
我們將a數組記錄前綴和,得到a數組:\(3\ 5\ 7\ 8\)
然后我們就可以發現一個奇妙的性質
對與第一關鍵字為1的數對,他們的排名為\(1-3\)
對與第一關鍵字為2的數對,他們的排名為\(4-5\)
對與第一關鍵字為3的數對,他們的排名為\(6-7\)
對與第一關鍵字為4的數對,他們的排名為\(8-8\)
所以從某種意義上來說,我們已經對第一關鍵字排好了序
第四步
我們從前往后倒序枚舉
首先,最后一個數對的第二關鍵字為8,第8個數對的第一關鍵字為2,2的桶現在為5,所以排名為5的位置是第8個
然后2的桶減一,因為第五個位置已經被占用,所以第一關鍵字為2的數對排名為\(4-4\)
第7個數對的第二關鍵字為6,第六個數的第一關鍵字的桶為7個,於是排名為7的位置是第6個
以此類推,我們可以拍好序,最后的sa數組為\(7\ 4\ 1\ 3\ 8\ 2\ 6\ 5\)
2.倍增
定義:
\(1.\ <a, b>\)為以a為第一關鍵字,b為第二關鍵字進行基數排序)
\(2.\ s[i]\)表示原數組的第i位字符
\(3.\ i-\)后綴表示對字符串S的每個后綴,取左邊i個字符,得到一個i-后綴
\(4.\ rk[i][j]\)表示第j位上的i-后綴的排名
操作:
我們可以先按\(<i, s[i]>\)進行基數排序,得到\(rk[1][i]\)
再按照\(<rk[1][i], rk[1][i + 1]>\)進行基數排序,得到\(rk[2][i]\)
因為是倍增,所以我們可以通過\(<rk[2][i], rk[2][i + 2]>\)進行基數排序,得到\(rk[4][i]\)
同理,我們也可以用\(<rk[4][i], rk[4][i + 4]>\)進行基數排序,得到\(rk[8][i]\)
當所有\(rk[2^k][i]\)互不相同,排序結束
代碼如下(倍增部分代碼有詳細注釋,就不模擬了,各位最好手動模擬,理解每一行代碼,注意在模擬的時候分清楚rk[i]和sa[i]的區別,一定要先清楚每一個數組的意義!!!):
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<cstdlib>
using namespace std;
#define il inline
#define re register
#define debug printf("Now is Line : %d\n",__LINE__)
#define file(a) freopen(#a".in","r",stdin);freopen(#a".out","w",stdout)
il int read() {
re int x = 0, f = 1; re char c = getchar();
while(c < '0' || c > '9') { if(c == '-') f = -1; c = getchar();}
while(c >= '0' && c <= '9') x = x * 10 + c - 48, c = getchar();
return x * f;
}
#define rep(i, s, t) for(re int i = s; i <= t; ++ i)
#define drep(i, s, t) for(re int i = t; i >= s; -- i)
#define _ 1000005
int n, m, a[_], sa[_], rk[_], tp[_];
char c[_];
/*
sa[i]:排名為i的后綴的位置
rk[i]:第i個位置開始的后綴的排名,作為基數排序的第一關鍵字
即sa[rk[i]] = rk[sa[i]] = i
tp[i]:第二關鍵字中,排名為i的數的位置
a[i]:有多少個元素排名為i
c[i]:原輸入數組
*/
il int get(char c) {
if(c >= '0' && c <= '9') return c - '0' + 1;
if(c >= 'A' && c <= 'Z') return c - 'A' + 11;
return c - 'a' + 37;
}
il void init() {
rep(i, 1, n) rk[i] = get(c[i]), tp[i] = i;
}
il void print() {
rep(i, 1, n) printf("%d ", sa[i]);
}
il void Qsort() {
rep(i, 1, m) a[i] = 0;
rep(i, 1, n) ++ a[rk[i]];
rep(i, 1, m) a[i] += a[i - 1];
drep(i, 1, n) sa[a[rk[tp[i]]] --] = tp[i];
}
il void get_sort() {
for(re int w = 1, p = 0; p < n && w <= n; m = p, p = 0, w <<= 1) {
//p在此時的意義是最高出現的排名以及一個計數器
//p < n的意義是如果當前最大排名== n則無繼續排序的意義
rep(i, n - w + 1, n) tp[++ p] = i;
//這里p的定義只是一個計數器
//tp[i]表示第二關鍵字中,排名為i的數的位置
//因為i - w + 1后面沒有第二關鍵字,所以要補0
//所以要設成極小值排在前面
rep(i, 1, n) if(sa[i] > w) tp[++ p] = sa[i] - w;
//在第一關鍵字中排名越靠前,表示應該排在前面
Qsort(), swap(rk, tp), p = rk[sa[1]] = 1;
//我們現在更新rk,tp已經無用,先備份,用memcpy也行
//注意到一個性質 : rk[sa[i]] = i
rep(i, 2, n) rk[sa[i]] = (tp[sa[i]] == tp[sa[i - 1]] && tp[sa[i] + w] == tp[sa[i - 1] + w]) ? p : ++ p;
//注意tp和rk已經交換,所以這個判斷的意思是:
//如果兩個后綴還相等,則排名不變,否則++
}
}
int main() {
scanf("%s", c + 1), n = strlen(c + 1), m = 62;
init(), Qsort(), get_sort(), print();
return 0;
}