練習:最長迴文子串(Manacher算法)

【例題】

點擊這裏

【思路】

最長迴文子串是個非常經典的問題,Manacher算法是解決它的O(n)優秀算法。

該算法提出在字符串相鄰字符間插入字符,從而在中心拓展時無需考慮串長度的奇偶性(顯然,對於任意長度爲n的串,有n-1個間隔,故而補全串長度爲2n-1,總爲奇數)。

舉個例子:原串str爲abababa,長度爲7。則補全串s爲$#a#b#a#b#a#b#a#*,長度爲17。其中#、$和*不屬於原串字符集合,#爲插入字符,$和*用來標記補全串邊界,這方便了中心拓展比較,同時避免索引溢出。

設數組r,r[i]表示補全串s中,以字符s[i]爲中心的迴文串半徑長度(半徑含中心字符s[i])。


一旦得出數組r,則只需遍歷r並將ans置爲最大的r[i]值,輸出ans-1即爲原串中迴文串的最大長度。

可以證明,經過上面的補全後,對於任意i,補全串中以s[i]爲中心的迴文串去掉所有#後,長度爲r[i]-1,即對應的原串中迴文子串的長度。

如何求出數組r呢?顯然在求r[i]時,要充分利用r[1]、r[2]……r[i-1]這些已經求出的信息。先在這裏寫出數學表達。


由集合Q確定r[i]的最小值後,還需嘗試性地增大r[i]向外圍擴展比較,直到遇見s[i-r[i]]≠s[i+r[i]],則已找到以s[i]爲中心的最長迴文串。

上面的數學表述雖然嚴謹,但不夠具象,下面對該數學形式作通俗闡述。

事實上,j+r[j]反映了1,2……i-1中已經找到的所有迴文串的最大覆蓋範圍。如果j+r[j]<i,則前面的結果對求r[i]並沒有作用,我們只能暫時置r[i]=1(一個字符本身,也是一個迴文串),然後再向外拓展比較。否則,i則在前面的覆蓋範圍內,那麼首先找到s[i]關於s[j]的對稱字符s[j*2-i],則s[i]、s[j*2-i]距以s[j]爲中心的迴文串邊緣的長度均爲j+r[j]-i,根據迴文串的對稱性可得,若r[j*2-i]< j+r[j]-i(以s[j*2-i]爲中心的迴文串是以s[j]爲中心的迴文串的子串),則在覆蓋範圍內可以確定r[i]=r[j*2-i] ; 若r[j*2-i]> j+r[j]-i,則以s[i]爲中心的迴文串可一直拓展至覆蓋邊緣。覆蓋範圍外的部分,則需要我們嘗試增大r[i],繼續老老實實地比較。

更形象的表述,則可見下圖(綠色爲重疊部分,藍色爲s[j]中心串,黃色爲s[j*2-i]中心串突出部分):



【代碼】

#include <stdio.h>
#include <string.h>
#define maxSize 1000010
#define min(x,y) (x<y)? x:y

long int manacher(const char *str)
{
    long int i,len=strlen(str);
    char s[maxSize*2+3];
    s[0]='$'; s[len*2+1]='#'; s[len*2+2]='*';
    for (i=0;i<len;i++) {s[2*i+1]='#'; s[2*i+2]=str[i];}

    long int r[maxSize*2+3], j=1; r[1]=1;
    for (i=2;i<=len*2;i++)
    {
        if (r[j]+j>i) r[i]=min(r[j]+j-i, r[2*j-i]); else r[i]=1;
        while (s[i-r[i]]==s[i+r[i]]) r[i]++;
        if (i+r[i]>j+r[j]) j=i;
    }

    long int ans=0;
    for (i=1;i<=len*2;i++) if (r[i]>ans) ans=r[i];
    return ans-1;
}

int main()
{
    char str[maxSize];
    int n,i;
    scanf("%d",&n);
    for (i=0;i<n;i++){scanf("%s",str); printf("%d\n",manacher(str));}
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章