LeetCode #5. 最大回文子串
I. Description:
II. Solution:
version 1
解题思路:
首先想到的是暴力破解,暴力破解的时间复杂度较高,但思路清晰。理清暴力解法的思路分析其优缺点,有时可以为后续的优化打开思路。具体的思路如下:
- 遍历给定的字符串中长度为2及以上的所有子串(长度为1的单个字符无须遍历,若前面的过程未找到,则自然最长的回文子串是任意的一个单个字符),然后依次判断他们是否符合回文字样
- 在上面遍历的过程中,可以做一个小小的优化。即,遍历的过程中记录下当前已经遍历得到的最长回文子串的长度 m a x L e n maxLen maxLen。可以只针对长度大于当前已经遍历得到的最长回文子串长度的子串进行遍历,因为最终的目的是获取最长的回文子串,这样可以跳过较多的更短的子串而无须去判断。
- 记录回文子串的时候可以选择只记录子串的起点位置和对应的长度
Code:
- cpp
class Solution {
public:
string longestPalindrome(string s) {
int L = s.size();
if (L <= 1) {
return s;
}
/* 遍历所有长度大于2的子串 */
int curLen; // 用于记录当前遍历得到的子串长度
int curMaxLen = 1; // 用于记录当前遍历得到的最大回文子串的长度
string res = s.substr(0, curMaxLen);
for (int i = 0; i <= L-2; i++) {
for (int j = i + 1; j <= L-1; j++) {
curLen = j - i + 1;
if (curLen > curMaxLen && isPalindromestring(s, i, j)) {
res = s.substr(i, curLen);
curMaxLen = curLen;
}
}
}
return res;
}
bool isPalindromestring(const string& str, int l, int r) {
while(l < r) {
if (str[l] == str[r]) {
l++;
r--;
} else {
return false;
}
}
return true;
}
};
version 2
解题思路:
第二种想到的解法是使用动态规划的思路。本题求解的思路主体上是,遍历其每个子串,然后判断其是否符合回文格式,因此需要找到一种可以判断其所有子串是否为回文子串的方法,于是想到了使用动态规划的思路。
动态规划算法的主要思路是考虑清楚状态转移方程,也就是确定用来保存历史记录的数组元素之间的关系式。可以从回文字符串本身的形式考虑:
- 若一个字符串的首尾两个字符不相等,则该字符串肯定不是回文字串
- 若一个字符串的首尾两个字符相等,则该字符串的回文性取决于里边子串的回文性
也就是说,在首尾字符相同的情况下,里边子串的回文性决定了整个字符串的回文性,这就是状态的转移,因此可以考虑将数组中的元素定义为其中的一个子串是否是回文字串。
按照动态规划求解的三个步骤如下:
-
定义数组元素的含义
上面说过,可以将数组中的元素定义为其中的一个子串是否是回文字串(true或false),本题应使用一个二维数组 d p [ ] [ ] dp[][] dp[][] 来保存历史记录,其中数组的行和列的大小均为原字符串的长度(因为子串的首尾都能取到长度上的每个位置),其中dp [ i ] [ j ] [i][j] [i][j]表示的是子串s [ i . . j ] [i..j] [i..j]的回文性,s[i]和s[j]均可取到即为闭区间。 -
找出数组元素之间的关系式,也就是状态转移方程
根据上面的分析,只有在首尾字符相同的情况下才存在状态转移,即得到如下关系式: d p [ i ] [ j ] dp[i][j] dp[i][j] = ( s [ i ] = = s [ j ] ) (s[i] == s[j]) (s[i]==s[j]) && d p [ i + 1 ] [ j − 1 ] dp[i + 1][j - 1] dp[i+1][j−1] ,即我们是在一个子串的首尾字符相同的情况下采取考虑状态转移的。
动态规划的实质,就是填写一张二维的表格,表格的长宽都是原字符串的长度 l l l 。同时,还需要考虑边界问题,因为在 s [ i ] = = s [ j ] s[i] == s[j] s[i]==s[j] 的情况下,若 s [ i . . . j ] s[i...j] s[i...j] 子串的长度等于2或者3的时候即 j − i < = 2 j - i <= 2 j−i<=2,则可以直接下结论:此时的 s [ i . . . j ] s[i...j] s[i...j] 就是一个回文字符串,即 d p [ i ] [ i ] = t r u e dp[i][i] = true dp[i][i]=true。 -
确定初始化
因为子串的首尾满足 i < = j i<=j i<=j,因此只需填写 d p [ ] [ ] dp[][] dp[][] 这个二维数组(这张“表格”)的对角线的上半部分即可。而初始条件就是,对角线上的元素值 d p [ i ] [ i ] dp[i][i] dp[i][i],可想而知对角线上的元素代表的是原字符串上的每个单独的字符,即肯定有 d p [ i ] [ i ] = t r u e dp[i][i] = true dp[i][i]=true。
需要注意的是,较大的子串的回文判断需要依赖较小的子串的回文结果,因此填表顺序也是很重要的。
Code:
- cpp
class Solution {
public:
string longestPalindrome(string s) {
if (s.size() < 2) {
return s;
}
int L = s.size();
int start = 0, curLen = 1, maxLen = 1;
vector<vector<int>> dp(L, vector<int>(L));
// 初始化
for (int i = 0; i < L; i++) {
dp[i][i] = 1;
}
for (int j = 1; j < L; j++) {
for (int i = 0; i < j; i++) {
if (s[i] == s[j])
{
// 子串首尾相同且间隔小于3的情况下,可直接判断此子串是回文的
if (j - i < 3) {
dp[i][j] = 1;
} else {
dp[i][j] = dp[i+1][j-1];
}
}
if (dp[i][j] == 1) {
curLen = j - i + 1;
if (curLen > maxLen) {
maxLen = curLen;
start = i;
}
}
}
}
return s.substr(start, maxLen);
}
};