1. 程式人生 > 其它 >刪去字串中的子串_leetcode 5 最長迴文子串(c++)

刪去字串中的子串_leetcode 5 最長迴文子串(c++)

技術標籤:刪去字串中的子串

e989673cd522f320e172eef05023ebb2.png

第一種直接利用動態規劃,主要是寫好狀態轉移方程,第二種是中心擴充套件法,第三種是manacher馬拉車(經評論區提醒後新增)。


### 題目

給定一個字串 s,找到 s 中最長的迴文子串。你可以假設s的最大長度為1000。

示例 1:

輸入: "babad"
輸出: "bab"
注意: "aba" 也是一個有效答案。
示例 2:

輸入: "cbbd"
輸出: "bb"

### 思路

主要是要找好動態轉移方程,定義一個長寬等於字串長度的二維陣列,其中若二維陣列dp [i][j]的值為1,代表在s中從下標i開始到下標j這段長度的字元子串是迴文字串。

首先,一個字元肯定是迴文子串,所以將dp[i][i](i從0到字串長度)都設定為1,然後開始遍歷,遍歷過程中主要就看轉移方程了,假設有s[i]==s[j],那表明它滿足了新增的兩個字元相等的條件,接下來就是看這兩個字元往內層的子串是否也是迴文的,所以只要dp[i+1] [j-1]為1,代表從i+1到j-1都是滿足迴文的,那麼s[i]如果等於s[j],那麼迴文子串就可以加上兩個新成員,還有一種情況就是它們兩個字元是相鄰的,那麼只要有j-1==1,也可以代表這是一個新增的長度為2的新迴文子串。

最後再找出長度最長的迴文子串輸出就可以了,但是這個的時間複雜度為n*2,因為有兩重巢狀迴圈,空間複雜度也是n**2,狀態轉移方程為:

dp[l] [r] = (s[l]==s[r] && (r-l==1 || dp[l+1] [r-1])) ? true : false

### code

class Solution {
    public:
    string longestPalindrome(string s) {
        //動態規劃做
        int l=s.size();
        if(l==0)
            return "";
        int max_l=0,max_r=0;
        vector<vector<int>>dp(l,vector<int>(l,0));
        for(int i=0;i<l;++i)
            dp[i][i]=1;
        for(int right=1;right<l;++right)
        {
            for(int left=0;left<right;++left)
            {
                if(s[left]==s[right]&&(right-left==1||dp[left+1][right-1]))
                {
                    dp[left][right]=1;
                    if(right-left>max_r-max_l)
                    {
                        max_r=right;
                        max_l=left;
                    }
                }
            }
        }
        return s.substr(max_l,max_r-max_l+1);    
    }
    };

### 思路

第二種思路就是中心擴充套件法,速度快了5倍左右吧,思想也比較容易懂,就是依次將每個字串中的字元作為一個可能存在的迴文子串的中心字元,那麼中心有兩種中心方式:


2.1,迴文子串長度是奇數,那麼就是往兩邊擴充套件的,假設中心字元下標為i,那麼只要有下標為i-1和i+1的兩個字元相等,那麼迴文子串的長度加2,並且繼續一個往前,一個往後遍歷,也就是說繼續比較下標為i-2和i+2的兩個字元,直到找到不相等的或者說到達字串邊界,那麼代表以s[i]為中心的最長迴文子串找到了。


2.2,迴文子串的長度是偶數,那麼就先對比s[i]以及s[i+1],如果兩者相等,那麼繼續比較s[i-1]和s[i+2],直到超出字串的邊界或者不相等,這樣就能找到最長的迴文子串,s[i]可能是中中心,也可能是右中心,但是這個不用分別計算,因為s[i]如果是這個子串的右中心,那麼就一定是下一個字元的子串的左中心,所以不會漏掉。

### code

 class Solution {
    public:
    int f(string &s,int i,int len)
    {
        int l1=1;
        int l2=1;
        int l=i-1;
        int r=i+1;
        while(l>=0&&r<len)
        {
            if(s[l]==s[r])
                l1+=2;
            else
                break;
            l--;
            r++;
        }
        for (l = i, r = i+1; l >= 0 && r < s.size() && s[l] == s[r]; l--, r++);
        l2 = r - l - 1;
        return max(l1, l2);
    }
    string longestPalindrome(string s) {
        //中心擴充套件法
        int len=s.size();
        if(len<=1)
            return s;
        int max_len=0,start=0;
        for(int i=0;i<len;++i)
        {
            int temp=f(s,i,len);
            if(temp>max_len)
            {
                max_len=temp;
                start=i-(temp-1)/2;
            }
        }
        return s.substr(start,max_len);     
    }
    };

### 思路

上訴提到的都是有冗餘計算的,這是因為迴文串長度的奇偶性造成了不同性質的對稱軸位置,前面的解法會當成兩種情況處理,可看下面幾張圖:

中心拓展法:

9c6a40a8e86ffa2e7e6a19b4ef2d744f.png

動態規劃:

b26cdb18930ab9743d0dbd2556cf4bf7.png

馬拉車:

41d6cfe9856aeddbe0c4136bdc53773f.png

這是因為由很多子串會被重複的訪問,例如下面這個字串:

e695c40103610b69b6557b96d9bc2f76.png

如果利用上面最快的擴充套件法,我們可以看到以d為中心的時候,aba被遍歷過一次,以b為中心的時候,兩個a又被遍歷了一次,Manacher可以避免這些計算。

首先是第一步,為了避免奇偶兩種情況,對字串做一個預處理,在每個字元的兩邊插上一個特殊字元,要求這個特殊字元是不會在原串中出現的。這樣會使得所有的串都是奇數長度的,因為假設原字串長度為n,現在字串長度為3*n,肯定奇數,並且迴文串的中心肯定是個奇數,因為每個原來的字元至少兩邊有個相等的特殊字元。

第二步就是解決重複訪問的問題,因為迴文串的中心為單數,我們用temp[i]表示以i位置為中心的迴文字串最左或者最右到達i的長度,用temp[i] 表示以第 i 個字元為對稱軸的迴文串的迴文半徑,因為處理時從左往右的,所以就定義temp[i] 為第 i 個字元為對稱軸的迴文串的最右一個字元與字元 i 的距離,加上本字元,具體如下:

40530eae729e87168a400f1fe25b5ae9.png

可以通過上表看出,其實temp[i]-1就是以i為中心的最長迴文字串,所以只要求出temp陣列,基本就可以搞定了所以引入一個輔助變數 Max_Right,表示以及訪問了的所有迴文子串能到達的最右的一個字元的位置。另外還要記錄下 Max_Right 對應的迴文串的對稱軸所在的位置,記為 pos,具體的位置如下所示:

a6f5c5bb7322749a19ef04c258bb6e93.png

我們從左往右地訪問字串來求temp,假設當前訪問到的位置為i。對應上圖,因為我們是從左到右遍歷i, 而pos是遍歷到的所有迴文子串中某個對稱軸位置(MaxRight最大時),所以必然有pos<=i,然後要處理的情況就是i在max_right左邊還是右邊。

1、i在max_right右邊,如下圖所示:

56f4c4f0856742214bbbbba559cd7e8f.png

這種情況下因為s[i]左右的字串都未處理過,所以只能讓Temp[i]=1,後續在做處理。

2、i在max_right的左邊

2.1 i關於pos對稱的那個點的最遠字元未超過以pos為中心的迴文字串

fd855d293c1b738df2d58b5e7b25fc1a.png

點j是點i關於pos的對稱點,因為pos是迴文字串的中心,所以i與j肯定相同,並且i與j兩邊的元素都是相同的,所以只要j的迴文字串在在以pos點為中心的迴文字串的範圍內,那麼肯定以i為中心的迴文字串也不會超出以pos點為中心的迴文字串,所以直接讓temp[i]=temp[j]就可以了。

2.2 i關於pos對稱的那個點的最遠字元超過以pos為中心的迴文字串

af5ac4d944e180160c8d07a5be48bfe2.png

同理,點j是點i關於pos的對稱點,因為pos是迴文字串的中心,所以i與j肯定相同,並且i與j兩邊的元素都是相同的,但是以j為中心的迴文字串已經包含以pos為中心的迴文字串外面的元素了,所以依靠以pos為中心的迴文字串所推匯出來的i與j附近元素相等這一結論不適用於字串外的元素,所以只能temp[i]=max_right-i,代表通過以pos為中心的字串所能保證的最遠距離。

所以就得到對於temp的賦值程式:

if(i<max_right)temp[i]=min(temp[2*pos-i],max_right-i);          
else temp[i]=1;

然後就是對資料進行更新,temp[i]的值因為不知道是(可以針對每個情況單獨寫)哪一種情況下賦值的,所以統一要往外擴充套件,擴充套件結束之後需要更新這四個max_right、 pos、res、res_pos。其中res代表最長的temp[i],res_pos儲存i。

最後返回最終的結果就是

s.substr((res_pos-res+1)/2,res-1);

因為上訴提到,temp[i]-1等於該位置的元素在原來的陣列中的迴文字串長度,而res_pos-res+1可以得出以i為中心,以temp[i]-1為長度的字串的第一個元素的位置。


### code

class Solution {
public:
string longestPalindrome(string s)
{
if(s.size()==0)
    return "";
string s1;
for(auto a:s)
{
    s1=s1+'#';
    s1=s1+a;
}
s1=s1+'#';
int l=s1.size();
vector<int>temp(l,0);
int max_right=0,pos=0,res=0,res_pos=0;
for(int i=0;i<l;++i)
{
    if(i<max_right)temp[i]=min(temp[2*pos-i],max_right-i);
    else temp[i]=1;
    while(i-temp[i]>=0&&i+temp[i]<l&&s1[i-temp[i]]==s1[i+temp[i]])
        temp[i]++;
    if(i+temp[i]-1>max_right)
        {max_right=i+temp[i]-1;
        pos=i;}
    if(res<temp[i])
    {
        res=temp[i];
        res_pos=i;
    }
}
return s.substr((res_pos-res+1)/2,res-1);
}
};