最長迴文子串的不同解法

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

方法1 : 很簡單,枚舉所有的區間 [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算法其實就是計算一個數組,數組中的每一個元素表示以當前元素爲中心的迴文的長度。其實是很簡單的,只要分情況來討論就可以了。



對上述的理解,當前需要計算的位置的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;
}

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


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