后綴數組 (Suffix Array) 學習筆記


\(\\\)

定義


介紹一些寫法和數組的含義,首先要知道 字典序

  • \(len\):字符串長度

  • \(s\):字符串數組,我們的字符串存儲在 \(s[0]...s[len-1]\) 中。

  • \(suffix(i) ,i\in[0,len-1]\): 表示子串 \(s[i]...s[len-1]\),即從 \(i\) 開始的后綴 。

加入我們提取出了 \(suffix(1)...suffix(len-1)\) ,將他們按照字典序從小到達排序。

  • \(sa[i]\) :排名為 \(i\) 的后綴的第一個字符在原串里的位置
  • \(rank[i]\)\(suffix(i)\) 的排名。

顯然這兩個數組可以在 \(O(N)\) 的時間內互相推出。

\(\\\)

Doubling Algorithm


由於博主太蒟並不會DC3,想看DC3的同志們可以溜了

\(\\\)

倍增構造法。

從小到大枚舉 \(k\) ,每次按照字典序排序,每一個后綴的長度為 \(2^k\) 的前綴,直到沒有相同排名的為止。

若有的后綴不夠長就在后面補上:比當前串全字符集最小字符還要小的字符,結果顯然符合字典序的定義。

\(\\\)

如何確定長度為 \(2^k\) 的每一個后綴對應前綴的排名?

倍增。有點像數學歸納法的感覺。

首先我們顯然可以直接求出來 \(k=0\) 的答案。

然后對於一個 \(k\) ,我們顯然已經完成了 \(k-1\) 部分的工作。

所以對於一個長度為 \(2^k\) 的前綴,它顯然可以由兩個長度為 \(2^{k-1}\) 的前綴拼成。

也就是說,我們可以把長度為 \(2^k\) 的前綴,寫成兩個長度為 \(2^{k-1}\) 的前綴的有序二元組。

有一個顯然的結論,因為長度 \(2^{k-1}\) 的所有前綴有序,所以我們對這些二元組排序法則可以寫成:

以前一個長度為 \(2^{k-1}\) 的前綴的 \(rank\) 為第一關鍵字,以后一個長度為 \(2^{k-1}\) 的前綴的 \(rank\) 為第二關鍵字排序。

對於此方法得到的順序,與將整個長度為 \(2^k\) 的前綴字典序排序得到的順序,想一想發現是相同的,因為它符合字典序定義

\(\\\)

比較到什么時候為止?顯然是求到一個 \(k\),使得每一個后綴 \(rank\) 不同時。

\(\\\)

附上 \(2009\) 年國家集訓隊論文中的排序圖片,可以加深體會一下整個排序的思想。

\(\\\)

代碼實現


下面重點說一下代碼實現,算法的精華也就體現在這里。附上一個寫的不錯的博客

\(\\\)

再次聲明一些數組的定義:

  • \(sa[i]\) :排名為 \(i\) 的后綴第一個字符在字符串內的位置,注意字符串數組是從 \(0\) 開始存儲的。

    需要注意的是,在倍增過程中 \(sa[i]\) 只表示對每一個后綴的長度為 \(2^k\) 的前綴排序的結果。

    同時需要注意的是,在 \(rank\) 相同時我們按照第一個字符在字符串出現的位置從小到大排序。

  • \(x[i]\) :上面的 \(rank[i]\) 我們在這里寫作 \(x[i]\) ,含義還是 \(suffix(i)\) 的排名。

    同理,在倍增過程中,\(x[i]\) 只表示每一個后綴的長度為 \(2^k\) 的前綴的排名,兩個位置的 \(x\) 可以相同。

  • \(y[i]\) :排序時的輔助數組,代表二元組的第二個元素排序的結果。

    其中 \(y[i]\) 表示 排名為 \(i\) 的第二個長度為 \(2^{k-1}\) 的前綴,對應整個前綴的開頭位置

    注意,此時下標表示名次,值代表第二關鍵字的首字符位置,與 \(x\) 數組的定義為逆運算。

  • \(cnt[i]\) :計數器數組,用於基數排序。

\(\\\)

第一步,將長度為 \(1\) 的每一個字符排序。

這個過程就是基數排序。過程中的 \(n\) 表示數組長度,\(m\) 表示原串字符集范圍為 \([1,m-1]\)

注意體會最后一行的倒序循環,此時體現了 \(rank\) 相同時按照第一個字符在字符串出現的位置排序的原則。

for(R int i=0;i<n;++i) ++cnt[x[i]=s[i]];
for(R int i=1;i<m;++i) cnt[i]+=cnt[i-1];
for(R int i=n-1;~i;--i) sa[--cnt[x[i]]]=i;

\(\\\)

然后我們就要開始倍增構造,設 \(k\) 直接表示當前考慮的前綴長度。

for(R int k=1,p=0;k<=n;k<<=1)

\(\\\)

首先看本次排序構造的 \(y[i]\)

由於 \(sa\) 數組是有序的,所以我們沒必要對 \(y[i]\) 數組進行一次基數排序。

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

第二行的含義是,因為字符串的后 \(k\) 個后綴一定不能再找到長度為 \(k\) 的后綴繼續拼接了。

根據字典序的定義,空串字典序優於任何一個字符串,所以他們的 \(y\) 應該最靠前。

同時因為 \(rank\) 相同時按照第一個字符在字符串出現的位置排序的原則,循環是正序。

第三行的含義是,如果一個長度為 \(k\) 的前綴起始位置 \(\le k\) ,那它必然作為一個后一段接在前面的某一個位置上。

可以注意到的是, \(sa\) 數組和 \(y\) 數組的定義形式是一致的,也就是說, 我們按照 \(sa\) 的順序構造 \(y\) 沒有問題。

\(\\\)

然后就要構造 \(sa[i]\) 。這也是構造過程中最精華的一部分。

for(R int i=0;i<m;++i) cnt[i]=0;
for(R int i=0;i<n;++i) ++cnt[x[y[i]]];
for(R int i=1;i<m;++i) cnt[i]+=cnt[i-1];
for(R int i=n-1;~i;--i) sa[--cnt[x[y[i]]]]=y[i];

這其實是一個雙關鍵字基數排序的過程。

雙關鍵字基數排序時,我們需要先將第二關鍵字直接排序,然后再使用上面的代碼。

現在 \(y[i]\) 顯然已經是有序的了。

這個過程的理解可以參考最開始的單關鍵字基數排序。

為什么那時我們做到了在 \(rank\) 相同時我們按照第一個字符在字符串出現的位置從小到大排序的要求?

因為我們是倒着掃描的。

同理,為了讓 \(x\) 相同的 \(y\) 越劣的越靠后,我們直接倒着掃描 \(y\) 不就可以了嗎!

此時我們成功在 \(sa\) 數組內完成了第一第二關鍵字合並后的排序。

\(\\\)

然后要做的就是還原 \(rank\) 數組了。

注意 \(rank\) 數組的定義中可以有相同的排名,所以第一第二關鍵字 \(rank\) 相同的注意要特殊對待。

inline bool cmp(int *a,int x,int y,int k){return a[x]==a[y]&&a[x+k]==a[y+k];}
swap(x,y); p=1; x[sa[0]]=0;
for(R int i=1;i<n;++i) x[sa[i]]=cmp(y,sa[i-1],sa[i],k)?p-1:p++;

注意這個指針交換的過程,它優化掉了 \(swap\) 兩個數組的復雜度。

因為 \(x\) 數組是上一個 \(k\)\(rank\) 結果,所以可以直接比對新的即將拼合的兩段是否相同。

\(\\\)

最后還有一個小優化。

if(p>=n) break;
m=p;

就是 \(p=n\) 時,可以發現當前長度的前綴已經具有了區分每一個后綴的作用,所以我們沒必要繼續比下去了。

同時,上一次不同 \(rank\) 的個數顯然是下一次基數排序的字符集大小。

\(\\\)

最后再多說一句,值得注意的是,不管是哪種實現方式,除了空字符外 \(rank\) 必須從 1 開始,否則會造成最小字符與空字符運行時混淆。

\(\\\)

一道例題


給出一個字符串,寫出其所有循環同構串,將其按字典序從小大排序,輸出排序后每一個串的尾字符。

\(\\\)

環的問題一般可以破環成鏈去搞。

拆開之后復制一倍接在后面,直接跑后綴數組,按 \(sa\) 順序輸出所有長度大於 \(len\) 的后綴對應答案。

\(\\\)

#include<cmath>
#include<cstdio>
#include<cctype>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define N 200005
#define R register
using namespace std;

char ss[N];

int s[N],sa[N],cnt[N],t1[N],t2[N];

void da(int n,int m){
  int *x=t1,*y=t2;
  s[n++]=0;
  for(R int i=0;i<n;++i) ++cnt[x[i]=s[i]];
  for(R int i=1;i<m;++i) cnt[i]+=cnt[i-1];
  for(R int i=n-1;~i;--i) sa[--cnt[x[i]]]=i;
  for(R int k=1,p=0;k<n&&p<n;k<<=1,m=p,p=0){
    for(R int i=n-k;i<n;++i) y[p++]=i;
    for(R int i=0;i<n;++i) if(sa[i]>=k) y[p++]=sa[i]-k;
    for(R int i=0;i<m;++i) cnt[i]=0;
    for(R int i=0;i<n;++i) ++cnt[x[y[i]]];
    for(R int i=1;i<m;++i) cnt[i]+=cnt[i-1];
    for(R int i=n-1;~i;--i) sa[--cnt[x[y[i]]]]=y[i];
    swap(x,y); p=1; x[sa[0]]=0;
    for(R int i=1;i<n;++i)
      if(y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+k]==y[sa[i]+k]) x[sa[i]]=p-1;
      else x[sa[i]]=p++;
  }
  --n;
  for(R int i=0;i<n;++i) sa[i]=sa[i+1];
}

int main(){
  scanf("%s",ss);
  int n=strlen(ss);
  for(R int i=0;i<n;++i) s[i]=ss[i];
  for(R int i=0;i<n-1;++i) s[n+i]=s[i];
  da((n<<1)-1,256);
  for(R int i=0;i<(n<<1)-1;++i) if(sa[i]<n) putchar(s[sa[i]+n-1]);
  return 0;
}


免責聲明!

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



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