LeetCode(5):最长回文子串
Medium!
题目描述:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 长度最长为1000。
示例:
输入: "babad" 输出: "bab" 注意: "aba"也是有效答案
示例:
输入: "cbbd" 输出: "bb"
回文串概念:
“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。
回文算法描述:
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 int main() 6 { 7 string str; 8 int i,j,l; 9 int flag = 1; 10 11 while (cin >> str) 12 { 13 l = str.length(); 14 for (i = 0,j = l-1; i <= j; i++,j--) 15 { 16 if (str[i] != str[j]) 17 { 18 flag = 0; 19 } 20 } 21 if (flag) cout << "YES" << endl; 22 else cout << "NO" << endl; 23 flag = true; 24 } 25 26 return 0; 27 }
解题思路:
LeetCode中关于回文串的题共有五道,除了这道,其他的四道为 Palindrome Number 验证回文数字, Validate Palindrome 验证回文字符串,Palindrome Partitioning 拆分回文串,Palindrome Partitioning II 拆分回文串之二,我们知道传统的验证回文串的方法就是两个两个的对称验证是否相等,那么对于找回文字串的问题,就要以每一个字符为中心,像两边扩散来寻找回文串,这个算法的时间复杂度是O(n*n),可以通过OJ,就是要注意奇偶情况,由于回文串的长度可奇可偶,比如"bob"是奇数形式的回文,"noon"就是偶数形式的回文,两种形式的回文都要搜索。这道题让我们求最长回文子串。
C++参考答案一:
1 // Time complexity O(n*n) 2 class Solution { 3 public: 4 string longestPalindrome(string s) { 5 int startIdx = 0, left = 0, right = 0, len = 0; 6 for (int i = 0; i < s.size() - 1; ++i) { 7 if (s[i] == s[i + 1]) { 8 left = i; 9 right = i + 1; 10 searchPalindrome(s, left, right, startIdx, len); 11 } 12 left = right = i; 13 searchPalindrome(s, left, right, startIdx, len); 14 } 15 if (len == 0) len = s.size(); 16 return s.substr(startIdx, len); 17 } 18 void searchPalindrome(string s, int left, int right, int &startIdx, int &len) { 19 int step = 1; 20 while ((left - step) >= 0 && (right + step) < s.size()) { 21 if (s[left - step] != s[right + step]) break; 22 ++step; 23 } 24 int wide = right - left + 2 * step - 1; 25 if (len < wide) { 26 len = wide; 27 startIdx = left - step + 1; 28 } 29 } 30 };
此题还可以用动态规划Dynamic Programming来解,与Palindrome Partitioning II 拆分回文串之二的解法很类似,我们维护一个二维数组dp,其中dp[i][j]表示字符串区间[i, j]是否为回文串,当i = j时,只有一个字符,肯定是回文串,如果i = j + 1,说明是相邻字符,此时需要判断s[i]是否等于s[j],如果i和j不相邻,即i - j >= 2时,除了判断s[i]和s[j]相等之外,dp[j + 1][i - 1]若为真,就是回文串,通过以上分析,可以写出递推式如下:
dp[i, j] = 1 if i == j
= s[i] == s[j] if j = i + 1
= s[i] == s[j] && dp[i + 1][j - 1] if j > i + 1
这里有个有趣的现象,就是如果我把下面的代码中的二维数组由int改为vector<vector<int> >后,就会超时,这说明int型的二维数组访问执行速度完爆std的vector,所以,以后尽还是可能用最原始的数据类型吧。
C++参考答案二:
1 // DP 2 class Solution { 3 public: 4 string longestPalindrome(string s) { 5 int dp[s.size()][s.size()] = {0}, left = 0, right = 0, len = 0; 6 for (int i = 0; i < s.size(); ++i) { 7 for (int j = 0; j < i; ++j) { 8 dp[j][i] = (s[i] == s[j] && (i - j < 2 || dp[j + 1][i - 1])); 9 if (dp[j][i] && len < i - j + 1) { 10 len = i - j + 1; 11 left = j; 12 right = i; 13 } 14 } 15 dp[i][i] = 1; 16 } 17 return s.substr(left, right - left + 1); 18 } 19 };
最后要提的就是大名鼎鼎的马拉车算法Manacher's Algorithm,这个算法的神奇之处在于将时间复杂度提升到了O(n)这种逆天的地步,而算法本身也设计的很巧妙,很值得我们掌握,参见博客:http://www.cnblogs.com/grandyang/p/4475985.html,代码实现如下:
C++参考答案三:
1 class Solution { 2 public: 3 string longestPalindrome(string s) { 4 string t ="$#"; 5 for (int i = 0; i < s.size(); ++i) { 6 t += s[i]; 7 t += '#'; 8 } 9 int p[t.size()] = {0}, id = 0, mx = 0, resId = 0, resMx = 0; 10 for (int i = 0; i < t.size(); ++i) { 11 p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1; 12 while (t[i + p[i]] == t[i - p[i]]) ++p[i]; 13 if (mx < i + p[i]) { 14 mx = i + p[i]; 15 id = i; 16 } 17 if (resMx < p[i]) { 18 resMx = p[i]; 19 resId = i; 20 } 21 } 22 return s.substr((resId - resMx) / 2, resMx - 1); 23 } 24 };
官方解答:
摘要:
这篇文章是为中级读者而写的。它介绍了回文,动态规划以及字符串处理。请确保你理解什么是回文。回文是一个正读和反读都相同的字符串。
解决方案:
方法一:最长公共子串
常见错误
有些人会忍不住提出一个快速的解决方案,不幸的是,这个解决方案有缺陷(但是可以很容易地纠正):
反转 S,使之变成 S'。找到 S 和 S′之间最长的公共子串,这也必然是最长的回文子串。
算法:
我们可以看到,当 SS 的其他部分中存在非回文子串的反向副本时,最长公共子串法就会失败。为了纠正这一点,每当我们找到最长的公共子串的候选项时,都需要检查子串的索引是否与反向子串的原始索引相同。如果相同,那么我们尝试更新目前为止找到的最长回文子串;如果不是,我们就跳过这个候选项并继续寻找下一个候选。
这给我们提供了一个复杂度为 O(n^2)O(n2) 动态规划解法,它将占用 O(n^2)O(n2) 的空间(可以改进为使用 O(n)O(n) 的空间)。请访问https://en.wikipedia.org/wiki/Longest_common_substring_problem阅读更多关于最长公共子串的内容。
方法二:暴力法
方法三:动态规划
方法四:中心扩展算法
Java代码:
1 public String longestPalindrome(String s) { 2 int start = 0, end = 0; 3 for (int i = 0; i < s.length(); i++) { 4 int len1 = expandAroundCenter(s, i, i); 5 int len2 = expandAroundCenter(s, i, i + 1); 6 int len = Math.max(len1, len2); 7 if (len > end - start) { 8 start = i - (len - 1) / 2; 9 end = i + len / 2; 10 } 11 } 12 return s.substring(start, end + 1); 13 } 14 15 private int expandAroundCenter(String s, int left, int right) { 16 int L = left, R = right; 17 while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) { 18 L--; 19 R++; 20 } 21 return R - L - 1; 22 }
方法五:Manacher算法
还有一个复杂度为 O(n) 的Manacher算法,你可以在该网站:https://articles.leetcode.com/longest-palindromic-substring-part-ii/找到详尽的解释。然而,这是一个非同寻常的算法,在45分钟的编码时间内提出这个算法将会是一个不折不扣的挑战。但是,请继续阅读并理解它,我保证这将是非常有趣的。