题目链接:https://leetcode.com/problems/longest-palindromic-substring/
题目难度:Medium
题目描述:
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
Example 1:
Input:
"babad"
Output:"bab"
Note: “aba” is also a valid answer.
Example 2:
Input:
"cbbd"
Output:"bb"
相关主题:String, Dynamic Programming
思路 1 – Brute Force
“回文”(palindromic)字符串的特点是逆序后和原来一样。一种思路是遍历 s
的所有子串,然后挨个判断是否是回文字符串。判断回文字符串时,如果对每个字符串都单独考虑,这样判断的时间复杂度将是 。遍历所有子串的时间复杂度又是 ,合起来复杂度会达到 ,这样实现会“Time Limit Exeeded”。
思路 2 – Expand Around Center
回文字符串还有一个重要的特点,回文字符串去掉首尾的字符后,剩余的子串也是回文字符串。可以利用这个特点,将这个判断回文的过程和遍历子串的过程结合起来,每一步都利用前一步的结果,来减少重复的计算量。·
我的思路是遍历回文字符串的轴(axis
),然后以轴为中心两个指针 left
和 right
分别向两侧扩展,寻找对称的字符,并统计符合条件的最长子串。例如字符串 "babad"
,最开始 axis = 0.5
,即以 b
和 a
的空隙为轴(" b | a b a d "
),left
指向第 1 个字符 b
,right
指向第 2 个字符 a
,没有回文字符串;下一次遍历时轴为 axis = 1
,即以第一个字符 a
为轴(即 " b |a| b a d "
),left
指向第 1 个字符 b
,right
指向第 3 个字符 b
,存在回文字符串 "bab"
,继续向两侧扩展……以此类推。
时间复杂度:
空间复杂度:
// C++
class Solution {
public:
string longestPalindrome(string s) {
int len = s.size();
if (len <= 1) {
return s;
}
int start = 0, max_len = 1;
for (double axis = 0.5; axis <= len - 1.5; axis = axis + 0.5) {
int left, right;
if (((int)(axis * 2)) % 2 == 0) {
left = (int)(axis - 1);
right = (int)(axis + 1);
} else {
left = (int)axis;
right = (int)(axis + 0.5);
};
while (left >= 0 && right < len && left < axis && right > axis) {
if (s[left] == s[right]) {
left--;
right++;
if (!(left >= 0 && right < len)) {
int temp_len = right - 1 - (left + 1) + 1;
if (temp_len > max_len) {
start = left + 1;
max_len = temp_len;
}
}
} else {
int temp_len = right - 1 - (left + 1) + 1;
if (temp_len > max_len) {
start = left + 1;
max_len = temp_len;
}
break;
}
}
}
return s.substr(start, max_len);
}
};
一时没有想起其他的思路,然后看了一下相关主题,发现和“动态规划(Dynamic Programming)”有关。之前没有了解过动态规划,所以又先去学习了一下,见《动态规划学习笔记》。
思路 3 – Dynamic Programming
按照动态规划的思想,我们可以把重叠的子问题结果存储下来,避免重复的计算。
刚开始实现了一个利用动态规划判断一个字符串是否为回文字符串的版本,然后遍历所有子串,依次判断是否为回文字符串,记录最大长度。我的实现在遍历子串时,每次都是先固定子串起始位置 i
,然后增长子串长度直至最大。这就导致中间又重复了一些计算,例如考虑 s.substr(0, 3)
也会考虑 s.substr(1, 2)
的情况,但是之后又遍历到了。最终导致提交后“Memory Limit Exceeded”。
参考了 LeetCode 的官方 Solution 和 GeeksforGeeks 上的动态规划实现,采用的是表格法。核心思想是如果一个字符串是回文字符串,那么去掉首尾两个字符后,剩余的字符串也应该是回文字符串。例如,cabac
为回文字符串,那么 aba
也是回文字符串。而这里我们自底向上考虑,先新建一个 table
用来存储“子问题”的结果,然后从最简单的情况出发,即回文字符串长度为 1 和 2 的情况,依次填充 table
。在此基础上,考虑回文字符串长度可能为 3 的情况,为 4 的情况……以此类推。这样在遍历的时候,我们考虑的回文字符串的长度是逐渐增加的。
时间复杂度:
空间复杂度:
// C++
#define NIL -1
#define YES 1
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
if (n <= 1) {
return s;
}
int table[n][n];
memset(table, NIL, sizeof(table));
int max_len = 1;
for (int i = 0; i < n; i++) {
table[i][i] = YES;
}
int start = 0;
for (int i = 0; i < n-1; i++) {
if (s[i] == s[i+1]) {
table[i][i+1] = YES;
if (max_len < 2) {
start = i;
max_len = 2;
}
}
}
for (int k = 3; k <= n; k++) {
for (int i = 0; i < n-k+1; i++) {
int j = i + k - 1;
if (table[i+1][j-1] == YES && s[i] == s[j]) {
table[i][j] = YES;
if (k > max_len) {
start = i;
max_len = k;
}
}
}
}
return s.substr(start, max_len);
}
};
TODO
还有其他更厉害的思路,有时间再看一下。
- 思路 4 – Using Palindromic Tree
- 思路 5 – Manacher’s Algorithm
2019年04月08日