本文主要針對leetcode上關於回文字串的演算法題進行總結。
總體而言,leetcode上關於回文字串的演算法題可以分為以下幾類:
(1)在已知的字串串找到最長的回文字串
(2)修改現有的字串得到回文字串
(3)分割現有的字串得到回文字串
涉及題目:
5. 最長回文子串
131. 分割回文串
132. 分割回文串 ii
214. 最短回文串
516. 最長回文子串行
647. 回文子串
1278. 分割回文串 iii
1312. 讓字串成為回文串的最少插入次數
1745. 回文串分割 iv
1771. 由子串行構造的最長回文串的長度
常用方法:中心擴充套件,動態規劃,回溯,manacher馬拉車演算法
修改現有的字串得到回文字串
分割現有的字串得到回文字串
找到最長的回文子串最基本的就是兩種,一則子串連續,二則子串不連續。其他最長回文子串都是關於這兩者的變體。
之前整理過這個題目的4種解法的c++和python版本,常見的方法有暴力法,中心擴充套件法,動態規劃法,manacher(馬拉車)法。其中中心擴充套件法最容易理解,也最推薦,動態規劃比較經典需要掌握,而馬拉車演算法效率最高,學有餘力的學習。
這裡講解一下中心擴充套件法和動態規劃法
中心擴充套件法
依次遍歷每一位作為中心,然後左右指標往兩邊走,找到每個中心對應的最長子字串。
class
solution
return ans;
} string expandtocenter
(string &s,
int left,
int right)
return s.
substr
(left+
1, right-left-1)
;}};
動態規劃法
首先是容易理解的二維dp
/*
dp[i][j]:對於子字串s[i...j]是否為回文串
s[i] == s[j] : dp[i][j] = dp[i+1][j-1]
base: dp[i][i] = true; dp[i][i+1] = (s[i] == s[i+1])
*/class
solution
}return ans;}}
;
對二維dp進行狀態壓縮
class
solution
}return ans;}}
;
這道題裡面中心擴充套件法就失效了,主要是動態規劃法
動態規劃法
首先是容易理解的二維dp
/*
dp[i][j]:對於子字串s[i...j],最長的回文子串行長度
s[i] == s[j] : dp[i][j] = dp[i+1][j-1] + 2
s[i] != s[j] : dp[i][j] = max(dp[i+1][j], dp[i][j-1])
*/class
solution
}return dp[0]
[n-1];
}};
狀態壓縮後
class
solution
}return dp[n-1]
;}};
這道題其實就在基礎問題二的乙個變體,把word1和word2合起來找最長不連續回文子串行,只不過這裡要保證word1和word2裡面至少取了乙個字元。那我們直接對於word1中某個字元c,從右開始找word2中第乙個c的位置,然後求這兩個位置之間的不連續回文子串行。
有兩點優化,一則word1種每種字元只需要針對左邊第乙個出現的位置找;二則用乙個minn保留c出現的最右邊的位置,如果當前字元c2第乙個出現的位置在minn之前,那麼得到的子字串肯定比之前小,這種情況也不用計算。
class
solution
return maxn;
}int
longestpalindromesubseq
(string s,
int left,
int right)
}return dp[n-1]
;}};
表面上看這道題是修改,但是研讀一下發現只要首部插入,其實變相就是求原字串最長的回文字首(連續)。
這道題用動態規劃和中心擴充套件都會超時,這裡用馬拉車演算法。這塊自己還沒完全搞懂,等後面消化了再把馬拉車好好理一下,先貼乙個我覺得講得最透徹的題解。
具體參考:最短回文串的manacher解法
求所有回文子串的個數,這題是問題一(連續回文子串)的應用,在原有的基礎上加上計數即可。這裡只寫乙個最方便的中心擴充套件法。
class
solution
return ans;
}void
expandtocenter
(string &s,
int left,
int right)}}
;
常見的修改主要包括插入、刪除、替換。
而上一節中的找不連續子串其實是刪除操作,同理插入也是動態規劃,只不過動態規劃方程不太一樣。
替換沒有找到對應題目,或者說如果要求某個字串需要替換多少個才能構成回文串其實是很簡單的,只要左右指標遍歷,記錄不同的個數即可。第3節的回文串分割3中涉及到替換操作,用dp記錄各個子串需要替換的次數,具體參考1278. 分割回文串 iii。
這裡修改主要講一下插入操作。
/*
dp[i][j]:對於子字串s[i...j],構成回文串的最少插入次數
s[i] == s[j}: dp[i][j] = dp[i+1][j-1]
s[i] != s[j]: dp[i][j] = min(dp[i+1][j], dp[i][j-1])
*/class
solution
}return dp[0]
[n-1];
}};// 狀態壓縮
class
solution
}return dp[n-1]
;}};
這節有點八仙過海各顯神通的意味了,主要是回溯和動態規劃。這裡把回文串分割這個系列具體問題具體分析下。
要給出所有的分割方案,用回溯是最佳的
class
solution
for(
int j = cur; j < s.
length()
;++ j)
++ k;}if
(flag)}}
vector>
partition
(string s)
};
這題理論上也可以用131的回溯法,然後找到最短分割次數,但是這種方法勢必會帶來重複運算,比如對於最少分割次數中,有一段為s[left,right],但是回溯還是會把s[left,right]中所有的分割方法都揪出來,且不大容易剪枝。因此這裡考慮動態規劃, 對子串[left,right]已經滿足回文要求的不再分割。
/*
dp[i]: s[0...i]分割成回文子串的最短次數
s[0...i]為回文子串:dp[i]=0
s[0...i]不是回文子串:dp[i] = min(dp[i], dp[j]+1), 其中s[j+1...i]為回文串
*/class
solution
} vector<
int>
dp(n, int_max)
;for
(int i =
0; i < n;
++ i)
}return dp[n-1]
;}};
這題有點特殊,涉及到修改字元。如果不考慮修改字元的話,本題即判斷字串能否分割成k個不為空的回文子串,容易想到動態規劃。
/*
dp[i][k]: s[0...i]能否分割成k個不為空的字串
dp[i][k] = dp[j][k-1] 其中s[j+1...i]為回文子串
*/
進一步想,要求最小的修改次數的話,用cost(s, j+1, i)表示將s[j+1…i]改為回文子串的修改次數。這個cost函式其實就是修改中的替換操作,可以用動態規劃提前記錄下所有的cost
/*
dp[i][k]: s[0...i]能否分割成k個不為空的字串需要修改的次數
dp[i][k] = min(dp[j][k-1]+cost(s,j+1,i))
*/class
solution
} dp[0]
[0]=
0;for(
int i =
1; i <= n;
++ i)
}return dp[n]
[k];}}
;
判斷原字串能否分割成3個回文子串,動態規劃+暴搜,在5. 最長回文子串得到各個子字串是否為回文串的基礎上,暴力搜尋兩個分隔線p和q即可
class
solution
}for
(int p =
0; p < n-1;
++ p)
}return
false;}
};
團滅 LeetCode 打家劫舍問題
有讀者私下問我 leetcode 打家劫舍 系列問題 英文版叫 house robber 怎麼做,我發現這一系列題目的點讚非常之高,是比較有代表性和技巧性的動態規劃題目,今天就來聊聊這道題目。打家劫舍系列總共有三道,難度設計非常合理,層層遞進。第一道是比較標準的動態規劃問題,而第二道融入了環形陣列的...
團滅 LeetCode 打家劫舍 問題
leetcode 打家劫舍 系列問題共有三道 198.打家劫舍 213.打家劫舍ii 337.打家劫舍iii int rob vector nums 建模 給定陣列 nums中都是正整數,nums中相鄰的數不能同時取,制定一種取數策略,使得取到的nums中的數和最大,返回取到的數的最大和。思路 題目...
LeetCode 驗證回文串
題目描述 給定乙個字串,驗證它是否是回文串,只考慮字母和數字字元,可以忽略字母的大小寫。說明 本題中,我們將空字串定義為有效的回文串。示例 1 輸入 a man,a plan,a canal panama 輸出 true 示例 2 輸入 race a car 輸出 false class solut...