基本應用
讀入一個長度為 $ n $ 的由大小寫英文字母或數字組成的字串,請把這個字串的所有非空字尾按字典序(用 ASCII 數值比較)從小到大排序。
解法
1.將每個字尾取出來,直接排序 \(O(n^2 \log n)\)
2.用hash二分LCP比較下一位,\(O(n \log^2 n)\)
3.倍增求字尾陣列,\(O(n \log n)\)
4.高階方法求字尾陣列,\(O(n)\)
倍增
先比較每個字尾的第一位,再比較前兩位,前四位...
問題在於如何快速比較前兩位,前四位。
一個有趣的性質是在比較\(2^k\)位時,我們知道\(2^{k-1}\)位的大小,所以\(2^k\)位的大小隻與前一半\(2^{k-1}\)和後一半\(2^{k-1}\)有關,所以可以用基數排序由上一層推到這一層。
基數排序
正常基數排序,是按數位從高到低依次比較大小,比如說三位數,就先比較百位的數字,將百位為 \(0\) 的放在一起,將百位為 \(1\) 的放在一起...。然後,對十位進行比較,在百位為 \(0\) 的裡面把十位為 \(0\) 的放在一起,十位為 \(1\) 的放在一起...,最後所有數都有序。
SA的基數排序,就是相當於只有兩位數來排序。
程式碼實現
程式碼比較抽象要多理解,多思考
點選檢視程式碼
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m,sa[N],rk[N],x[N],y[N],cnt,num;
char s[N];
void SA()
{
for(int i=1;i<=n;i++)rk[x[i]=s[i]]++;//rk輔助陣列,x是上一層的排名
for(int i=1;i<=m;i++)rk[i]+=rk[i-1];
for(int i=n;i>=1;i--)sa[rk[x[i]]--]=i;//正序倒序都可以,sa是排名為i的字尾的起始下標
for(int k=1;k<=n;k<<=1)
{
cnt=0;
for(int i=n-k+1;i<=n;i++)y[++cnt]=i;//沒有後一半是最強的,最靠前的
for(int i=1;i<=n;i++)if(sa[i]>k)y[++cnt]=sa[i]-k;//如果可以做後一半,就做
//正序列舉,因為y的順序是後一半從小到大的順序
for(int i=1;i<=m;i++)rk[i]=0;//清零
for(int i=1;i<=n;i++)rk[x[i]]++;//根據前一半
for(int i=1;i<=m;i++)rk[i]+=rk[i-1];
for(int i=n;i>=1;i--)sa[rk[x[y[i]]]--]=y[i],y[i]=0;//後一半更大的在前一半相同時排後面
swap(x,y);//y臨時存一下上一層x的值。
x[sa[1]]=1,num=1;
for(int i=2;i<=n;i++)
{
x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?num:++num;//確定這一層的排名
}
if(num==n)break;//分完了
m=num;
}
for(int i=1;i<=n;i++)cout<<sa[i]<<' ';
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>s+1;
n=strlen(s+1),m=150;
SA();
return 0;
}