【LeetCode】5. 最长回文子串
给你一个字符串 s
,找到 s
中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
提示:
1 <= s.length <= 1000
s
仅由数字和英文字母组成
动态规划
- 确定dp含义:
dp[i] [j]表示:字符串s,以i下标开始,j下标结束,是不是回文
- 确定dp递推公式
因为以j下标结束,则j一定要大于等于i
如果i下标和j下标相邻,即j - i == 1
,当对应下标字符相等时为回文,即dp[i] [j] = (arr[i] == arr[j])
如果i下标和j下标不相邻,则当两下标之间的最大子串是回文且下标i和下标j对应字符相等时为回文,即dp[i] [j] = dp[i+1] [j-1] && (arr[i] == arr[j])
- dp初始化
单个字符的都是回文
for(int i = 0; i < n; i++) {
dp[i][i] = true;
}
-
确定遍历顺序
从递归公式中
dp[i] [j]
依赖于dp[i+1] [j-1]
,所以i正序遍历,j逆序遍历。 -
推导dp数组
j小于i的位置无效,以"cbbd"
举例:
实现代码:
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if(n < 2) {
return s;
}
char[] arr = s.toCharArray();
boolean[][] dp = new boolean[n][n];
for(int i = 0; i < n; i++) {
dp[i][i] = true;
}
int start = 0;
int end = 0;
for(int i = n - 2; i >= 0; i--) {
for(int j = i + 1; j < n; j++) {
if(j - i == 1) {
dp[i][j] = (arr[i] == arr[j]);
}else {
dp[i][j] = (dp[i + 1][j - 1] && (arr[i] == arr[j]));
}
if(dp[i][j] && j - i > end - start) {
start = i;
end = j;
}
}
}
return s.substring(start, end + 1);
}
}
中心扩展算法
遍历每一个回文中心,扩展计算每个回文中心的最大回文长度,取最大值,回文中心有两种情况,以一个字符为中心,以两个相同字符为中心。
- 如果i下标和j下标相邻,即
j - i == 1
,当对应下标字符相等时为回文 - 如果i下标和j下标不相邻,则当两下标之间的最大子串是回文且下标i和下标j对应字符相等时为回文
class Solution {
public String longestPalindrome(String s) {
int n = s.length();
if(n < 2) {
return s;
}
char[] arr = s.toCharArray();
int start = 0;
int end = 0;
for(int i = 0; i < n; i++) {
int len1 = expand(arr, i, i);
int len2 = expand(arr, i, i + 1);
int len = Math.max(len1, len2);
if(end - start + 1 < len) {
start = i - ((len - 1) / 2);
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
public int expand(char[] arr, int left, int right) {
while(left >= 0 && right < arr.length && arr[left] == arr[right]) {
left--;
right++;
}
return right - left - 1;
}
}
对start、end的解释:
以bcb和bccb举例
带入式子,无论长度是奇数还是偶数:
start = i - ((len - 1) / 2);
end = i + len / 2;
都能成立
right - left - 1的解释:
当跳出循环时,left、right所指向的已经是不相等的两个字符,right - left已经减掉了一侧的一个不相等的字符,则还需要再减去另一侧那个不相等的字符。
Manacher (马拉车)算法
Manacher算法帮助我们在给定的字符串中找到最长的回文子串。它的本质就是**对暴力算法的优化。**我们先只关注奇数长度回文的处理,至于偶数的回文后面会有方法将其转化成奇数长度的回文。
约定
设 c 为已经找到的右边界最大的回文字符串的中心,并设 l 和 r 是这个回文的左右边界,即分别为最左边的字符索引和最右边的字符索引。现在,让我们举个例子来理解 c、l 和 r。
例如:“abacabacabb”
当从左到右一个字符一个字符遍历时,i表示正在处理的字符的索引,当 i = 1时,最长的回文子串是“aba",长度为3。
当 i = 5时,最长的回文子串是"bacabacab",长度为9:
现在我们知道了c、l和r表示什么,为了下面算法的讲解更加自然,我们需要了解一个概念:镜像索引。
即将中心作为对称轴的对称点,对于任何以中心 c 为中心的回文,索引 j 的镜像是 j’ 有:
c - j = j' - c
#此时,j的镜像j':
j' = (2 * c) - j
则镜像索引的计算公式为:j’ = (2 * c) - j
对于回文“abacaba”,j 的镜像是 j’,j’ 的镜像是 j:
当我们将 i 从左向右移动时,我们试图在每个 i 处扩展回文。这意味着我将检查是否存在以 i 为中心的回文,如果存在,我会将回文长度的一半,把它称为回文半径或者扩展的长度存储在一个名为P的新数组中。
后面都以回文半径来称呼。
如果 i 索引为中心的最大回文扩展超出当前右边界 r,则最大右边界改变,则 c 被更新为 i 并且新的 l, r 被找到并更新。
让我们以前面讨论的以 i = 3 为中心的回文“abacaba”为例。
因此,P[] 数组存储了以每个索引为中心的回文的回文半径。但是我们不需要每次都手动去每个索引展开去检查回文半径。c 表示当前最长奇数回文的中心。而 l, r 表示回文的左右边界。
当 i 等于3时,通过向两侧延伸,不难计算出以 i 为中心的最长回文字符串是"abacaba",回文半径为3,此时,我们令P[i]=3。
马拉车算法的核心作用就是:借助c、r、l提供的信息,在求P[i]的值时,不用傻傻的暴力向两侧延伸计算。
情况①:
以字符串 "abacaba"的P[]数组为例:
当 i = 4时,可以看出 i < r。此时,我们不用天真地在i处向两侧扩展。我们可以先计算出确定的以 i 为中心的回文字符串最短的回文半径,这样我们就可以在这个基础上通过继续向两侧扩展来计算P[i],而不是从头开始做。
只要索引 i’ 为中心的回文半径不超出当前最长回文的左边界 l,我们可以说索引 i 为中心的回文的最小回文半径为 P[i’]。即我们检查镜像索引i’。只要值i’ - P[i’]没有小于l,我们就可以确定在索引i为中的最短回文字符串的长度是2P[i’] + 1,也就是说,至少可以从i向左右扩展P[i’]步,最小回文半径就是P[i’]。
注意,我们只是在谈论最小的回文半径,实际的回文半径可能会更大,仍需继续向外扩获取最大值。
在上图中,P[4]=P[2]=0。我们尝试借助已有的P数组,但就这个例子而言,很不幸,P[4]仍然是0,这里有些特殊的情况我们还需要进一步考虑。
如果索引 i’ 为中心的的回文半径超出当前最长回文数的左边界l,我们可以说索引 i 为中心的回文的最小回文半径为 r - i。
情况②:
为什么索引i为中心的最小回文半径不能大于r - i ?
例子: “acacacb”
若最小回文半径大于r - i,即下标为6的元素变为a时,那么右边界最大的就不是 "cacac "而是"acacaca "了,和前提条件不符!
上述两种情况,用数学公式进行总结:
if(i < r){
P[i] = Math.min(r - i, P[mirror]);
}
接下来,就只需继续向外扩展,开始从下标 i - P[I’] - 1 和 i + P[I’] + 1继续检查字符,扩展过程不断更新P[i],也不断更新最大回文半径和最大长度回文的回文中心。
如果此回文扩展长度超出 r,则将 c 更新为 i,将 r 更新为 (i + P[i]), l 更新为 (i - P[i])。
为了使这个算法成功,需要将偶数回文串转换为奇数回文串从而统一操作。只需在字符之间添加特殊字符隔开即可。
例子1: aba -> #a#b#a#。
例子2:Abba-> #a#b#b#a#。
代码实现:
class Solution {
public String longestPalindrome(String s) {
StringBuffer t = new StringBuffer("#");
for (int i = 0; i < s.length(); ++i) {
t.append(s.charAt(i));
t.append('#');
}
int n = t.length();
int[] P = new int[n];
//记录最大回文半径
int maxLen = 0;
//记录最大长度回文的中心下标
int index = 0;
int c = 0;
int r = 0;
for(int i = 0; i < n; i++) {
int mirror = (2 * c - i);
if(i < r) {
P[i] = Math.min(r - i, P[mirror]);
}
int tmpL = i - P[i] - 1;
int tmpR = i + P[i] + 1;
while (tmpL >= 0 && tmpR < n && t.charAt(tmpL) == t.charAt(tmpR)) {
P[i]++;
tmpL--;
tmpR++;
}
if(i + P[i] > r) {
c = i;
r = i + P[i];
if(P[i] > maxLen) {
maxLen = P[i];
index = i;
}
}
}
return s.substring((index - P[index]) / 2, (index + P[index]) / 2);
}
}