ID:5.最长回文子串

118 阅读4分钟

考点:动态规划

题目链接

题目

给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

思路

一、倒转法(TLE)

把原字符串反转以后逐位和原字符串比对,若相等则相对左上角的值加1,同时维护一个子串最大值 max 和当前最大值的横坐标 cur,这是因为反转后的字符串中找到的回文子串的末尾下标在反转后应该等于原字符串中回文子串的首位下标(这里解释得很乱,借用一下题解区的答案解法 2: 最长公共子串

var longestPalindrome = function(s) {
    /**
     *   b a b a d
     * d 0 0 0 0 1
     * a 0 1 0 1 0
     * b 1 0 2 0 0
     * a 0 2 0 3 0
     * b 1 0 3 0 0
     */
    const dp = Array.from({ length: s.length }, _ => 
        Array.from({ length: s.length }, _ => 0)
    );
    let max = 0, cur = 0;
    for(let i = 0; i < s.length; i++) {
        for(let j = 0; j < s.length; j++) {
            if(s[s.length - 1 - i] === s[j]) {
                if(i === 0 || j === 0) {
                    dp[i][j] = 1;
                } else {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
            }
            if(dp[i][j] > max) {
                if(j - dp[i][j] + 1 === s.length - 1 - i) {
                    max = dp[i][j];
                    cur = j;
                }
            }
        }
    }
    return s.slice(cur - max + 1, cur + 1);
};

但是超时了🤔,题解区用的是Java,不知道跟语言有没有关系。

二、动态规划(TLE)

根据回文特点可知,一个字符串是否是回文字符串,取决于它本身去掉头尾两点后中间的部分是不是回文字符串。同时一个字符串的长度为1时一定是回文串,长度为2时取决于两个值是否相等。

参考题解(这个解释得比官方清楚一些)

var longestPalindrome = function(s) {
    const len = s.length;
    if(len < 2) return s;
    /**
     * s.length > 2:
     *  P[i][j] = isHui(s[i + 1][j - 1]) && s[i] === s[j]
     * s.length <= 2:
     *  p[i][i] = true
     *  p[i][i+1] = s[i] === s[i + 1]
     */
    const dp = Array.from({ length: s.length }, (_, i) => 
        Array.from({ length: s.length }, (_, j) => i === j)
    );
    // 注意这里的i、j顺序,为什么这么奇怪呢?
    // 根据题解作者的说法,我们用不到对角线以下的数据
    // 因为对角线以下的数据的i都比j大,这不符合字符串要求
    // 所以我们要遍历右上角的数据
    // 由于某位置的数据与其左下角的数据有关
    // 因此要从左到右地遍历(上下顺序无所谓)
    let max = 1, cur = 0;
    for(let j = 1; j < len; j++) {
        for(i = 0; i < j; i++) {
            // 如果边缘两端的值相等
            if(s[i] === s[j]) {
                // 如果去掉边缘两点后中间剩余的字符串长度小于 2
                // 即原始字符串的长度为 2 或 3
                // 那么直接判断为回文串
                if(j - i < 3) {
                    dp[i][j] = true;
                } else {
                    dp[i][j] = dp[i + 1][j - 1];
                }
            }
            // 只要dp[i][j] == true就表示s[i..j]是回文子串,
            // 此时记录回文长度和位置
            if(dp[i][j] && j - i + 1 > max) {
                max = j - i + 1;
                cur = i;
            }
        }
    }
    return s.slice(cur, cur + max);
};

然后又超时了😓

三、中心扩散法

核心思想:以每个字符或字符间隙为中心,分别向两端扩散

参考官方题解

var longestPalindrome = function(s) {
    const len = s.length; // 字符串长度
    if(len < 2) return s; // 长度为1一定回文
    // 中心扩散函数
    const centerSpread = (left, right) => {
        // 首先判断左右值是否在范围内:left >= 0 && right < len
        // 其次判断左右两端点是否相等,相等的话则向两边扩散<- ->
        while(left >= 0 && right < len && s[left] === s[right]) {
            left--; right++;
        }
        // 判断结束后,注意此时符合条件的字符串应该是s.slice(left + 1, right)
        // 这里只返回长度,所以直接 right - (left + 1)
        return right - left - 1;
    }
    let start = 0, end = 0; // 记录左右端点
    // 遍历s的每一个字符,调用中心扩散函数
    // 注意最后一个字符没有扩散价值,因为它的下标加1就越界了
    for(let i = 0; i < s.length - 1; i++) {
        // 子串长度为奇数,扩散中心为具体值
        // 子串长度为偶数,扩散中心为两个值之间的空隙
        let odd = centerSpread(i, i);
        let even = centerSpread(i, i + 1);
        // 取两者较大值
        let max = Math.max(odd, even);
        // 如果max比当前子串长度还大
        if(max > end - start) {
            // 更新最长子串的左边界和右边界
            // 此时 i 相当于子串中心,求左右边界只需分别加/减子串长度的一半即可
            start = i - Math.floor((max - 1) / 2);
            end = i + Math.floor(max / 2);
        }
    }
    return s.slice(start, end + 1);
};

四、Manacher算法

太难了先不看