後綴數組(suffix array)詳解

寫在前面

在字符串處理當中,後綴樹和後綴數組都是很是有力的工具。ios

其中後綴樹你們瞭解得比較多,關於後綴數組則不多見於國內的資料。算法

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

可以實現後綴樹的不少功能而時間複雜度也不太遜色,而且,它比後綴樹所佔用的空間小不少。數組

能夠說,在信息學競賽中後綴數組比後綴樹要更爲實用!ide

所以在本文中筆者想介紹一下後綴數組的基本概念、構造方法,工具

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

What Is Suffix Array?

學習後綴數組須要認識幾個概念:ui

子串this

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

後綴

  後綴是指從某個位置 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. 多個字符串的相關問題

相關文章
相關標籤/搜索