先介绍计数排序。思考一下桶排序,桶排序是不稳定的。计数排序相当于是稳定的桶排序,时间复杂度为\(O(值域)\)
设数组\(a\)的值域为\([1,n]\),数组\(c\)表示每个元素的数目(也就是桶),数组\(r[i]\)表示\(a[i]\)的排名(注意这个排名是稳定的,也就是说当有多个\(a[i]\)的时候,相对顺序不会变化),然后对\(c\)求前缀和,这样\(c[i]\)就表示小于等于\(i\)的数有多少个(下面的\(c\)都表示已经求了前缀和的\(c\))。然后倒序循环数组\(a\),循环到\(a[i]\)的时候,令\(r[i]=c[a[i]]\),并令\(c[a[i]]\)减一
正确性:假设有若干个\(k\),我们现在只关心\(k\)。那么我们知道,最终排完序的\(a\)数组,严格小于\(k\)的有\(c[k-1]\)个,也就是说\(k\)的排名最低是\(c[k-1]+1\),最高是\(c[k]\);而由于要稳定,所以原数组\(a\)中最后一个\(k\)的排名是\(c[k]\),最前面的一个\(k\)的排名是\(c[k-1]+1\);我们倒序循环,由于\(c\)求了前缀和,上述过程刚好帮我们完成了排序工作
接下来进入正题,介绍后缀数组。下文默认字符串下标从\(1\)开始,那么长度为\(n\)的字符串\(s\)一共有\(n\)个后缀,第\(i\)个后缀就是\(s[i...n]\)
后缀数组涉及三个数组:\(sa,rk,height\)。\(sa[i]\)表示\(s\)所有后缀中,排名为\(i\)的后缀是第几个后缀;\(rk[i]\)表示\(s\)的第\(i\)个后缀的排名;\(height[i]\)表示排名第\(i\)位的后缀与排名第\(i-1\)位的后缀的最长公共前缀(\(\text{lcp}\))
然后先求\(sa\)。下文的\(x[i]\)表示按照前\(k\)个字母排序的前提下,\(s\)的第\(i\)个后缀的离散化之后的值(若不同后缀的前\(k\)个字母相同,则离散化之后的值相同),\(m\)表示离散化值域,\(y[i]\)数组表示排名为\(i\)的是第几个后缀(最后也会作为辅助数组)
第一步:将所有后缀按照第一个字母稳定地排序(利用计数排序)
for(int i=1;i<=n;i++) c[x[i]=s[i]]++;
//这里由于字符集不大,所以桶的大小直接开成字符集大小,离散化按照字符离散化
for(int i=2;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i;i--) sa[c[x[i]]--]=i;
第二步:假设我们已经对所有后缀按照前\(k\)个字母稳定地排好了序,我们接下来将所有后缀按照前\(2k\)个字母稳定地排序。
外层循环:for (int k = 1; k <= n; k <<= 1)
我们先按照所有后缀的第\(k+1\) ~ \(2k\)个字母排序(这里不要求稳定),然后再按照所有后缀的第\(1\) ~ \(k\)个字母排序(这里要求稳定);由于不可能存在两个后缀相同,所以经过上面两次排序之后,得到的\(sa\)一定是按所有后缀按照前\(2k\)个字母稳定地排序得到的
先进行第一次排序:
对于\(\forall i∈[n-k+1,n],s[i...n]\)是没有前\(2k\)个字母的,由于空字符串的字典序小于任何字符串,所以我们直接按照顺序将其放入\(y\)中即可
int num = 0;
for (int i = n - k + 1; i <= n; i ++ ) y[ ++ num] = i;
对于\(i∈[1,n-k],s[i...n]\)的第\(k+1\) ~ \(2k\)个字母就是\(s[i+k...i+2k]\)(长度不够的用空字符串补齐),于是比较\(s[i...n]\)的第\(k+1\) ~ \(2k\)个字母就是比较\(s[i+k...i+2k]\),也就是\(s\)的第\(i+k\)个后缀的前\(k\)个字母,而此时我们的\(sa[i]\)刚好记录了按照前\(k\)个字母排序,排名为\(i\)的后缀,于是我们从小到大循环\(i\),若\(sa[i]>k\),则将\(sa[i]-k\)放入\(y\)数组即可
for (int i = 1; i <= n; i ++ )
if (sa[i] > k) y[ ++ num] = sa[i] - k;
再进行第二次排序:
首先按照前\(k\)个字母进行计数排序
for (int i = 1; i <= m; i ++ ) c[i] = 0;
for (int i = 1; i <= n; i ++ ) c[x[i]] ++ ;
for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];
for (int i = n; i; i -- ) sa[c[x[y[i]]] -- ] = y[i];
然后将\(y\)作为辅助数组记录所有后缀按照前\(k\)个字母排序的离散化数值
swap(x, y);
然后更新\(x\),让\(x[i]\)表示s的第\(i\)个后缀按照前\(2k\)个字母排序的离散化数值(注意此时\(y[i]\)表示的是\(s\)的第\(i\)个后缀按照前\(k\)个字母排序的离散化数值,所以\(s\)的第\(i\)个后缀的第\(k+1\) ~ \(2k\)个字母就是\(s\)的第\(i+k\)个后缀的前\(k\)个字母)
x[sa[1]] = 1, num = 1;
for (int i = 2; i <= n; i ++ )
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])
x[sa[i]] = num;
else x[sa[i]]=++num;
注意y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]
是不会越界的,因为
最后,如果离散化值域达到了\(n\),则说明排序完成,否则的话令\(m\)为离散化值域重复上述过程
if (num == n) break;
m = num;
上述倍增法的时间复杂度为\(O(n\log n)\)
求\(height\)见OI-wiki
补充一个\(\text{lcp}\)的性质:设\(i<j\),则\(\text{lcp}(i,j)=\overset{j}{\underset{k=i}{\min}}(\text{lcp}(i,k),\text{lcp}(k,j))\),其中\(k\)是任取的(证明见视频00:37:20);由这个性质不难得出,如果想求\(\text{lcp}(i,j)\)的话,求一下\(\text{lcp}(i,i+1),\text{lcp}(i+1,i+2),...,\text{lcp}(j-1,j)\)并取最小值就好了