1. 程式人生 > >後綴數組小結

後綴數組小結

穩定 必須 其中 作用 lse 輔助 字符串 給定 本質

後綴數組(SA)是解決一系列字符串問題的強有力的工具

後綴數組的本質其實是對字符串\(s\)的所有後綴按照字典序從小到大排序

比如說,對於字符串\(s="aabbabab"\),我們將它的所有後綴排序得到如下的結果

\(aabbabab\)
\(ab\)
\(abab\)
\(abbabab\)
\(b\)
\(bab\)
\(babab\)
\(bbabab?\)

前置芝士

後綴

這玩意要說嗎

計數排序

所有的基於比較大小的排序算法最優是\(O(nlogn)\)的,如果我們想要加速的話就必須考慮那些並不是基於比較大小的算法

計數排序是一個\(O(n+maxd)\)的穩定算法,其中\(maxd\)

表示要排序的元素中的最大值

計數排序的思想主要是利用前綴和統計出比當前元素小的元素有多少個,比如說對於某一個元素\(a\)如果序列中有4個比它小的元素,那麽\(a\)就應該排在第五名

在實現計數排序的時候一般需要三個數組

(1)\(tax\),作為一個桶,用來統計前綴和

(2)\(rnk\),儲存用來排序的關鍵字大小(即原序列)

(3)\(ans\),第\(i\)位表示排名為\(i\)的元素是什麽

首先在\(O(maxd)\)的時間內統計出\(tax\)數組

接著在\(O(n)\)的時間內統計出\(ans\),註意在每排出一個元素的位置之後將\(tax--\)

如果有雙關鍵字的話同理,註意枚舉順序

rmq問題

一個簡單的問題,給定一個序列\(A\)\(q\)個詢問,每次詢問區間\([l..r]\)的最小值

如果有修改的話那麽這就是一道線段樹

但是問題在於這個問題沒有修改,於是我們可以用一些奇奇怪怪的方法搞過去

維護一個\(mind[i][p]\)表示區間起始點為\(i\),長度為\(2^w\)的一個區間的最小值

這個玩意可以遞推得到,\(mind[i][p]=min(mind[i][p-1],mind[i+(1<<(p-1))][p-1]\)(類似於倍增),邊界條件\(mind[i][0]=A[i]\)

同時處理一個\(low\)數組,表示\(log_2i\)(因為系統自帶的log很慢)

然後在處理詢問的時候先根據區間長度(\(r-l+1\))處理出相應的\(p\)

然後這一段區間的最小值就是\(min(mind[l][p],mind[r-(1<<p)+1][p])\)(考慮從\(l\)\(r?\)開頭的區間將原區間覆蓋)

模板

求sa數組

這是一道模板題

後綴數組的構造主要會用到四個數組

(1)\(sa\),第\(i\)位表示排名為\(i\)的後綴在原串中的位置(即首字母的出現位置)

(2)\(rnk\),第\(i\)位表示位置為\(i\)的後綴的字典序排名

那麽很明顯就會有
\[ rnk[sa[i]]=i\\sa[rnk[i]]=i \]
(3)\(tp\),輔助作用,主要是用來儲存第二關鍵字和上一輪的rnk的臨時儲存

(4)\(tax\),計數排序的桶

構造後綴數組主要有倍增法\((O(nlogn))\)和DC3法\((O(n))\),由於前者除了時間復雜度以外都與第二個方法相比更為優秀,故在這裏只討論倍增法(又是倍增啊

在使用倍增法的時候,我們假設我們已經完成了前\(2^w?\)位的排序,接下來將要對\(2^w+1\text~2^{w+1}?\)位排序

首先明確一點:在前\(2^w\)位中排在\(i\)之後的後綴,在完成\(2^{w+1}\)位排序之後是不會跑到\(i\)之前的

所以我們排序的第一個關鍵字就是當前的\(rnk\)

但是這明顯是不夠的,我們還需要一個第二關鍵字——以第\(2^w+1\text~2^{w+1}\)位排序得到的結果

我們將會用到\(tp\)——第\(i\)位表示排名為\(i\)的後綴的位置,作為第二關鍵字進行排序

首先排在\(tp\)最前面的就是那些沒有後\(2^w\)位的後綴,這一些後綴的起始位置是\(n-w+i(i\leq w)\)

那麽接下來就是對剩下的那些後綴排序,我們如何快速確定它們的順序呢?

對於一個有後\(2^w\)位的後綴,這後\(2^w\)位不只是屬於這一個後綴,他還會成為前\(2^w\)位,如果所有的後\(2^w\)位都變成了前\(2^w\)位,那麽我們就可以用之前得到的\(rnk\)來確定順序

對於一個起始位為\(i\)的後綴,如果它有後\(2^w\)位的話,那麽這後\(2^w\)位應該是起始位置是\(i+2^w\)的後綴

經過這一轉化之後,我們就可以利用\(rnk\)得到\(tp\)的值

得到了\(rnk\)\(tp\)之後,我們就可以以它們為第一、二關鍵字進行計數排序,得到新的\(sa\)數組

那麽我們就只剩下從新的\(sa\)推出新的\(rnk\)

很明顯由一開始的性質有\(rnk[sa[1]]=1\),那麽接下來是否也可以這麽遞推呢?

很遺憾並不行,因為對於前\(2^{w+1}\)位如果有兩個字符串出現相同的話,那麽暫時我們認為這兩個字符串是大小相等的,即它們的\(rnk\)均為一樣的

對於這種情況,我們需要使用上一次的\(rnk\)數組,我們考慮在現在的\(sa\)中相鄰的兩個後綴,如果它們的前\(2^w\)位和後\(2^w\)位都是相同的話就說明它們在這一輪的排序中無法被區分,即\(rnk\)相同

而這兩個都是可以利用上一次的\(rnk\)數組得到,我們先將上一次的\(rnk\)放入\(tp\)中,然後直接比較\(tp\)的大小

emmmm,講的可能比較亂,直接上代碼吧

    void qsort()
    {
        int i;
        for (i=0;i<=m;i++) tax[i]=0;
        for (i=1;i<=n;i++) tax[rnk[i]]++;
        for (i=1;i<=m;i++) tax[i]+=tax[i-1];
        for (i=n;i>=1;i--) sa[tax[rnk[tp[i]]]--]=tp[i];
    }
    
    void get_sa()
    {
        memset(rnk,0,sizeof(rnk));
        memset(sa,0,sizeof(sa));
        memset(tp,0,sizeof(tp));
        memset(tax,0,sizeof(tax));
        int i,p=0,w;n=strlen(s+1);m=30;
        for (i=1;i<=n;i++) {rnk[i]=s[i]-‘a‘+1;tp[i]=i;}
        qsort();
        for (w=1;p<n;w<<=1)//這裏只要比較出了n個後綴的大小就算完成任務了,所以可以直接退出
        {
            p=0;
            for (i=1;i<=w;i++) tp[++p]=n-w+i;
            for (i=1;i<=n;i++)
                if (sa[i]>w) tp[++p]=sa[i]-w;
            qsort();
            memcpy(tp,rnk,sizeof(tp));
            rnk[sa[1]]=1;p=1;
            for (i=2;i<=n;i++) 
                if ((tp[sa[i]]==tp[sa[i-1]]) && ((tp[sa[i]+w]==tp[sa[i-1]+w]))) rnk[sa[i]]=p;
                else rnk[sa[i]]=(++p);
            m=p;
        }
    }

求height數組

如果只是單純的構造sa數組是體現不出後綴數組的強大作用的

我們知道,很多字符串題都是與lcp(最長公共前綴)或lcs(最長公共後綴)有關

於是利用\(sa\)\(rnk\)這兩個數組可以構建出一個新的數組——\(height\)

\(height[i]\)表示\(sa[i]\)\(sa[i-1]\)的最長公共前綴

如果我們再記\(H[i]=height[rnk[i]]\)的話,就會有一個十分有意思的性質:\(H[i]\geq H[i-1]+1?\)

證明的話,就還是移步這位dalao的博客吧,大體思路就是按照兩個後綴的首字母是否相同進行分類討論

博客鏈接(%%%dalao):https://www.cnblogs.com/zwfymqz/p/8413523.html

    void get_h()
    {
        int i,pre=0;
        for (i=1;i<=n;i++)
        {
            if (pre) pre--;
            int j=sa[rnk[i]-1];
            while ((i+pre<=n) && (j+pre<=n) && (s[i+pre]==s[j+pre])) pre++;
            height[rnk[i]]=pre;
        }
    }

利用\(height\)數組我們可以處理一系列問題

例題先咕著,有時間再補qwq

後綴數組小結