1、5. 最長回文子串
給定一個字符串 s,找到 s 中最長的回文子串。你可以假設 s 的最大長度為1000。
示例 1:
輸入: "babad" 輸出: "bab" 注意: "aba"也是一個有效答案。
示例 2:
輸入: "cbbd" 輸出: "bb"
(1)動態規划方法
回文字符串的子串也是回文,P[i][j](表示以i開始以j結束的子串)是回文字符串,那么P[i+1][j-1]也是回文字符串。該問題可以分解成一系列子問題。
定義狀態方程和轉移方程:
P[i][j]=0表示子串[i,j]不是回文串 P[i][j]=1表示子串[i,j]是回文串
dp[i][j] = (s[i] == s[j] && dp[i+1][j-1] == true);
class Solution { public: string longestPalindrome(string s) { int len = s.size(); if(len < 2) return s; vector<vector<bool>> dp(len, vector<bool>(len, false)); int start = 0, maxlen = 1; dp[0][0] = true; for (int i = 1; i < len; i++) { dp[i][i] = true; dp[i][i-1] = true; //這個初始化容易忽略,當k=2時要用到 } for(int k=2; k<=len; ++k) { // 枚舉子串的長度 for(int i=0; i<=len-k; ++i) { // 枚舉子串起始位置 int j = i+k-1; if(s[i] == s[j] && dp[i+1][j-1]) { dp[i][j] = true; start = i; // 記錄回文子串的起點和長度 maxlen = k; } } } return s.substr(start, maxlen); } };
(2)中心擴展
以某個元素為中心,分別計算偶數長度的回文最大長度和奇數長度的回文最大長度。時間復雜度O(n^2),空間O(1)
中心擴展就是把給定的字符串的每一個字母當做中心,向兩邊擴展,這樣來找最長的子回文串。算法復雜度為O(N^2)。
class Solution { public: string longestPalindrome(string s) { int len = s.size(); if(len < 2) return s; int start = 0, maxlen = 1; for(int i=1;i<len;++i) { int low = i-1, high = i; //尋找以i-1,i為中點偶數長度的回文 while(low>=0 && high<len && s[low]==s[high]) { low--; high++; } if(high-low-1 > maxlen) { maxlen = high-low-1; //應該是 high-low+1-2 = high-low-1 start = low + 1; } low = i-1, high = i+1; //尋找以i為中心的奇數長度的回文 while(low>=0 && high<len && s[low] == s[high]) { low--; high++; } if(high-low-1 > maxlen) { maxlen = high-low-1; start = low +1; } } return s.substr(start, maxlen); } };
2、516. 最長回文子序列
給定一個字符串s
,找到其中最長的回文子序列。可以假設s
的最大長度為1000
。
示例 1:
輸入:"bbbab"
輸出:4
一個可能的最長回文子序列為 "bbbb"。
示例 2:
輸入:"cbbd"
輸出:2
一個可能的最長回文子序列為 "bb"。
這道題給了我們一個字符串,讓我們求最大的回文子序列,子序列和子字符串不同,不需要連續。而關於回文串的題之前也做了不少,處理方法上就是老老實實的兩兩比較吧。像這種有關極值的問題,最應該優先考慮的就是貪婪算法和動態規划,這道題顯然使用DP更加合適。我們建立一個二維的DP數組,其中dp[i][j]表示[i,j]區間內的字符串的最長回文子序列,那么對於遞推公式我們分析一下,如果s[i]==s[j],那么i和j就可以增加2個回文串的長度,我們知道中間dp[i + 1][j - 1]的值,那么其加上2就是dp[i][j]的值。如果s[i] != s[j],那么我們可以去掉i或j其中的一個字符,然后比較兩種情況下所剩的字符串誰dp值大,就賦給dp[i][j],那么遞推公式如下:
/ dp[i + 1][j - 1] + 2 if (s[i] == s[j])
dp[i][j] =
\ max(dp[i + 1][j], dp[i][j - 1]) if (s[i] != s[j])
對於任意字符串,如果頭尾字符相同,那么字符串的最長子序列等於去掉首尾的字符串的最長子序列加上首尾;如果首尾字符不同,則最長子序列等於去掉頭的字符串的最長子序列和去掉尾的字符串的最長子序列的較大者。
因此動態規划的狀態轉移方程為:
設字符串為str,長度為n,p[i][j]表示第i到第j個字符間的子序列的個數(i<=j),則:
狀態初始條件:dp[i][i]=1 (i=0:n-1)
狀態轉移方程:dp[i][j]=dp[i+1][j-1] + 2 if(str[i]==str[j])
dp[i][j]=max(dp[i+1][j],dp[i][j-1]) if (str[i]!=str[j])
class Solution { public: int longestPalindromeSubseq(string s) { int n = s.size(); vector<vector<int>> dp(n, vector<int>(n)); for (int i = n - 1; i >= 0; --i) { dp[i][i] = 1; for (int j = i + 1; j < n; ++j) { if (s[i] == s[j]) { dp[i][j] = dp[i + 1][j - 1] + 2; } else { dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); } } } return dp[0][n - 1]; } };
3、647. 回文子串
給定一個字符串,你的任務是計算這個字符串中有多少個回文子串。
具有不同開始位置或結束位置的子串,即使是由相同的字符組成,也會被計為是不同的子串。
示例 1:
輸入: "abc" 輸出: 3 解釋: 三個回文子串: "a", "b", "c".
示例 2:
輸入: "aaa" 輸出: 6 說明: 6個回文子串: "a", "a", "a", "aa", "aa", "aaa".
在剛開始的時候博主提到了自己寫的DP的方法比較復雜,為什么呢,因為博主的dp[i][j]定義的是范圍[i, j]之間的子字符串的個數,這樣我們其實還需要一個二維數組來記錄子字符串[i, j]是否是回文串,那么我們直接就將dp[i][j]定義成子字符串[i, j]是否是回文串就行了,然后我們i從n-1往0遍歷,j從i往n-1遍歷,然后我們看s[i]和s[j]是否相等,這時候我們需要留意一下,有了s[i]和s[j]相等這個條件后,i和j的位置關系很重要,如果i和j相等了,那么dp[i][j]肯定是true;如果i和j是相鄰的,那么dp[i][j]也是true;如果i和j中間只有一個字符,那么dp[i][j]還是true;如果中間有多余一個字符存在,那么我們需要看dp[i+1][j-1]是否為true,若為true,那么dp[i][j]就是true。賦值dp[i][j]后,如果其為true,結果res自增1,
class Solution { public: int countSubstrings(string s) { int n = s.size(); int res=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) { dp[i][j] = (s[i]==s[j]) && ((j-i)<=2 || dp[i+1][j-1]); if(dp[i][j]) res++; } } return res; } };
4、131. 分割回文串
給定一個字符串 s,將 s 分割成一些子串,使每個子串都是回文串。
返回 s 所有可能的分割方案。
示例:
輸入: "aab" 輸出: [ ["aa","b"], ["a","a","b"] ]
這又是一道需要用DFS來解的題目,既然題目要求找到所有可能拆分成回文數的情況,那么肯定是所有的情況都要遍歷到,對於每一個子字符串都要分別判斷一次是不是回文數,那么肯定有一個判斷回文數的子函數,還需要一個DFS函數用來遞歸,再加上原本的這個函數,總共需要三個函數來求解。
那么,對原字符串的所有子字符串的訪問順序是什么呢,如果原字符串是 abcd, 那么訪問順序為: a -> b -> c -> d -> cd -> bc -> bcd-> ab -> abc -> abcd, 這是對於沒有兩個或兩個以上子回文串的情況。那么假如原字符串是 aabc,那么訪問順序為:a -> a -> b -> c -> bc -> ab -> abc -> aa -> b -> c -> bc -> aab -> aabc,中間當檢測到aa時候,發現是回文串,那么對於剩下的bc當做一個新串來檢測,於是有 b -> c -> bc,這樣掃描了所有情況,即可得出最終答案。
class Solution { public: vector<vector<string>> partition(string s) { vector<vector<string>> res; if(s.empty()) return res; vector<string> temp; partitionDfs(s,0,temp,res); return res; } void partitionDfs(string &s,int start,vector<string> &temp,vector<vector<string>> &res) { if(start == s.size()) { res.push_back(temp); return; } for(int i=start;i<s.size();++i) { if(isPartition(s,start,i)) { temp.push_back(s.substr(start,i-start+1)); partitionDfs(s,i+1,temp,res); temp.pop_back(); } } } bool isPartition(string s,int start,int end) { while(start<end) { if(s[start++] != s[end--]) return false; } return true; } };