题目:
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"
某a公司OA题库之一,要求一个string里最长的回文序列。
LC的solutions里给出了几种解法,第一种是先把它倒过来,称作s‘,然后采用longest common substring的dp解法判断s和s‘之间的lcs,但是要注意check这个substring的起始位置是否相同,因为可能会出现倒过来和正过来一样但并不是回文序列的情况。
感觉这道题的主流解法有以下两种:
1. 动态规划
记P(i, j)为以i开头以j结尾的substring是否为回文序列,那么我们可以得出:P(i, i)=true,P(i, i+1)=(S[i] == s[i + 1])。对于任意P(i, j)来说,P(i, j) = P(i + 1, j - 1) && S[i] == P[j]。在这里需要注意的是,由于我们的i <= j,因此dp的二维矩阵只需要考虑对角线的右上部分,并且由于base case已经考虑了(i, i)和(i, i+1)的情况,因此矩阵的最后一行和倒数第二行也不需要进行判断,从倒数第三行开始就可以了,遍历的顺序由于P(i, j)是基于P(i + 1, j - 1)的,所以i从倒数第三行开始往前遍历,j正着从i+2开始遍历。写代码的时候还有一个小坑,就是初始化数组的时候虽然写了={false},但是并不会直接写成全是false的二维数组,所以还得手动再写一次,不然后面遍历的时候可能会出现数组元素未被初始化的错误。代码的时间复杂度和空间复杂度都是O(n^2),运行时间184ms,24.14%,空间10M,48.96%:
class Solution {
public:
string longestPalindrome(string s) {
int size = s.size();
if (size == 0) {
return "";
}
bool dp[size][size] = {false};
for (int i = 0; i < size; i++) {
for (int j = i; j < size; j++) {
dp[i][j] = false;
}
dp[i][i] = true;
if (i + 1 < size && s[i] == s[i + 1]) {
dp[i][i + 1] = true;
}
}
for (int i = size - 3; i >= 0; i--) {
for (int j = i + 2; j < size; j++) {
if ((s[i] == s[j]) && dp[i + 1][j - 1]) {
dp[i][j] = true;
}
else {
dp[i][j] = false;
}
}
}
int max_index = 0;
int max_length = 0;
for (int i = 0; i < size; i++) {
for (int j = i; j < size; j++) {
if (dp[i][j]) {
if (j - i + 1 > max_length) {
max_length = j - i + 1;
max_index = i;
}
}
}
}
return s.substr(max_index, max_length);
}
};
2. 从中心到两边的扩展方法:
由于回文序列相当于从中间往两边走是一样的,因此我们可以对字符串进行遍历,看以某一个字符为中心能够扩展出多长的回文序列。我们可以写一个helper function用来check从两个中心开始分别往两边找,能找出多长的回文序列(要从两个中心开始的原因是,回文序列可能是以一个元素为中心,此时两个中心重合,也可能是以两个元素为中心)。在遍历字符串的时候,对每个字符都执行一次以它自己为中心、一次以它和它后面的元素为中心的两次查找,最后取所有里面的最大值即可。需要注意的是,由于我们传入的是两个中心的index,所以最后返回来时计算回文序列的index需要仔细留意一下。运行时间32ms,62.38%,空间103.9M,17.24,这个空间使用率真是不堪入目。
class Solution {
public:
string longestPalindrome(string s) {
if (s.size() == 0) {
return "";
}
int max_len = 0;
int max_index = 0;
for (int i = 0; i < s.size(); i++) {
int len1 = palindromeLen(s, i, i);
int len2 = palindromeLen(s, i, i + 1);
int len = max(len1, len2);
if (len > max_len) {
max_len = len;
max_index = i - (len - 1) / 2;
}
}
return s.substr(max_index, max_len);
}
int palindromeLen(string s, int l, int r) {
while (l >= 0 && r < s.size() && s[l] == s[r]) {
l--;
r++;
}
return r - l - 1;
}
};
在expand(从中心向外延伸,也就是我的计算len的函数之前),如果可以把重复的字符跳过,将会大大提升运行效率,像这个:https://leetcode.com/problems/longest-palindromic-substring/discuss/2929/Accepted-4ms-c%2B%2B-solution.
还有一些其他的做法,暂时先不看了,贴在这里:https://leetcode.com/problems/longest-palindromic-substring/discuss/3060/(AC)-relatively-short-and-very-clear-Java-solution
2020.10.10 Java
前两天面试遇到这道题,还好前阵子复习了一下这道题的解法,就直接上了。刚把面试时写的代码放lc上运行了下,除了一些小细节问题以外居然直接就pass了,菜鸡震惊脸.jpg 时间复杂度O(n^2)。直接把代码丢上来:
Runtime: 29 ms, faster than 56.67% of Java online submissions for Longest Palindromic Substring.
Memory Usage: 38.9 MB, less than 5.02% of Java online submissions for Longest Palindromic Substring.
class Solution {
public String longestPalindrome(String s) {
int longestLength = 0;
String longestString = "";
for (int i = 0; i < s.length(); i++) {
String curr1 = longestPalindrome(s, i, i);
String curr2 = longestPalindrome(s, i, i + 1);
if (curr1.length() > longestLength) {
longestLength = curr1.length();
longestString = curr1;
}
if (curr2.length() > longestLength) {
longestLength = curr2.length();
longestString = curr2;
}
}
return longestString;
}
private String longestPalindrome(String s, int index1, int index2) {
int left = index1;
int right = index2;
while (left >= 0 && right < s.length()) {
if (s.charAt(left) != s.charAt(right)) {
break;
}
left--;
right++;
}
return s.substring(left + 1, right);
}
}