https://blog.csdn.net/Bob__yuan/article/details/81431484
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
Example 1: Example 2:
Input: "babad" Input: "cbbd"
Output: "bab" Output: "bb"
Note: "aba" is also a valid answer.
方法一:中心扩展 (beats 84.72%)
这道题如果暴力求解的话太麻烦了,这种回文问题,用从中心逐渐向左右两边扩展的方式进行计算会快很多,值得注意的是,回文问题中,“aba”和“abba”不是一种类型,前一种是以一个字符为中心的对称,后一种是以两个相同字符为中心的对称,两种中心方式都要考虑。我的代码如下:
string longestPalindrome(string s)
{
int len = s.length();
if (len == 1 || len == 0)
return s;
int max_len = 1, start = -1; // 最长回文string的起始位置,max_len是最长回文长度
for (int i = 0; i < len; i++)
{
int l = i - 1, r = i + 1; // left, right
// 把一个字符作为中心
while (l >= 0 && r <= len - 1 && s[l] == s[r]){ --l; ++r; }
if (r - l - 1 > max_len)
{
max_len = r - l - 1;
start = l + 1;
}
if (i != len - 1 && s[i] == s[i + 1])
{
l = i - 1, r = i + 2;
// 两个相同字符作为中心
while (l >= 0 && r <= len - 1 && s[l] == s[r]){ --l; ++r; }
if (r - l - 1 > max_len)
{
max_len = r - l - 1;
start = l + 1;
}
}
}
if(start == -1) // 当没有回文子串的时候,随便返回一个就行,为保证不会越界,返回第一个就行
return string() + s[0];
return s.substr(start, max_len);
}
就是从左往右,以所有位置为中心进行扩展尝试,如果有两个连着一样的,以这两个并列为中心,向左右进行扩展尝试,找出最长的长度,以及最长回文子串的开头,这样利用substr就可以得到最长回文子串了(如果有两个以上连着相同的情况并不用考虑,因为三个可以看成1个扩展了一层,4个可以看成2个扩展了一层,and so on)。
方法二:DP (beats 44.34%)
一个简单的改进暴力算法的想法就是,比如“cabac”这种情况下,我们已经知道“aba”是回文的了,只需要判断左右两边是不是,就可以知道整个string是不是了(和中心扩展思想一样,但是需要O(N ^ 2)的空间)。也就是说比如我们设 dp[i][j] 为 true 表示 s 从 i 到 j 是回文的,那么要求就是 dp[i + 1][j - 1] 为 true,且 s[i] == s[j]。
最开始我们知道 dp[i][i] 对所有 i 来说都成立,然后如果 s[i] == s[i + 1],那么 dp[i][i + 1] 就是true。
比如 s 长度为 len,基于这两点就可以创建一个 len * len 大小的矩阵,并且对矩阵进行初始化,然后一步一步计算矩阵每个位置上的值,最后找出 j - i 最大的位置。直接照着网上的solution写的:
string longestPalindrome(string s)
{
int n = s.length();
int start = 0, max_len = 1;
bool table[1000][1000] = {false}; // 这样初始化其实不行(只能给table[0][0]赋值),不过默认是false
for (int i = 0; i < n; i++)
table[i][i] = true;
for (int i = 0; i < n - 1; i++)
{
if (s[i] == s[i+1])
{
table[i][i+1] = true;
start = i;
max_len = 2;
}
}
for (int len = 3; len <= n; len++)
{
for (int i = 0; i < n - len + 1; i++)
{
int j = i + len - 1;
if (s[i] == s[j] && table[i + 1][j - 1]) // 谁先谁后感觉区别不大
{
table[i][j] = true;
start = i;
max_len = len;
}
}
}
return s.substr(start, max_len);
}
方法三:Manacher's Algorithm (beats 99.23%)
做题的时候感觉特别像字符串匹配的KMP算法,感觉这道题肯定有更加高效的算法,只不过是我不知道而已= =。
果然,LeetCode这道题给出的Solution里提到了只需要O(N)时间(Linear Time)即可完成的算法,叫作“Manacher's Algorithm”。
这个算法和我最开始的想法一样,就是在每个字符中插入一个特定字符,我想的是“-”,比如“aba”编程 “a-b-a”,这里就是用的“#”,没区别,但是跟我想的不一样的是,字符串的左右两端也扩充了,而且在两个又各加了一个控制符,为了控制便捷,比如“aba”,就变成了“^$#a#b#a#$”。这样做的好处就是,不需要再考虑是单独一个字符为中心,还是两个相同字符为中心,而是在插入“#”后,只需要考虑以单个字符为中心的情况。比如说第一方法中,如果“bb”是中心,那么在这个算法中是,“b#b” 中间的 “#” 为中心,如果是“b”为中心,那这个算法里也就是“b”为中心。
这个不是这个算法的核心,这个算法的核心思想就是要跳过不必要的重复判断。
算法的思想是,先对字符串进行上述处理,然后创建一个和新的字符串同样大小的数组,比如说叫P,P[i] 表示以第i位为中心的最长回文串的一半的长度,也可以理解为以这个圆为圆心的半径。直接用Solution里的原图看看数组的值:
比如说,“habcbap”,转换之后是“^$#h#a#b#c#b#a#p#$”
算法的核心思想就是,可以看到中间“abacaba”是最长回文,当我们已经算到了 “c” 的位置,知道p[9] = 5(如果我没算错的话- -),那当我们算 “p” 这个位置的时候,发现“p”位置的下标减去 “c” 的下标,大于“c”位置(center)的值,也就是在以“c”为中心的最长回文串的外边,那么我们肯定是不知道什么对“p”位置的值有贡献的东西,只能用第一种方法,中心扩充,来计算这个位置的值。
但是!如果我们要算 “c” 后边的 “b” 的值的时候,我们知道了这个 “b” 是处在 “c” 的回文子串之中的,所以如果它的下标是 i,那么它对应过去的下标 mirror_i 位置上一定也是b,这时候需要判断对应位置上的回文值,因为对应位置一定已经计算过了,所以通过镜像过去位置上的回文值计算当前位置的回文值,能够避免掉很多运算!
如果对应位置的回文长度不如 “c” 的回文长度长,那么当前位置的一定也就是那么长,因为是镜像的!下图就是实例:
但是如果镜像位置上的回文长度超出了 “c” 辐射的范围,那么当前位置上的就是至少能够达到碰到 “c” 辐射范围的边缘。如下图所示。
总结来说,就是在从左往右遍历整个字符串的过程中,通过已有信息,跳过不必要的判断,共有三种情况:
1、要计算的位置在已知最大的回文串覆盖范围外 —— 只能通过中心扩展一点一点算,没有跳过计算;
2、要计算的位置在已知最大回文串覆盖范围内,且镜像位置上的回文串被最大回文串包含 —— 不需要计算,直接 p[i] = p[mirror_i] 即可,跳过这个位置所有计算;
3、要计算的位置在已知最大回文串覆盖范围内,但镜像位置上的回文串没有完全被最大回文串包含 —— 可以从最大回文串的边缘开始中心扩充,能跳过一部分计算。
看懂了算法思想,代码就比较简单了:
// Transform S into T.
// For example, S = "abba", T = "^#a#b#b#a#$".
// ^ and $ signs are sentinels appended to each end to avoid bounds checking
string preProcess(string s)
{
int n = s.length();
if (n == 0) return "^$";
string ret = "^";
for (int i = 0; i < n; i++)
ret += "#" + s.substr(i, 1);
ret += "#$";
return ret;
}
string longestPalindrome(string s)
{
string T = preProcess(s);
int n = T.length();
int *P = new int[n];
int C = 0, R = 0; // center, right
for (int i = 1; i < n-1; i++)
{
int i_mirror = 2 * C - i; // equals to i' = C - (i-C)
P[i] = (R > i) ? min(R-i, P[i_mirror]) : 0;
// Attempt to expand palindrome centered at i
while (T[i + 1 + P[i]] == T[i - 1 - P[i]])
P[i]++;
// If palindrome centered at i expand past R, adjust center based on expanded palindrome.
if (i + P[i] > R)
{
C = i;
R = i + P[i];
}
}
// Find the maximum element in P.
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n-1; i++)
{
if (P[i] > maxLen)
{
maxLen = P[i];
centerIndex = i;
}
}
delete[] P;
return s.substr((centerIndex - 1 - maxLen) / 2, maxLen);
}
由于这个算法其实最关心的就是 right 边界,每次就是移动 R, 算法可以保证 finish in 2 * n steps(具体为什么是2*n还没有很明白),所以是 O(N) 的时间富足度。
方法四:后缀树(Suffix Trees)
并没有看