1. 程式人生 > 其它 >字尾陣列SA模板詳解

字尾陣列SA模板詳解

也許應該叫字尾排序,是求出\(sa[i]\)\(rk[i]\)的一種演算法。
\(sa[i]\)代表排名為i的字尾的開始位置。
\(rk[i]\)代表開始位置為\(i\)的字尾的排名。
這是實現比原理要複雜的演算法。
先求出\(sa[i]\)之後再求出\(rk[i]\)
考慮先求出長度為1時候的\(sa[i]\)陣列。這個時候\(sa[i]\)代表長度為1排名為i的子串的開始位置。然後倍增求長度數為2時候的\(sa[i]\)陣列。直到長度大於\(n\)(也就是字串的長度)(超出長度的地方就是空字元)。然後考慮如何用長度為x時的\(sa[i]\)陣列求長度為\(2x\)時的\(sa[i]\)

陣列。把長度為\(2x\)的串從中間劈開得到\(a\),\(b\)兩個串,\(a,\)\(b\)的排名已經知道其實就是做一遍以\(a\)排名,\(b\)排名為第一第二關鍵字的排序。
知道\(sa[i]\)之後求\(rk[i]\)易如反掌,它們就是權值和下標相反的兩個陣列。
下面是求sa陣列的程式碼。

void get_sa(){
	for(int i=1;i<=n;i++)c[x[i]=s[i]]++;
	for(int i=1;i<=m;i++)c[i]+=c[i-1];
	for(int i=n;i>=1;i--)sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1){
		int num=0;
		for(int i=n;i>=n-k+1;i--)y[++num]=i;//y[]第二關鍵字排名為num的第一關鍵字位置。 
		for(int i=1;i<=n;i++)if(sa[i]>k)y[++num]=sa[i]-k;
		for(int i=1;i<=m;i++)c[i]=0;
		for(int i=1;i<=n;i++)c[x[i]]++;
		for(int i=1;i<=m;i++)c[i]+=c[i-1];
		for(int i=n;i>=1;i--)sa[c[x[y[i]]]--]=y[i],y[i]=0;//第一第二關鍵字的基數排序 
		swap(x,y);
		num=0;
		for(int i=1;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(n==num)break;
		m=num;
	}
	for(int i=1;i<=n;i++)printf("%d ",sa[i]);
}

把程式碼拆開看

for(int i=1;i<=n;i++)c[x[i]=s[i]]++;
for(int i=1;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[x[i]]--]=i;

這裡是基數排序,但不完全是基數排序。
它的作用是求出上面提到的長度為1時的\(sa\)陣列。
\(s\)是原串陣列,\(c\)是基數排序時用的桶,\(x[i]\)\(i\)位置上的字元,\(m\)是編號的值域也就是桶的大小。
瞭解這個基數排序對了解字尾陣列的實現十分重要。
第一個\(for\)是把出現的字元計數。第二個\(for\)是求字首和。
考慮這個字首和陣列的意義是什麼?
\(c[i]\)

就是排名小於等於字元i的個數。換種說法\(c[i]\)是字元i的最大的排名。
假設桶長這樣

求完字首和之後長這樣

從小到大
第一個出現的字母排第一,第二個出現的字母有三個,所以最大排名是4。。。
最後一個for就是把這些字元的排名求出。
至於為什麼寫,為什麼要倒著迴圈,其實在求長度為1的串時並不重要。但之後以兩個關鍵字排序時很重要

		int num=0;
		for(int i=n;i>=n-k+1;i--)y[++num]=i;//y[num]第二關鍵字排名為num的第一關鍵字位置。 
		for(int i=1;i<=n;i++)if(sa[i]>k)y[++num]=sa[i]-k;

y陣列的意義在註釋中。

比如要求長度為4的串,以最後一個點為開始的串後半部分完全是空的,排序一定是最靠前的,第一個迴圈主要處理這種情況。這裡第一個迴圈正著反著迴圈都沒問題。
第二個迴圈處理剩下部分

	for(int i=1;i<=m;i++)c[i]=0;
	for(int i=1;i<=n;i++)c[x[i]]++;
	for(int i=1;i<=m;i++)c[i]+=c[i-1];
	for(int i=n;i>=1;i--)sa[c[x[y[i]]]--]=y[i],y[i]=0;

這個部分是基數排序。跟第一部分十分相似。
不過\(x\)陣列代表以i開始的串的編號。
這裡重點看最後一個迴圈,\(c[i]\)相當是求出了編號為\(i\)的串根據第一關鍵字排序的上界。
之後根據排好序的第二關鍵字倒著賦排名。
模擬一遍可以更好理解。

	swap(x,y);
	num=0;
	for(int i=1;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(n==num)break;
	m=num;

最後的部分就是根據排序結果賦給每個字尾新的編號。
當編號數位\(n\)時代表已經對所有的\(n\)個字尾完成了排序,程式就可以結束了。

\(height\)陣列是常用的輔助陣列(做做題就知道了)
\(height[i]\)代表排名為i的字尾與排名為\(i-1\)的字尾的最長公共字首。
為了不產生混淆,寫成\(height[rk[i]]\)
如何求這個輔助陣列?
直接求並不好求,但是有這個性質:\(height[rk[i]]=>height[rk[i-1]]-1\)
\(height[rk[i-1]]<=1\)時這個式子顯然成立。
考慮\(height[rk[i-1]]>1\)

每個矩形代表一個字尾,在圖中按字典序的大小排列放置。
下面的字母代表這個字尾在串中起始位置。
\(k+1\)\(i\)代表的串其實是\(k\)\(i-1\)代表的串砍去第一個字母。所以\(height[rk[i]]=>height[rk[i-1]]-1\)

總程式碼如下

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=2010000;
int c[N],x[N],y[N],sa[N],rk[N],height[N],n,m;
char s[N];
void get_sa(){
	for(int i=1;i<=n;i++)c[x[i]=s[i]]++;
	for(int i=1;i<=m;i++)c[i]+=c[i-1];
	for(int i=n;i>=1;i--)sa[c[x[i]]--]=i;
	for(int k=1;k<=n;k<<=1){
		int num=0;
		for(int i=n;i>=n-k+1;i--)y[++num]=i;//y[]第二關鍵字排名為num的第一關鍵字位置。 
		for(int i=1;i<=n;i++)if(sa[i]>k)y[++num]=sa[i]-k;
		for(int i=1;i<=m;i++)c[i]=0;
		for(int i=1;i<=n;i++)c[x[i]]++;
		for(int i=1;i<=m;i++)c[i]+=c[i-1];
		for(int i=n;i>=1;i--)sa[c[x[y[i]]]--]=y[i],y[i]=0;//第一第二關鍵字的基數排序 
		swap(x,y);
		num=0;
		for(int i=1;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(n==num)break;
		m=num;
	}
	for(int i=1;i<=n;i++)printf("%d ",sa[i]);
}
void get_height(){
	for(int i=1;i<=n;i++)rk[sa[i]]=i;
	int k=0; 
	for(int i=1;i<=n;i++){//i是原串的下標 
		if(rk[i]==1)continue;
		if(k)k--;//height[rk[i]]>=height[rk[i-1]]-1 
		int j=sa[rk[i]-1];
		while(s[i+k]==s[j+k]&&i+k<=n&&j+k<=n)k++;
		height[rk[i]]=k;//height的下標是排名 
	}
	for(int i=1;i<=n;i++)printf("%d ",height[i]);
}
int main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	m=122;
	get_sa();
	get_height();
	return 0;
}