一,回文子串
題目鏈接:LCR 020. 回文子串 - 力扣(LeetCode)
?【問題描述】
求一個字符串中有多少個回文子串,其中一個字符也算是一個回文子串。
【解法】
動態規劃
求一個字符串中回文子串的個數,我么可以找到每個回文子串,然后統計個數即可。
首先定義狀態表示:
dp[i]:表示子串[i-j],是否是回文串。其中下標i<=j。
狀態轉移方程的推導:
當s[i]!=s[j]時,也就是子串的第一個字符和最后一個字符不相等,那么肯定不是回文串,所以dp[i][j]=false。
當s[i]==s[j]時,在該情況下,又細分三種情況:
- 如果i==j,那么該子串就是一個字符,是回文串,所以dp[i][j]=true。
- 如果i+1==j,也就是該字符串由兩個字符構成,且s[i]==s[j],是回文串,所以dp[i][j]=true。
- 如果i+1<j,表示s[i]和s[j]中還有一些字符,那么dp[i][j]=dp[i+1][j-1]。
初始化dp表時,根據狀態表示的定義,我們只會用到dp表主對角線的右上部分,左下部分不會用到,對于狀態轉移dp[i][j]=dp[i+1][j-1],我們不需要考慮越界的問題,因為前兩種情況已經做了判斷。
?最后,只需統計dp表中dp[i][j]==true的數量即可。
【代碼】
class Solution {
public:int countSubstrings(string s) {//dp[i]:子串[i-j]是否是回文串int n=s.size();int sum=0;//統計結果vector<vector<bool>> dp(n,vector<bool>(n,false));for(int i=n-1;i>=0;i--)for(int j=i;j<n;j++){if(s[i]!=s[j]){dp[i][j]=false;}else{dp[i][j]=i+1<j?dp[i+1][j-1]:true;}if(dp[i][j])sum++;}return sum;}
};
時間復雜度O(N^2),空間復雜度O(N^2)?
二,最長回文子串
題目鏈接:5. 最長回文子串 - 力扣(LeetCode)
【題目描述】
?求字符串中最長的回文子串。
【解法1】
動態規劃
上一題是求回文子串的個數,我們用一個dp表來表示【i-j】是否是回文串。
本題可以直接使用上題 的思路,如果dp[i][j]是回文串,也就是dp[i][j]==true,那么看看該子串的長度是否是最大的,該子串的長度是j-i+1。可以使用一個變量len來記錄最長的回文子串的長度,begin來記錄該回文串的開始位置i。最后使用substr就可以獲取到該子串。
【代碼】
class Solution {
public:string longestPalindrome(string s) {int n=s.size();vector<vector<bool>> dp(n,vector<bool>(n));int len=0,begin=0;for(int i=n-1;i>=0;i--)for(int j=i;j<n;j++){if(s[i]==s[j])dp[i][j]=i+1<j?dp[i+1][j-1]:true;if(dp[i][j]){if(len<j-i+1){len=j-i+1;begin=i;}}}return s.substr(begin,len);}
};
時間復雜度:O(N^2),空間復雜度O(N^2)
【解法2】
中心擴展算法
求最長的回文子串。我們可以從一個字符出發,求出它作為中心所能形成的最長回文串的長度。
用一個變量i來記錄遍歷到字符串的哪一個為位置。
每遍歷到一個位置,向兩邊擴展,用兩個變量left和right來記錄擴展位置的下標。
回文子串的長度可能是奇數,也可能是偶數,所以需要計算兩次:
1,回文子串的長度是奇數,讓left=i-1,right=i+1,向兩邊擴展,如果s[left]==s[right],
left--,right++。最后回文子串的長度就是right-left-1。
2,回文子串的長度是偶數,讓left=i-1,right=i(left=i,right=i+1也可以),同理,如果s[left]==s[right],left--,right++。最后回文子串的長度就是right-left-1。
記錄這兩種情況下回文子串最長的長度,以及該回文子串開始的下標。
【代碼】
class Solution {
public:string longestPalindrome(string s) {int n=s.size();int begin=0,len=0;for(int i=0;i<n;i++){//奇數擴展int left=i,right=i;while(left>=0&&right<n&&s[left]==s[right]){left--;right++;}if(len<right-left-1){len=right-left-1;begin=left+1;}//偶數擴展left=i-1,right=i;while(left>=0&&right<n&&s[left]==s[right]){left--;right++;}if(len<right-left-1){len=right-left-1;begin=left+1;}}return s.substr(begin,len);}
};
時間復雜度O(N),空間復雜度O(1)?
三,回文串分割IV
題目鏈接:1745. 分割回文串 IV - 力扣(LeetCode)
?【題目描述】
對于一個字符串,將他分為任意的三個子串,如果存在這三個子串都是回文串,返回true,否則返回false
【解法】
將一個字符串分割成三個子串,可以想象成在一個字符串中,切兩刀,分成三部分,而這兩“刀”就是下標,我們用下標i,j將字符串分成三個部分,[0,i-1],[i,j],[j+1,n-1],n為字符串長度。
所以只需判斷這三個子串是否是回文串即可。而在第一道題中我們使用一個dp表來表示[i-j]子串是否是回文串。所以,在本題中,我們可以使用dp表來做預處理,然后枚舉這三個子串:
[0,i-1],[i,j],[j,n-1],判斷這三個子串是否是回文串。
【代碼】
class Solution {
public:bool checkPartitioning(string s) {//dp表存儲回文信息【i,j】int n=s.size();vector<vector<bool>> dp(n,vector<bool>(n));for(int i=n-1;i>=0;i--){for(int j=i;j<n;j++){if(s[i]==s[j])dp[i][j]=i+1<j?dp[i+1][j-1]:true;}}for(int i=0;i<n;i++)for(int j=0;j<n;j++){if(dp[i][j]&&i-1>=0&&j+1<n)if(dp[0][i-1]&&dp[j+1][n-1])return true;}return false;}
};
四,回文串分割II
題目鏈接:LCR 094. 分割回文串 II - 力扣(LeetCode)
【解法】
動態規劃
首先定義狀態表示:
dp[i]:表示【0-i】區間的子串,最少的切割次數。
狀態轉移方程的推導:
如果子串【0-i】本身就是回文串,就不需要切割,即dp[i]=0.
如果子串【0-i】不是回文串,需要切割,我們可以枚舉切割的位置j,0<j<=i。其中不用枚舉j=0,因為j=0,也就是【0-i】區間。j從i開始枚舉,接下來判斷【j-i】是否是回文串:
- 如果【j-i】是回文串,那么就找到了一個分割點,dp[i]就等于【0,j-1】區間的最少分割次數+1,
- 即dp[i]=dp[j-1]+1。
- 如果【j-i】不是回文串,就不是一個分割點,繼續找分割點。
其中求的是最少的分割次數,所以
dp[i]=min(dp[i],dp[j-1]+1)。
上面的過程 中,會一直判斷某一段區間是否是回文串,所以可以用第一道題的思路,用一張表來記錄區間【i-j】是否是回文串。
初始化dp表:
不用判斷數組越界的問題,因為j是大于0的。
由于求的是最小值,所以不能將數組的初始值設為0,會影響最終結果。需要設為無窮大。
【代碼】
class Solution {
public:int minCut(string s) {int n=s.size();//i-j區間是否是回文串vector<vector<bool>> isPal(n,vector<bool>(n,false));for(int i=n-1;i>=0;i--)for(int j=i;j<n;j++){if(s[i]!=s[j]){isPal[i][j]=false;}else{isPal[i][j]=i+1<j?isPal[i+1][j-1]:true;}}//0-i區間最少分割次數vector<int> dp(n,INT_MAX);for(int i=0;i<n;i++){//0-i是回文串if(isPal[0][i]){dp[i]=0;continue;}//枚舉分割點,求最少分割次數for(int j=i;j>0;j--){//j-i是回文串if(isPal[j][i]){dp[i]=min(dp[i],dp[j-1]+1);}}}return dp[n-1];}
};
?時間復雜度O(N^2),空間復雜度O(N^2)
五,最長回文子序列
題目鏈接:516. 最長回文子序列 - 力扣(LeetCode)
【題目描述】
本題與上面的題不同,本題是求最長的回文子序列的長度,子序列是指元素可以不連續,而前面的子串問題,都是要求元素是連續的。
?【解法】
按照常規的思路,我們會定義如下的狀態方程:
dp[i]:表示以第i個元素為結尾的所有子序列中,最長的回文子序列的長度。
要想推導dp[i],一定會與dp[i-1],dp[i-2],dp[i-3]......有關,因為是子序列問題,第i個字符可以拼接到第i-1個字符的后面,也可以拼接到第i個字符的后面......所以會需要前面的dp值來更新dp[i]的值。比如需要通過dp[i-1]來推導dp[i]的值,但是dp[i-1]表示的以第i-1個元素為結尾的最長回文子序列的長度,我們將第i個元素拼接到第i-1個元素的后面,無法判斷拼接后的子序列是否是回文串,所以無法推導,所以這樣的狀態表示是不行的。
在“回文子串”題中,我們判斷一段區間【i-j】是否是回文串,對于這段區間,我們只關心兩個端點的狀態,也就是s[i]和s[j]的關系。本題也類似。
首先定義狀態標識:
dp[i][j]:表示【i-j】區間中最長的回文子序列的長度。
狀態轉移方程的推導:
【i? ? ?i+1? ? ? ?j-1? ? j】? ??
如果s[i]==s[j],有三種情況:
- i==j,表示一個字符,是回文串,長度為1,所以dp[i]=1。
- i+1==j,表示兩個 字符相鄰并且相等,那么回文串的長度就是2,即dp[i]=2。
- i+1<j,表示i和j之間還有其他字符,我們需要求出區間【i+1,j-1】的最長回文子序列的長度,然后再在前面和后面分別拼上s[i]和s[j]即可。所以dp[i]=dp[i+1][j-1]+2。
如果s[i]!=s[j]:
從整體上來看這整個區間【i-j】,【i-j】的回文子序列包含兩部分:
【i,j-1】的回文子序列和【i+1,j】的子序列。所以dp[i][j]=max(dp[i][j-1],dp[i+1][j])。
初始化dp表示:
只需關心dp[i][j]=max(dp[i][j-1],dp[i+1][j])這個表達式中的每一項是否會越界。
前提條件還是i<j,也就是我們只會用到dp表主對角線的右上部分。
而dp[i][j]的更新會用到左邊和下邊的值:
可以發現,這兩個位置其實是主對角線上的元素,滿足i==j,而這種情況我們在前面已經判斷過了,所以不需要初始化。
?
class Solution {
public:int longestPalindromeSubseq(string s) {int n=s.size();//i-j區間的最長回文子序列vector<vector<int>> dp(n,vector<int>(n,0));for(int i=n-1;i>=0;i--)for(int j=i;j<n;j++){if(s[i]==s[j]){if(i==j)dp[i][j]=1;else if(i+1==j)dp[i][j]=2;elsedp[i][j]=dp[i+1][j-1]+2;}else{dp[i][j]=max(dp[i][j-1],dp[i+1][j]);}}return dp[0][n-1];}
};