1. 程式人生 > 其它 >字尾陣列學習筆記

字尾陣列學習筆記

字尾陣列學習筆記

蒟蒻作者通過 ~(o°ω°o) (cnblogs.com)字尾陣列 - 百度文庫 大致弄明瞭字尾陣列

字尾陣列本身是對字串字尾進行排序的。

它有一個重要的前置知識,~~(蒟蒻作者最開始就死在這裡 ~~ ,基數排序

它還可以解決許多關於字串的問題,一個經典的應用是 LCP

我們從前置知識開始

基數排序

假設我們已經有了一點點基數排序的基礎

可以簡單將基數排序的作用理解成對於一些 \(pair(p,q)\) 排序。

第一關鍵字 \(p\) 相等時,則比較第二關鍵字 \(q\)

具體應用在這裡,我們首先僅考慮將每個字尾第一個字元從小到大排序。

考慮把第 \(i\) 個字母看作 \((s[i],i)\)

這樣的二元組。

這樣我們可以保證 \(ascii\) 小的在前面,若 \(ascii\) 相同則先出現的在前面,然後

(圖源:https://www.sohu.com/a/244143883_100201031)

(當然,蒟蒻作者並不是很看得懂這張圖)

大體的意思是,將第一關鍵字放入一個桶 \(tax[i]\),表示 \(i\) 出現次數

(這裡實現了一個桶排序第一關鍵字對吧)

for(int i=1;i<=n;++i)tax[s[i]]++;

然後,我們做一個字首和,統計排名(\(m\) 是值域範圍)

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

現在,\(tax[i]\)

的意義變成了小於等於 \(i\) 的數的個數(聯想排名的意義!!)

考慮到第二關鍵字的影響,對於第一關鍵字相同的情況(被分到同一個 \(tax\) ),我們應該按順序分配排名

for(int i=n;i>=1;--i)rank[tax[s[i]]--]=i;

在這裡 \(rank[i]\) 表示的意義是排名為 \(i\) 的下標位置。

(相信大家一定很容易意會這個倒序的意義所在)

其中 tax[i]-- 的目的是為了分配不同排名。

比如 \(('a',1),('a',2)\),(大家一定還記得第二關鍵字的意義是下標)

\(tax['a']=2\),我倒序列舉,第一次給 \(rank[2]\)

分配 \(2\),第二次給 \(rank[1]\) 分配 \(1\)

(好像這並不是一個很好的例子)

意會意會,它的模板應該是這樣的:

for(int i=1;i<=n;++i)

最後一個迴圈的重點是從大到小列舉第二關鍵字。

顯然,時空複雜度 \(O(n)\)

字尾陣列

我們現在進入正題。

暴力構造大概是 \(O(n^3)\) 的,顯然不行。

比較高效的 \(\text{Multi-key Quick Sort}\) 也是 \(O(n^2)\)

這些都太拉了,我不要,所以:

Definition

首先,我們可以記 \(Suf(i)\) 表示字串從 \(i\) 位置開始(到整個串末尾結束的一個)的字尾,也就是說 \(Suf(i)=s[i\dots strlen(s)]\)

字尾陣列:\(sa[i]\) ,表示排名為 \(i\) 的字尾的起始位置。

名次陣列:\(rk[i]\),表示 $Suf(i) $ 的排名

(大家可能要仔細揣摩一下這個陣列的含義)

也就是說,\(sa\) 儲存 \(1\dots n\) 的某個排列 \(sa[1],sa[2]\dots sa[n]\) 使得 \(Suf(sa[i])\leq Suf(sa[i+1]),1\leq i< n\)

顯然有 \(sa[rk[i]]=rk[sa[i]]=i\) ,這說明 \(sa\)\(rk\) 是可以 \(O(n)\) 遞推的。

求得這兩個陣列的方法大致有兩種:

  • \(O(n\log n)\) 的倍增
  • $O(n) $ 的 \(DC3\)

由於蒟蒻作者不會第二種,並且,拋去理論時間複雜度,倍增法在常數、空間複雜度、程式碼複雜度完爆 \(DC3\) (這就是你懶得學的拙劣藉口麼

Solution:

我們記 \(Suf(i)_k\) 表示從 \(i\) 開始的字尾的前 \(k\) 個字元。

*我們可以把比較 \(s\) 的前 \(k\) 個字元叫做 \(k\) 意義下。

考慮倍增思想,我們已經知道了如何對每個字尾的第一個字元排序。

我們還可以發現一些顯然的性質:

  1. \(Suf(i)_{2k}=Suf(j)_{2k}\) \(\Leftrightarrow\) \(Suf(i)_k=Suf(j)_k,Suf(i+k)_k=Suf(j+k)_k\)
  2. \(Suf(i)_{2k}<Suf(j)_{2k}\) \(\Leftrightarrow\) \(Suf(i)_k<Suf(j)_k或(Suf(i)_k=Suf(j)_k且Suf(i+k)_k<Suf(j+k)_k)\)

注意中間是等價符號。

觀察第二個性質,在聯想一下我們的雙關鍵字排序是怎麼做的,

我們發現,這其實可以理解成 對二元組 \((Suf(i)_k),Suf(i+k)_k)\) 排序後,就得到了 \(Suf(i)_{2k}\) 的順序(即比較前 $ 2k$ 的字元意義下的 \(sa\) 陣列)!!!

而我們還知道, \(sa\) 可以 \(O(n)\) 推求 \(rk\) 陣列。

一個特殊情況是, \(i+k>n\) 怎麼辦?仔細想想,其實是沒有影響的。

這麼說,我們一共需要進行 \(\log n\)\(O(n)\) 的過程,

可以在 \(O(n\log n)\) 的時間內計算出字尾陣列 \(sa\) 和名次陣列 $rk $

Code:

由於作者太菜了,我們直接通過程式碼講解。

這裡多了一個 \(tmp[i]\) 表示第二關鍵字中排名為 \(i\) 的字尾的位置

(具體在下面)

#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+5;

char s[N];
int n,m,rk[N],sa[N],tax[N],tmp[N];

void radix_sort(){//基數排序二元組 (rk[i],tmp[i])
    for(int i=1;i<=m;i++)tax[i]=0;
    for(int i=1;i<=n;i++)tax[rk[i]]++;
    for(int i=1;i<=m;i++)tax[i]+=tax[i-1];
    for(int i=n;i>=1;i--)sa[tax[rk[tmp[i]]]--]=tmp[i];
}

void suffix_sort(){
    m=75;
    for(int i=1;i<=n;i++)//字尾第一個字元
        rk[i]=s[i]-'a'+1,tmp[i]=i;
    radix_sort();
    for(int w=1,p=0;p<n;m=p,w<<=1){
        p=0;
        for(int i=1;i<=w;i++)tmp[++p]=n-w+i;//對第二關鍵字進行排序
        for(int i=1;i<=n;i++)if(sa[i]>w)tmp[++p]=sa[i]-w;
        radix_sort();
        swap(tmp,rk);
        rk[sa[1]]=p=1;//遞推求 rk
        for(int i=2;i<=n;i++)
            rk[sa[i]]=(tmp[sa[i-1]]==tmp[sa[i]]&&tmp[sa[i-1]+w]==tmp[sa[i]+w])?p:++p;       
    }
    for(int i=1;i<=n;i++)
        printf("%d ",sa[i]-1);
}

int main(){
    scanf("%s",s+1);
    n=strlen(s+1);
    suffix_sort();
    return 0;
}

其中 \(m\) 是字符集大小,用於基數排序。(可以發現,字符集大小就是排名的個數)

由上面對基數排序的講解,此時,第一個字母的相對關係我們已經知道了。

我們在這裡用 $rk[i] $ 表示 \(Suf(i)_k\)\(tmp[i]\) 表示 \(Suf(i+k)_k\)

最開始,

for(int i=1;i<=n;i++)//字尾第一個字元
    rk[i]=s[i]-'a'+1,tmp[i]=i;
radix_sort();

我們得到了 \(Suf(i)_1\) 的大小關係。

進行倍增:(\(w\) 是已知長度)

for(int w=1,p=0;p<n;m=p,w<<=1)

然後,我們根據 $w $ 意義下的 \(sa\) 陣列(上一輪的結果),求得 $ 2w$ 意義下的 \(tmp\) ,即第二關鍵字中排名為 \(i\) 的字尾的位置。(這裡的 \(p\) 僅僅是計數器)。我們其實可以把 \(tmp\) 看作是 \(sa\)\(temp\)

for(int i=1;i<=w;i++)tmp[++p]=n-w+i;//對第二關鍵字進行排序
for(int i=1;i<=n;i++)if(sa[i]>w)tmp[++p]=sa[i]-w;
  • 對於第一個 for:顯然,有一些字尾是沒有第二關鍵字的,即 \(Suf(i+w),i+w>n\)

    我們需要把他們按順序排在前面。

  • 對於第二個 for:見圖。

注意,我們的第二關鍵字指的是 \(Suf(i+w)_w\),所以我們要減去一個 \(w\)

這時候,\(rk\) 已經有上一輪求好了,$tmp $ 的順序也確定了,我們基數排序的最後一個迴圈的正確性得以保證,我們於是進行一個基數排序。

現在的唯一問題在於求新的,\(2w\) 意義下的 \(rk\)

新的 \(rk\) 顯然是會有重複的,因為我們在倍增的過程中只是對其前幾個字元進行排名。

我們可以通過判掉一些重複的 \(rk\) 來優化一下 \(m\) ,即排名個數。

判斷重複的方法很簡單,其實就是我們之前所提到的性質一,所以我們有:

memcpy(tmp,rk,sizeof(rk));
rk[sa[1]]=p=1;//遞推求 rk
for(int i=2;i<=n;i++)
    rk[sa[i]]=(tmp[sa[i-1]]==tmp[sa[i]]&&tmp[sa[i-1]+w]==tmp[sa[i]+w])?p:++p;   

這裡,我們把 \(rk\) 在比較前 \(w\) 個字元意義下的值賦給 \(tmp\)

而下面的 \(rk\) 的意義表示的是在比較前 $2w $ 個字元意義下的

至此,字尾陣列的基礎部分大概就結束了。

看看單用 \(sa\) 我們可以做什麼?

(好像什麼都不行

最長公共字首

\(\text{(Longest Common Perfix)}\)

字尾陣列是處理字串的有力工具。

——羅穗騫

字尾陣列一個重要的作用就是求出字尾之間的的最長公共字首,它是發揮字尾陣列作用的重要工具。

Definition:

\(lcp(s,t)=max\{i|s和t在i意義下相同\}\) ,也就是從頭開始順次比較 \(s[i]\)\(t[i]\)

\(LCP(i,j)=lcp(Suf(sa[i]),Suf(sa[j]))\) ,也就是排名 \(i\) 的字尾和排名位 \(j\) 的字尾的最長公共字首。

我們先考慮如何求得 $LCP $

由定義,有性質:

  • \(LCP(i,j)=LCP(j,i)\)
  • \(LCP(i,i)=len(Suf(sa[i]))=n-sa[i]+1\)

這樣,我們在計算 $LCP $ 的時候,就只需要考慮 \(i<j\) 的情況。

有這樣一個引理:

Lemma: \(LCP(i,j)=\min(LCP(i,k),LCP(k,j))\) ,其中 \(1\leq i<k<j\leq n\)

\(p=\min(LCP(i,k),LCP(k,j))\)

我們可以先口胡一個簡單證明:

Proof 1:

顯然,\(s[sa[i]+LCP(i,k)+1]!=s[sa[k]+LCP(i,k)+1]\) ,\(s[sa[k]+LCP(j,k)+1]!=s[sa[j]+LCP(j,k)+1]\) ,而在之前的部分都是相同的,

也就是說:\(s[sa[i]\dots sa[i]+p]=s[sa[j]\dots sa[j]+p]\)

那麼,\(LCP(i,j)\) 會不會大於 \(p\) 呢?反證法。

我們不妨假設 \(p=LCP(i,k)\), 即 \(LCP(i,k)<LCP(j,k),\)另外一個情況同理,

那麼,至少有 \(s[sa[k]+p+1]=s[sa[j]+p+1]\)

\(p\) 增加 \(1\)(大於 \(LCP(i,k)\)),那麼,\(s[sa[i]+p+1]=s[sa[j]+p+1]=s[sa[k]+p+1]\)

此時, \(LCP(i,k)=p+1\) ,矛盾。

所以,\(LCP(i,j)=\min(LCP(i,k),LCP(k,j))\)

Proof 2: 這個證明來自 字尾陣列 - 百度文庫 明顯更為嚴謹。

(在文中,這條引理表述為:\(\forall 1\leq i<j<k\leq n,LCP(i,k)=\min(LCP(i,j),LCP(j,k))\)

我們設 \(p=\min(LCP(i,j),LCP(j,k))\),那麼有 \(LCP(i,j)\geq p,LCP(j,k)\geq p\)

\(Suf(sa[i])=u,Suf(sa[j])=v,Suf(sa[k])=w\)

\(u_{LCP(i,j)}=v_{LCP(i,j)}\) ,得 \(u_p=v_p\) ,同理有 \(v_p=w_p\)

所以 \(Suf(sa[i])_p=Suf(sa[k])_p\)

\[LCP(i,k)\geq p\qquad\qquad(1) \]

又設 \(LCP(i,k)=q>p\),則 \(u_q=w_q\)

\(\min{LCP(ij),LCP(j,k)}=p\) 說明 \(u[p+1]≠v[p+1]\)\(v[p+1]≠w[q+1]\),

\(u[p+1]=x,v[p+1]=y,w[p+1]=z\),顯然有 \(x≤y≤z\)(字尾陣列),又由 \(p<q\)\(p+1≤q\),(所以u[p+1]=w[p+1])

應該有 \(x=z\),也就是 \(x=y=z\),這與 $u[p+1 ]\neq v[p+1 ] $ 或 $v[p+1 ]≠w[q+1] $ 矛盾。所以:

\[LCP(i,k)\leq p\qquad\qquad(2) \]

\((1)(2)\) 式,\(LCP(i,k)=p=\min(LCP(i,j),LCP(j,k))\)

d餓到這個引理後,我們可以得到一個定理:

**Theorem: **設 \(i<j\) ,則 $ LCP(i,j)=\min{LCP(k-1,k)|i+1\leq k\leq j}$

我們已經知道了那個引理的正確性,這個定理的正確性是顯然的,可以通過數學歸納法證明。

更進一步的,我們有:

Corollary:\(i\leq j<k,LCP(j,k)\geq LCP(i,k)\)

\(---------\)

Definition:

\(height[i]=LCP(sa[i],sa[i-1])\) ,即排名為 \(i\) 的字尾和排名為 \(i-1\) 的字尾的最長公共字首

\(H[i]=height[rk[i]]\) ,即 \(Suf(i)\) 和它前一名字尾的最長公共字首

我們有:

Theorem: \(H[i]\ge H[i-1]-1\)

Proof:

定義 \(i=sa[rk[i]],j=sa[rk[i]-1],k=sa[rk[i-1]-1]\)

先對這張圖有個印象:

我們可以知道的是:

  • \(Suf(i)>Suf(j)\)
  • \(Suf(i-1)>Suf(k)\)
  • \(H[i-1]=LCP(i-1,k)>1\) (若此條不成立,那麼結論顯然成立)

由此:

  • \(LCP(k+1,i)=LCP(k,i-1)-1=H[i-1]-1\)​​​
  • \(rk[k+1]\leq rk[i]-1\)

對於第一條: 想一想其實是顯然對 ,我們同時在 \(Suf(k)\)\(Suf(i-1)\) 前面減少一個相同字元(肯定相等),就得到了 \(LCP(k+1,i)\)

對於第二條:因為 \(Suf(i-1)>Suf(k)\),所以 \(Suf(i)>Suf(k+1)\),那麼,\(rk[i]>rk[k+1]\) ,那麼 \(rk[k+1]\leq rk[i]-1\)

關注一下我們 \(i,j,k\) 的位置關係。在聯想一下我們之前證明的 \(\text{Corollary}\) ,有:

\[\begin{align} LCP(rk[i]-1,rk[i])&\geq LCP(rk[k+1],rk[i])\\ &=lcp(k+1,i)\\ &=H[i-1]-1 \end{align} \]

這樣的話,我們就可以在 \(O(n)\) 的時間中,遞推求出 \(H\) 陣列了。

大致的思想其實大家可能也猜得到,比較像 \(\text{KMP}\) 那種重複利用之前的資訊

(好像時間複雜度也是類似的證法)

就直接放程式碼了:

void getheight() {
    int j,k=0;
    for(int i=1;i<=n;i++) {
        if(k)k--;
        int j=sa[rak[i]-1];
        while(s[i+k]==s[j+k])k++;
        height[rak[i]]=k;
    }
    for(int i=1;i<=n;i++)
        printf("%d ",height[i]);
}

應用

咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕

由於 wtcl !

所以,咕咕。(這就你懶的拙劣藉口?)

算了,給一道''例題'' P5353 樹上字尾排序