后綴數組(suffix array)詳解


寫在前面

在字符串處理當中,后綴樹和后綴數組都是非常有力的工具。

其中后綴樹大家了解得比較多,關於后綴數組則很少見於國內的資料。

其實后綴數組是后綴樹的一個非常精巧的替代品,它比后綴樹容易編程實現,

能夠實現后綴樹的很多功能而時間復雜度也不太遜色,並且,它比后綴樹所占用的空間小很多。

可以說,在信息學競賽中后綴數組比后綴樹要更為實用!

因此在本文中筆者想介紹一下后綴數組的基本概念、構造方法,

以及配合后綴數組的最長公共前綴數組的構造方法,最后結合一些例子談談后綴數組的應用。

What Is Suffix Array?

學習后綴數組需要認識幾個概念:

子串

  字符串S的子串r[i..j],i<=j,表示S串中從i到j這一段,就是順次排列r[i],r[i+1],...,r[j]形成的子串。

后綴

  后綴是指從某個位置 i 開始到整個串末尾結束的一個特殊子串。字符串r的從第i個字符開始的后綴表示為Suffix(i),

    也就是Suffix(i)=S[i...len(S)-1] 。

后綴數組(SA[i]存放排名第i大的后綴首字符下標)

  后綴數組 SA 是一個一維數組,它保存1..n 的某個排列SA[1] ,SA[2] ,...,SA[n] ,

  並且保證Suffix(SA[i])<Suffix(SA[i+1]), 1<=i<n 。

    也就是將S的n個后綴從小到大進行排序之后把排好序的后綴的開頭位置順次放入SA 中。

名次數組(rank[i]存放suffix(i)的優先級)

  名次數組 Rank[i] 保存的是 Suffix(i) 在所有后綴中從小到大排列的“名次”

   注:這個是排序的關鍵字~(這句話是我們排序的重點)

 

(我的理解):

sa[i]:保存的是S字符串的所有后綴在以字典序排序后,排在第i名的字符串在原來子串中的位置。

rank[i]:保存的是S字符串的所有后綴在以字典序排序后,原來的第i名現在排第幾。

簡單的說,后綴數組(SA)是“排第幾的是誰?”,名次數組(RANK)是“你排第幾?”

容易看出,后綴數組和名次數組為互逆運算。我們只要算出了sa數組,就可以在O(n)的時間復雜度內算出rank數組。

height數組:height[i]保存的是suffix(i)和suffix(i-1)的最長公共前綴的長度。也就是排名相鄰的兩個后綴的最長公共前綴。

 

How To Build Suffix Array?

要構造Suffix Array,主要就是構造sa數組,rank數組和height數組。

首先來看一下如何構造sa數組:

構造sa數組的方法有三種:

1)倍增算法:O(nlongn)

2)DC3算法:O(n)

3)skew算法(不常用)

 

這里主要講一下DC3算法

DC3算法是一個優秀的線性算法!

很多人都認為DC3算法很復雜,其實也沒多復雜,代碼也就40多行,只是for循環多了點。

DC3算法:

1) 先將后綴分成兩部分,然后對第一部分的后綴排序。 

  字符的編號從0開始。

  將后綴分成兩部分:

    第一部分是后綴k(k模3不等於0)

    第二部分是后綴k(k模3等於0)

2) 利用(1)的結果,對第二部分的后綴排序。
3) 將(1)和(2)的結果合並,即完成對所有后綴排序。

於是求出了所有后綴的排序,有什么用呢?主要是用於求它們之間的最長公共前綴(Longest Common Prefix,LCP)。

求出sa數組之后,根據rank[sa[i]]=i,rank數組自然也就能夠在O(n)的時間內求出。

那我們如何快速的求出height數組呢?

令LCP(i,j)為第i小的后綴和第j小的后綴(也就是Suffix(SA[i])和Suffix(SA[j]))的最長公共前綴的長度,則有如下兩個性質: 

    1. 對任意i<=k<=j,有LCP(i,j) = min(LCP(i,k),LCP(k,j))

    2. LCP(i,j)=min(i<k<=j)(LCP(k-1,k))

令height[i]=LCP(i-1,i),即height[i]代表第i小的后綴與第i-1小的后綴的LCP,則求LCP(i,j)就等於求height[i+1]~height[j]之間的RMQ,套用RMQ算法就可以了,復雜度是預處理O(nlogn),查詢O(1).

這樣一來我們就將height數組也求出來了。

 

下面用草稿紙來模擬一遍:

例如:
aabaaaab


總共有n=8個后綴:

1: aabaaaab

2: abaaaab

3: baaaab

4: aaaab

5: aaab

6: aab

7: ab

8: b


按照字典序排序后

sa[ 1 ] = 4 aaaab
sa[ 2 ] =  5 aaab
sa[ 3 ] =  6 aab
sa[ 4 ] =  1 aabaaaab
sa[ 5 ] =  7 ab
sa[ 6 ] =  2 abaaaab
sa[ 7 ] =  8 b
sa[ 8 ] =  3 baaaab

 

rank數組為:
rank[1]=4
rank[2]=6
rank[3]=8
rank[4]=1
rank[5]=2
rank[6]=3
rank[7]=5
rank[8]=7


height數組為:

height[ 1 ]=null
height[ 2 ]= 3
height[ 3 ]= 2
height[ 4 ]= 3
height[ 5 ]= 1
height[ 6 ]= 2
height[ 7 ]= 0
height[ 8 ]= 1


    因此,所有子串的最長公共子串就是3.

 

這里給出一個理解程序:

/*
* this code is made by crazyacking
* Verdict: Accepted
* Submission Date: 2015-05-09-21.22
* Time: 0MS
* Memory: 137KB
*/
#include <queue>
#include <cstdio>
#include <set>
#include <string>
#include <stack>
#include <cmath>
#include <climits>
#include <map>
#include <cstdlib>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
#define  LL long long
#define  ULL unsigned long long
using namespace std;
const int MAXN=100010;
//以下為倍增算法求后綴數組
int wa[MAXN],wb[MAXN],wv[MAXN],Ws[MAXN];
int cmp(int *r,int a,int b,int l)
{return r[a]==r[b]&&r[a+l]==r[b+l];}
/**< 傳入參數:str,sa,len+1,ASCII_MAX+1 */
void da(const char *r,int *sa,int n,int m)
{
      int i,j,p,*x=wa,*y=wb,*t;
      for(i=0; i<m; i++) Ws[i]=0;
      for(i=0; i<n; i++) Ws[x[i]=r[i]]++;
      for(i=1; i<m; i++) Ws[i]+=Ws[i-1];
      for(i=n-1; i>=0; i--) sa[--Ws[x[i]]]=i;
      for(j=1,p=1; p<n; j*=2,m=p)
      {
            for(p=0,i=n-j; i<n; i++) y[p++]=i;
            for(i=0; i<n; i++) if(sa[i]>=j) y[p++]=sa[i]-j;
            for(i=0; i<n; i++) wv[i]=x[y[i]];
            for(i=0; i<m; i++) Ws[i]=0;
            for(i=0; i<n; i++) Ws[wv[i]]++;
            for(i=1; i<m; i++) Ws[i]+=Ws[i-1];
            for(i=n-1; i>=0; i--) sa[--Ws[wv[i]]]=y[i];
            for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1; i<n; i++)
                  x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
      }
      return;
}
int sa[MAXN],Rank[MAXN],height[MAXN];
//求height數組
/**< str,sa,len */
void calheight(const char *r,int *sa,int n)
{
      int i,j,k=0;
      for(i=1; i<=n; i++) Rank[sa[i]]=i;
      for(i=0; i<n; height[Rank[i++]]=k)
            for(k?k--:0,j=sa[Rank[i]-1]; r[i+k]==r[j+k]; k++);
      // Unified
      for(int i=n;i>=1;--i) ++sa[i],Rank[i]=Rank[i-1];
}

char str[MAXN];
int main()
{
      while(scanf("%s",str)!=EOF)
      {
            int len=strlen(str);
            da(str,sa,len+1,130);
            calheight(str,sa,len);
            puts("--------------All Suffix--------------");
            for(int i=1; i<=len; ++i)
            {
                  printf("%d:\t",i);
                  for(int j=i-1; j<len; ++j)
                        printf("%c",str[j]);
                  puts("");
            }
            puts("");
            puts("-------------After sort---------------");
            for(int i=1; i<=len; ++i)
            {
                  printf("sa[%2d ] = %2d\t",i,sa[i]);
                  for(int j=sa[i]-1; j<len; ++j)
                        printf("%c",str[j]);
                  puts("");
            }
            puts("");
            puts("---------------Height-----------------");
            for(int i=1; i<=len; ++i)
                  printf("height[%2d ]=%2d \n",i,height[i]);
            puts("");
            puts("----------------Rank------------------");
            for(int i=1; i<=len; ++i)
                  printf("Rank[%2d ] = %2d\n",i,Rank[i]);
            puts("------------------END-----------------");
      }
      return 0;
}
View Code

 

The Use Of Suffix Array

這里只是簡單的介紹幾種后綴數組的運用,真正的熟練后綴數組,還需要通過不斷的做題、不斷的實踐來掌握。

  1. 最長公共子串

    我們知道,字符串的任何一個子串都可以看作是這個字符串某個的后綴的前綴。
    求A和B的最長公共子串等價於求A的后綴和B的后綴的最長公共前綴的最大值。
    將第二個字符串寫在第一個字符串的后面,中間用一個沒有出現過的字符隔開,在求出這個新字符串的后綴數組,然后我們只需要找最大的height[i]就可(前提是要判斷是否不在同一個字符串中)。

  2. 單個字符串的相關問題

  3. 兩個字符串的相關問題

  4. 多個字符串的相關問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM