1. 程式人生 > 其它 >字尾樹組 學習筆記

字尾樹組 學習筆記

0xFF 一些備註

本篇部落格所有證明基本略過,主要總結字尾樹組的應用

引用有 [2009]字尾陣列——處理字串的有力工具 by. 羅穗騫OI wiki日報 的大量內容

實現方面本篇部落格只會了倍增方法

0x00 一些定義

  • \(i\) 個字尾指的是首字元在 \(i\) 位置的字尾

  • 這裡排序的關鍵字是字典序,定義空字元最小

  • \(sa[i]\) 表示排名為 \(i\) 的字尾是第幾個字尾

  • \(rk[i]\) 表示第 \(i\) 個字尾的排名是多少

  • \(lcp(i,j)\) 表示字尾 \(i\) 和字尾 \(j\) 的最長公共字首長度

  • \(height[i]\) 表示 \(lcp(sa[i],sa[i-1])\)

0x01 字尾排序

目標:將一個字串的 \(n\) 個字尾按照字典序大小排序

實現:按照倍增的思想,分別排序前 \(2^0, 2^1, 2^2, 2^3……\) 個字元。上一次排序過後,以上一次排序為第二關鍵字,當前層為第一關鍵字繼續排序,總複雜度 \(O(nlogn)\)

至於怎樣 \(O(n)\) 排序,可以採用基數排序,因為上一層可以順便幫助這一層離散化
具體來講,開 \(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-k+1;i<=n;i++)y[++num]=i;
		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);
		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;
	}
	return ;
}
int main(){
	m=122;
	scanf("%s",s+1);
	n=strlen(s+1);
	get_sa();
}

\(c[i]\) 陣列有關的是基數排序的過程,不再贅述

for(int i=n-k+1;i<=n;i++)y[++num]=i;
for(int i=1;i<=n;i++){
	if(sa[i]>k)y[++num]=sa[i]-k;	
}

這兩行的意思是這樣的,由於從 \(n-k+i\) 開始,由於長度不夠,所以子串為空,自然最短,而剩下的則有 \(sa[i]\) 決定,這樣省去了第二關鍵字排序的過程

lcp問題的求解

直接放一些我也不會證的結論:

\(∀1≤i< j < k \le n, lcp(sa_i,sa_k)=min \{lcp(sa_i,sa_j),lcp(sa_j,sa_k) \}\)