题目描述
给你一个字符串 s
,找到 s
中最长的 回文子串。
示例 1:
输入: s = "babad"
输出: "bab"
解释: "aba" 同样是符合题意的答案。
示例 2:
输入: s = "cbbd"
输出: "bb"
提示:
1 <= s.length <= 1000
s
仅由数字和英文字母组成
解题思路
回文串: 正读和反读都一样的字符串
可以将字符串的每一个字符都作为回文子串的中心对称点,
[A B C D C B A]
以 D 为中心,半径为 r 构成了一个回文子串。
Manacher 算法首先都会进行一个预处理,将字符串中字符都插入一个同样的符号,这个符号不可以在原字符串中出现过。
如: ABCDCBA 变成了 #A#B#C#D#C#B#A#
这样的做法为了将偶回文或者奇回文都转串成奇回文,解决长度奇偶性的问题。
ABBACDC 分别可以转换为
#A#B # B#A#
#C# D #C#
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
str | # | A | # | B | # | B | # | A | # | C | # | D | # | C | # |
sNew[i] | 1 | 2 | 1 | 2 | 5 | 2 | 1 | 2 | 1 | 2 | 1 | 4 | 1 | 2 | 1 |
从表中不难得到,sNew[i] − 1 的最大值即为原字符串的最长回文子串长度。
转换后,所有回文子串的长度为奇数,故以中心位置下标为 i 的最长回文串长度为 2 × len[i] − 1。( #A#B # B#A# -> 2*5-1 )
在回文串中,特殊字符数为 len[i],而除去特殊字符数剩下的就为原字符数,即 (2 × len[i] − 1) − len[i] = len[i] -1。( #A#B # B#A# -> (2*5-1)-5=5-1 )
目标就是为了求 len[i] 中的所有数。
已知 P 的最长回文子串长度 len[p],则回文串左边界为 p - len[p],右边界为 p + len[p]。
假设在已知中心 p 的左边有一点 j,其对称点为 i。
-
若 i > len[p] + p,暴力比较,通常出现在求取最开始时。
-
若 i < len[p] + p,且 len[j] < len[p] + p - i (右边界到 i 的距离),则他被完全包裹入以 p 为中心的子串中,必有 len[i] = len[j]。
-
若 i = len[p] + p,且 len[j] >= len[p] + p - i, len[i] = len[j],此时可能存在超出原有 p 的回文区域,仍需从边界 i + 1 + len[i] 出发一一比较。
做完中心 i 的长度求取之后,判断是否 i 的回文区域右边界大于原有回文右边界值,若大于,更新中心点为 i ,右边界为 i 的回文右边界。
/**
* @ClassName LongestPalindrome
* @Description 最长回文子串
* @Version 1.0.0
* @Date 2024/6/5 0:32
* @Author By Dwl
*/
public class LongestPalindrome {
public static void main(String[] args) {
String s = longestPalindrome("ABBACDC");
System.out.println(s);
}
public static String longestPalindrome(String s) {
String str = "#" + s.replaceAll(".(?!$)", "$0#") + "#";
List<Character> sNew = str.chars().mapToObj(o -> (char) o).toList();
List<Integer> len = new ArrayList<>();
// 最长回文子串
String sub = "";
// 表示在 i 之前所得到的 len 数组中的最大值所在位置
int subMid = 0;
// 表示以 subMid 为中心的最长回文子串的最右端在 sNew 中的位置
int subSide = 0;
len.add(1);
for (int i = 1; i < sNew.size(); i++) {
// i < subSide 时,在 len[j] 和 subSide - i 中取最小值,省去了 j 的判断
if (i < subSide) {
int j = 2 * subMid - i;
if (j >= 2 * subMid - subSide && len.get(j) <= subSide - i) {
len.add(len.get(j));
} else {
len.add(subSide - i + 1);
}
}
// i >= subSide 时,从头开始匹配
else {
len.add(1);
}
while ((i - len.get(i) >= 0 && i + len.get(i) < sNew.size()) && (sNew.get(i - len.get(i)).equals(sNew.get(i + len.get(i))))) {
// sNew[i] 两端开始扩展匹配,直到匹配失败时停止
len.set(i, len.get(i) + 1);
}
// 匹配的新回文子串长度大于原有的长度
if (len.get(i) >= len.get(subMid)) {
subSide = len.get(i) + i - 1;
subMid = i;
}
}
// 在 s 中找到最长回文子串的位置
sub = s.substring((2 * subMid - subSide) / 2, subSide / 2);
return sub;
}
}