LeetCode 第五题:最长回文子串

17 阅读3分钟

题目描述

给你一个字符串 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#

i01234567891011121314
str#A#B#B#A#C#D#C#
sNew[i]121252121214121

从表中不难得到,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;
    }
}