代码题(17)— 最长回文子串、最长回文子序列、回文子串、分割回文串(2)
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; } };