後綴數組 代碼詳解

研究了好幾天的後綴數組,今天終於是把代碼實現給看明白了了。

大多數博客都講解了了後綴數組以及倍增法,但是對於代碼的講解不是很明白。

這篇就把LRJ藍書上的代碼拆開來一句一句的進行解釋。

關於倍增算法我建議去看lrj的數,寫的非常清晰明瞭;基數排序可以去看看百度百科,其實和桶排序是一家子。

代碼:

char s[maxn];
int sa[maxn],t[maxn],t2[maxn],c[maxn],n;
void print(int *arr){
	for(int i=0;i<n;i++) cout<<arr[i]<<" ";
	cout<<endl;
}
void getSa(int m){
	int *x=t,*y=t2;
	for(int i=0;i<m;i++) c[i]=0;
	for(int i=0;i<n;i++) c[x[i]=s[i]]++;
	for(int i=1;i<m;i++) c[i]+=c[i-1];
	for(int i=n-1;i>=0;i--)
		sa[--c[x[i]]]=i;
	cout<<"sa:";print(sa);
	cout<<"x:";print(x);
	
	
	for(int k=1;k<=n;k<<=1){
		int p=0;
		for(int i=n-k;i<n;i++) y[p++]=i;
		for(int i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;
		cout<<"y:";print(y);
		
		for(int i=0;i<m;i++) c[i]=0;
		for(int i=0;i<n;i++)
		 	c[x[y[i]]]++;
		for(int i=0;i<m;i++) c[i]+=c[i-1];
		for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
		cout<<"sa:";print(sa);
		
		swap(x,y);
		cout<<"y:";print(y);
		p=1;x[sa[0]]=0; 
		for(int i=1;i<n;i++)x[sa[i]]=(y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+k]==y[sa[i]+k])?p-1:p++;
		cout<<"x:";print(x);
		if(p>=n) break;
		m=p; 
	}
}

代碼中爲了便於調試增加了數組輸出的函數;

首先我們必須搞懂每一個數組的意義和用途,這是非常重要的而且是書上所沒有提及的。

不搞懂容易讓人一頭霧水。

sa y c s x
後綴數組(第一關鍵字的名次) 第二關鍵字的名次 基數排序用到的“桶” 字符串 後綴數組的一個離散

 

 

 

上表用於在完全理解了幾個數組的含義後對照使用,下面在深度的說一下這幾個數組

sa:就是最後要得到的後綴數組,也就是第一關鍵字的名次,第一關鍵字的定義的藍書上非常明瞭,可以參看書上。

sa[i]:排名爲i的後綴的下標爲sa[i]

x:很多人把x數組當做第一關鍵字的排序,我覺得這麼理解的話容易讓人產生誤解。

我覺得把x理解爲後綴的一個離散化數組更好。至於怎麼個離散化,一會看代碼;

比如第一遍倍增法的得到的後綴是這樣的(書上例子):aa,ab,ba,aa,aa,aa,ab,b$;

那麼對應的x數組爲:0 1 3 0 0 0 1 2

x[i]:後綴i的離散化;

y:以第二關鍵字進行排序,排名第i名的後綴爲y[i]

                                                                                                                                                                           

下面看具體代碼:

先看這四行:

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

第一行:桶清零,沒啥好說的

for(int i=0;i<n;i++) c[x[i]=s[i]]++;

將s[i]賦值到x[i]中,並放入桶中。

此時x數組爲:97 97 98 97 97 97 97 97 98;c數組爲:c[97]=6;c[98]=2;其餘爲0

(97 97分別爲a,b對應的ASC碼值)

for(int i=1;i<m;i++) c[i]+=c[i-1];

加上前面桶的值,也就得到了這個值的排名;c[97]=6;c[98]=8;

for(int i=n-1;i>=0;i--) sa[--c[x[i]]]=i;

這一行稍微難理解一點,再重申一下sa的定義:sa[i]:排名爲i的後綴;

在上一行代碼中,對應的桶中已經記錄了對應“字符”的排名(字符離散到x數組中)

所以說每個排名對應的下面就全部放入到sa之中了;

eg:sa[--c[x[7]]=7->sa[--c[98]]=7->sa[7]=7 所以排第七的是後綴7

--c[x[i]]的意思是這個元素已經拿走了,下一次再取到和這個相同的元素的話排名會提前一個,所以要--

然後就是這個循環的順序問題,他是從大到小的。想一下爲什麼,可以反過來嗎?

提示一下這個c[i]的值是有關係的, 因爲c[i]在取完之後是減一,這就保證了對於相同後綴,後面的排名更高。

                                                                                                                                                                                     

然後看下面這堆代碼

	for(int k=1;k<=n;k<<=1){       //k是倍增的‘步數’
		int p=0;    //p在下面兩行起一個下標的作用
		for(int i=n-k;i<n;i++) y[p++]=i;
		for(int i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;
		cout<<"y:";print(y);
		
		for(int i=0;i<m;i++) c[i]=0;
		for(int i=0;i<n;i++) c[x[y[i]]]++;
		for(int i=0;i<m;i++) c[i]+=c[i-1];
		for(int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];
		cout<<"sa:";print(sa);
		
		swap(x,y);
		cout<<"y:";print(y);
		p=1;x[sa[0]]=0; 
		for(int i=1;i<n;i++)x[sa[i]]=(y[sa[i-1]]==y[sa[i]] && y[sa[i-1]+k]==y[sa[i]+k])?p-1:p++;
		cout<<"x:";print(x);
		if(p>=n) break;
		m=p; 
	}

開始進入了倍增法

看這兩行

for(int i=n-k;i<n;i++) y[p++]=i;
for(int i=0;i<n;i++) if(sa[i]>=k) y[p++]=sa[i]-k;

這兩行是求y數組,第二關鍵字的排序,向上翻看y的定義並牢牢記住,非常重要,不要容易把自己繞進去;

第一行:後面k個後綴是沒有第二關鍵字的,所以他們的排名在前面,參照課本很容易理解;

第二行:根據sa求y,也就是說根據第一關鍵字求第二關鍵字;

這很容易理解,也很容易想,當前位置的第一關鍵字,不就是前一位置的第二關鍵字嘛,對吧

根據代碼實際動手構造一下會很容易理解;sa[i]>=k是因爲前k個字符根本不能成爲任何後綴的第二關鍵字;

sa[i]-k是因爲當前下標是由後面下標提過來的,需要-k;(建議手動理解)

看下面四行代碼

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

看上去和剛開始的四行一樣,但是有有點不一樣;

就是第二行和第四行的i換成了y[i],也可以這樣理解,剛開始沒有第二關鍵字,所以y就是從0開始遞增的,也就是y[i]=i;

對於相同的不作過多的贅述

看這一行:

for(int i=0;i<n;i++) c[x[y[i]]]++;

那麼x[y[i]]表示啥意思呢?就是一第二關鍵字爲順序組成的一個離散化的數組

這個數組第二關鍵字是有序的。

建議把這個數組寫一遍,就能發現其含義;

再看第四行

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

 從上面知道了,x[y[i]]的第二關鍵字是有序的,那麼再把第一關鍵字排一下,那麼兩個不就都有序了嗎

同樣,順序是從大到小的,原因同上;

 

到這裏,比較難懂的地方就完成了,細節比較多,不容易一次性理解。

剩下的地方看着書很容易理解,代碼也容易書寫,就不做過多解釋了。

發佈了49 篇原創文章 · 獲贊 15 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章