1. 程式人生 > >最長回文子串的不同解法

最長回文子串的不同解法

思想 manacher turn 核心 都是 一個 ges 例如 tracking

給定一個字符串,返回該字符串的最長回文子串。回文也就是說 。正著讀和反著讀是一樣的。以下總結了幾種求回文的方式:

方法1 : 非常easy,枚舉全部的區間 [i,j] ,查看該範圍內是否是一個回文.

時間復雜度 O(n^3),空間復雜度 O(1).

方法2: 方法1的時間復雜度太高,而且存在著大量的反復運算。能夠使用DP來解。而且保存已經檢查過的字符串的狀態.

時間復雜度: O(n^2)。空間復雜度O(n^2).

這裏存在兩種DP的方法,是依據區間來進行DP,還是長度。只是都是大同小異,不改變整個算法的時間復雜度。

代碼例如以下:

//dp 1
string LongestPalindrome(const string &s)
{
    const int n = s.size();
    if(n < 2) return s;
    bool f[n][n+1];

    fill_n(&f[0][0],n*(n+1),false);
    int start = 0, len = 1;

    f[0][0] = true;
    for(int i=0;i<n;++i)
    {
        f[i][0] = true;
        f[i][1] = true;
    }
    for(int i=n-2;i>=0;--i)
    {
        for(int j=2;j<=n && (i+j-1)<n;++j)
        {
            f[i][j] = f[i+1][j-2] && s[i] == s[i+j-1];
            if(f[i][j] && j > len) {start = i; len = j;}
        }
    }

    return s.substr(start,len);
}

//dp 2
string LongestPalindrome_dp2(const string &s)
{
    const int n = s.size();
    if(n < 2) return s;
    bool f[n][n];

    fill_n(&f[0][0],n*n,false);
    int start=0,len=1;
    f[0][0] = true;

    for(int i=0;i<n;++i)
        f[i][i] = true;

    for(int i = n-1 ; i >= 0; --i)
    {
        for(int j = i+1; j < n;++j)
        {
            if(j == i+1) f[i][j] = (s[i] == s[j]);
            else
                f[i][j] = f[i+1][j-1] && s[i] == s[j];
            if(f[i][j] && (j-i+1) > len) {start = i; len = j-i+1;}
        }
    }

    return s.substr(start,len);
}

方法3: 非常直觀的想法。以每個字符串為中心,計算該字符串左右能夠延伸的部分。註意處理長度為奇數和偶數的情況。

時間復雜度 : O(n^2) 。空間復雜度 : O(1)

//從中間往兩端延伸(考慮奇數偶數的情況就可以)

string LongestPalindrome_extend(const string &s)
{
    const int n = s.size();
    if(n < 2) return s;
    int low,high;
    int start=0,len=1;
    for(int i=1;i<n;++i)
    {
        //even
        low = i-1;
        high = i;
        while(low>=0&&high<n&&s[low]==s[high])
        {
            if(high-low+1>len)
            {
                start=low;
                len=high-low+1;
            }
            --low;++high;
        }
        //odd
        low = i-1;
        high = i+1;
        while(low>=0&&high<n&&s[low]==s[high])
        {
            if(high-low+1 > len)
            {
                start = low;
                len=high-low+1;
            }
            --low;++high;
        }
    }
    return s.substr(start,len); 
}


方法4:使用後綴數組的思想,將字符串s取s的逆,拼接在s的後面,也就是說 如今考察的字符串是 s#s‘。當中的#是額外的一個字符,s‘是s的逆串。求當前這個新拼接而成的字符串的後綴樹組的最長公共前綴。

時間復雜度: O(n^2),空間復雜度 O(n^2)

//關於此方法還沒想明確。暫不貼代碼



方法5: manacher算法。此算法也就是直接參考的上述的方法3,以每個點為中心,來計算左右能夠延伸的部分,可是這個方案存在冗余的比較,manacher則是利用已經有的信息,盡可能的降低冗余的信息。

詳細請參考 點擊打開鏈接。

以下的圖是我對manacher算法的理解,manacher算法事實上就是計算一個數組。數組中的每個元素表示以當前元素為中心的回文的長度。事實上是非常easy的,僅僅要分情況來討論就能夠了。


技術分享

對上述的理解,當前須要計算的位置的index為 i,此時的最右端的位置是right,這個right相應的回文的中心為idx。

分兩種情況來討論:

1 right <= i , 也就是最以下的一幅圖,非常顯然之前計算過的回文的信息對於計算此時的 i 的回文是全然沒有幫助的,也就是說。此時 須要以 i 為中心一個一個的去匹配就可以。

2 right > i , 也就是中間3三幅圖的情況,圖中的 j 表示以 idx 為對稱中心的 i 的對稱點的位置, 顯然 j = 2 * id - i 。

這裏又分兩種情況:

1) 假設以 j 為中心的回文子串的左邊界超出了以idx為中心的回文(圖4)。那麽這時的 i 的回文子串的長度除了至少能夠到達right。 至於超出right的部分,僅僅好一個一個的去匹配了。

2) 假設以j為中心的回文子串的左邊界沒有超出以idx為中心的回文。那麽直接就是j的回文的長度就可以。


也就是說, 假設 i > right ,那麽P[right] = 1;

假設 i < right。此時的 P[i] 的值取決於 i 關於 idx 的對稱點 j 的 P[j]的值 。 假設 i + P[j] > right ,那麽P[i] = right-i ,余下的部分一個一個的去匹配。 假設 i + P[j] < right。那麽P[i] = P[j] ,剩下的一個一個去匹配。


時間復雜度 : O(n), 空間復雜度 O(n).


代碼為:

//Manacher O(n)
string Manacher(const string &str)
{
    //add ‘#‘
    string s = "$";
    for(auto a : str)
    {
        s += ‘#‘;
        s += a;
    }
    s += ‘#‘;
    cout << s << endl;
    const int n = s.size();
    vector<int> P(n,0);
    int right = -1, idx = -1;    //right記錄當前已經計算過的回文的最右邊的邊界(這個邊界是不包括在回文中的)
    for(int i=1;i<n;++i)
    {
        P[i] = (right > i)? min(P[2*idx-i],right-i):1; //這一句就是整個算法的核心!

!!

while(s[i+P[i]] == s[i-P[i]])P[i]++; if(i+P[i]>right) { right = i + P[i]; idx = i; } } auto pos = max_element(P.begin(),P.end()); int len = *pos-1; string ret; int i = pos-P.begin(); //print ret += s[i]; cout << ret << endl; int k=1; while(len) { ret += s[i+k]; ret = s[i-k]+ret; cout << ret << endl; ++k; --len; } //trim # string ret2; for(auto a :ret) if(a!=‘#‘)ret2 += a; return ret2; }


上述代碼均已驗證正確。至於原理,全在代碼中。


最長回文子串的不同解法