1. 程式人生 > >【知識總結】字尾陣列(Suffix_Array)

【知識總結】字尾陣列(Suffix_Array)

又是一個學了n遍還沒學會的演算法……

字尾陣列是一種常用的處理字串問題的資料結構,主要由\(sa\)\(rank\)兩個陣列組成。以下給出一些定義:

\(str\)表示處理的字串,長度為\(len\)。(下標從\(0\)開始)

\([i,j)\)表示\(str\)\(i\)\(j - 1\)的字串。

字尾\(i\)表示子串\([i,len)\),以字典序排序。

\(sa[i]\)表示排名為\(i\)的字尾的起始位置(即字尾\(sa[i]\)是第\(i\)名)

\(rank[i]\)表示字尾\(i\)的排名(從\(0\)開始)。顯然\(rank[sa[i]]=i\)

一、基數排序

先簡單介紹一下字尾陣列的前置技能:基數排序。

以對整數陣列\(arr\)排序為例。從低到高遍歷每一個十進位制位,對於每個位:

\(1.\)\(arr\)陣列已經按照前\(i-1\)位排好序,(\(i=0\)時忽略這句),現在我們將把它變為按前\(i\)位排好序。腦補以下整數的比較方式,現在應該把第\(i\)位作為第一關鍵字,前\(i-1\)位作為第二關鍵字。

\(2.\)統計第\(i\)位為數字\(a\)的數的數量,存入\(count[a]\)

\(3.\)\(count\)陣列求字首和,算出最後一個第\(i\)位為\(a\)的數在按照前\(i\)位排序後陣列中的位置的下一個。這句表達比較鬼畜,看下面的例子。

比如,\(i\)位為\(0\)的有\(2\)個,為\(1\)的有\(1\)個,為\(2\)的有\(3\)個,第\(3\)步以後\(count\)\(\{2,3,6\}\),那麼排序後\(arr[0]\)\(arr[1]\)的第\(i\)位為\(0\)\(arr[2]\)的第\(i\)位為\(1\)\(arr[3]\)\(arr[5]\)的第\(i\)位為\(2\)

\(4.\)逆序遍歷\(arr\),按照上一步中算出的第\(i\)位為\(a\)的數排序後的位置逆序填充臨時陣列。兩個均逆序保證了對於第\(i\)位相同的數按照最初在\(arr\)中的位置排序。

\(5.\)最後,把臨時陣列複製給\(arr\)

,此時\(arr\)按照前\(i\)位有序。

int count[10];
for(int i = 1; i <= 10; i++, ra *= 10)
{
    memset(count, 0, sizeof(count));
    for (int j = 1; j <= n; j++)
        ++count[arr[j] / ra % 10];//step 2
    for (int j = 1; j < 10; j++)
        count[j] += count[j - 1];//step 3
    for(int j = n - 1; j >= 0; j--)
        buc[--count[arr[j] / ra % 10]] = arr[j];
    memcpy(arr, buc, sizeof(int[n]));
}

二、倍增構造字尾陣列

考慮我們現在有了對所有形如\([i,min(i+tmp,len))\)的子串排序的陣列\(sa\)\(rank\)(對於相同的子串,它們的\(rank\)值相同,在\(sa\)中順序任意),我們現在要構造對所有形如\([i,min(i+2tmp,len))\)的子串排序。最壞情況下,當\(2tmp\geq len\)時就得到了答案。

可以發現此時很類似於基數排序時排到某一位時的情況。此時,第一關鍵字是\([i,i+tmp)\),第二關鍵字是\([i+tmp, i+2tmp)\)。並且,現在已經按照第二關鍵字排好序了。

於是我們先看看此處的基數排序。其中\(kind\)\(rank\)中不同值的種數(由於\(rank\)\(0\)開始,也可以看成\(rank\)中最大值加\(1\)),\(tp[i]\)表示哪個串的第二關鍵字在所有第二關鍵字中的排名是\(i\)

void radix_sort()
{
    static int count[N];
    memset(count, 0, sizeof(int[kind]);
    for (int i = 0; i < len; i++)
        count[rank[tp[i]]]++;
    for (int i = 1; i < kind; i++)
        count[i] += count[i - 1];
    for (int i = len - 1; i >= 0; i--)
        sa[--count[rank[tp[i]]]] = tp[i];
}

然後我們來構造\(tp\)陣列。首先,對於起點在\([len-tmp,len)\)中的串,它們的第二關鍵字都是空串,排名是最低的。所以它們應當在\(tp\)的開頭:

for (int i = len - tmp; i < len; i++)
    tp[cnt++] = i;

然後,按照\(sa\)加入剩下的串。注意只有起點在\(tmp\)及以後的串才能作為第二關鍵字。

for(int i=0;i<len;i++)
    if(sa[i]>=tmp)
        tp[cnt++]=sa[i]-tmp;

至此,\(tp\)陣列構造完畢,可以進行基數排序。排序後,我們要按照新的\(sa\)和舊的\(rank\)構造新的\(rank\)。首先,把舊的\(rank\)進行拷貝。為了優化常數可以這樣寫:

swap(rank,tp)

記住,此後\(tp\)就只是舊的\(rank\)的一份拷貝了,沒有更多實際意義。更新\(rank\)的過程比較顯然。

rank[sa[0]] = 0;
kind = 1;
for (int i = 1; i < len; i++)
{
    if (tp[sa[i]] == tp[sa[i - 1]] && 
        (sa[i] + tmp < len && sa[i - 1] + tmp < len) && 
        (tp[sa[i] + tmp] == tp[sa[i - 1] + tmp]))
        rank[sa[i]] = rank[sa[i - 1]];
    else
        rank[sa[i]] = kind++;
}

最後,如果\(kind=len\),即\(rank\)已經兩兩不同,則說明已經得出了答案。

三、應用:構造\(height\)陣列

我不會,你開心不qwq

四、完整程式碼:

int sa[N], rank[N], tp[N], kind, len;
void radix_sort()
{
    static int count[N];
    memset(count, 0, sizeof(int) * kind);
    for (int i = 0; i < len; i++)
        count[rank[tp[i]]]++;
    for (int i = 1; i < kind; i++)
        count[i] += count[i - 1];
    for (int i = len - 1; i >= 0; i--)
        sa[--count[rank[tp[i]]]] = tp[i];
}
void build(const string &s)
{
    len = s.size();
    for (int i = 0; i < len; i++)
        rank[i] = s[i], tp[i] = i;
    kind = CH;
    radix_sort();
    for (int tmp = 1; tmp < len; tmp *= 2)
    {
        int cnt = 0;
        for (int i = len - tmp; i < len; i++)
            tp[cnt++] = i;
        for (int i = 0; i < len; i++)
            if (sa[i] >= tmp)
                tp[cnt++] = sa[i] - tmp;
        radix_sort();
        swap(rank, tp);
        rank[sa[0]] = 0;
        kind = 1;
        for (int i = 1; i < len; i++)
        {
            if (tp[sa[i]] == tp[sa[i - 1]] && 
                (sa[i] + tmp < len && sa[i - 1] + tmp < len) && 
                (tp[sa[i] + tmp] == tp[sa[i - 1] + tmp]))
                rank[sa[i]] = rank[sa[i - 1]];
            else
                rank[sa[i]] = kind++;
        }
        if (kind == len)
            break;
    }
}