1

题目描述

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。

示例 1:
输入: "babad"
输出: "bab"
注意: "aba"也是一个有效答案。

示例 2:
输入: "cbbd"
输出: "bb"

解决方法

首先,我们通过在字母之间插入特殊字符'#'来将输入字符串S转换为另一个字符串T,这么做的原因很快就会很快清楚。
例如:S =“abaaba”,T =“#a#b#a#a#b#a#”。
为了找到最长的回文子串,我们需要在每个Ti周围扩展,使得Ti-d ... Ti + d形成回文。你应该马上看到d是以Ti为中心的回文的长度。
我们将中间结果存储在数组P中,其中P[i]等于在Ti处的回文中心的长度。最长的回文子串将成为P中的最大元素。
使用上面的例子,我们填充P如下(从左到右):

序号 0 1 2 3 4 5 6 7 8 9 10 11 12
T # a # b # a # a # b # a #
P 0 1 0 3 0 1 6 1 0 3 0 1 0

看着P,我们立即看到最长的回文是“abaaba”,如P6 = 6所示。
您是否注意到在字母之间插入特殊字符(#),是否优雅地处理了奇数和偶数长度的回文? (请注意:这是为了更容易地演示这个想法,并不一定需要对算法进行编码。)
现在,想象你在回文中心“abaaba”绘制一条想象的垂直线。你有没有注意到P中的数字是围绕这个中心对称的?不仅如此,请尝试使用另一个回文“aba”,这些数字也反映了类似的对称性。这是巧合吗?答案是肯定的,不是。这仅仅是一个条件,但无论如何,我们有很大的进步,因为我们可以消除P[i]的重新计算部分。
让我们继续讨论一个稍微复杂的例子,其中有更多的重叠回文,其中S =“babcbabcbaccba”。

上图显示T由S =“babcbabcbaccba”转化而来。假设你达到了表P部分完成的状态。垂直的实线表示回文“abcbabcba”的中心(C)。两条虚线垂直线分别表示其左(L)和右(R)边缘。您处于索引i处,其围绕C的镜像索引是i'。你如何有效地计算P[i]?
假设我们已经到达指数i = 13,并且我们需要计算P[13](由问号?表示)。我们首先看一下它在回文中心C周围的镜像索引i',索引i'= 9。

上面的两条绿色实线表示以i和i'为中心的两个回文序列覆盖的区域。我们看一下C周围的镜像索引i'。P[i'] = P[9] = 1.由于回文在其中心附近具有对称性,因此P[i]必须也是1。
正如你在上面看到的那样,P[i] = P[i'] = 1是非常明显的,由于回文中心周围的对称性,这一定是正确的。事实上,C之后的所有三个元素都遵循对称性(即P[12] = P[10] = 0,P[13] = P[9] = 1,P[14] = P[8] = 0)。

现在我们处于索引i = 15,其C上的镜像索引是i'= 7。P[15] = P[7] = 7?
现在我们处于索引i = 15处。P[i]的值是什么?如果我们遵循对称性,P[i]的值应该与P[i'] = 7相同,但这是错误的。如果我们在T15围绕中心展开,它形成回文“A·B·C·B·一”,它实际上比它的镜像索引i'更短。 为什么?

在索引i和i'处围绕中心重叠彩色线。 由于C周围的对称属性,绿色实线显示两侧必须匹配的区域。红色实线表示两侧可能不匹配的区域。 虚线绿线表示穿过中心的区域。
很显然,由两条实线表示的区域中的两个子串必须完全匹配。中心区域(用绿色虚线表示)也必须是对称的。注意P[i']是7,并且它一直扩展到回文的左边缘(L)(用红色实线表示),它不再落在回文的对称属性之下。我们所知道的是P[i] ≥ 5,并且为了找到P[i]的实际值,我们必须通过扩展右边缘(R)来进行字符匹配。在这种情况下,由于P[21]≠P[1],我们得出结论P[i] = 5。
我们总结一下这个算法的关键部分如下:

如果 P[i'] ≤ R - i,
则 P[i] = P[i']
否则 P[i] ≥ P[i']。那么我们必须扩展到右边缘(R)以找到P[i]。

看看它有多优雅?如果你能够充分掌握上述总结,那么你已经获得了这个算法的本质,这也是最难的部分。
最后一部分是确定我们应该在何时将C的位置与R一起移动到右侧,这很容易:

如果以i为中心的回文确实扩展到R,我们将C更新为i(这个新回文的中心),并将R延伸到新回文的右边。

在每一步中,都有两种可能性。如果P[i]≤R-i,我们将P[i]设置为P[i'],这只需要一步。否则,我们试图通过从右边缘R开始将回文中心改为i。扩展R(内部while循环)总共最多需要N个步骤,并且定位和测试每个中心总共需要N步。因此,该算法确保在至多2 * N步完成,给出线性时间解决方案,时间复杂度:O(N)
Manacher(马拉车)算法解释原文

实现:真的添加#字符

    public String longestPalindrome(String s) {
        // 改造字符串,每个字符间添加#。添加头^尾$两个不同的字符用于消除边界判断
        StringBuilder sb = new StringBuilder("^");
        for (int i = 0, len = s.length(); i < len; i++)
            sb.append("#").append(s.charAt(i));
        sb.append("#$");

        int c = 0, r = 0, len = sb.length(), centerIndex = 0, maxLen = 0;
        int[] p = new int[len];

        for (int i = 1; i < len - 1; i++) {
            int iMirror = 2 * c - i; // 相当于 c - (i - c)

            p[i] = r > i ? Math.min(r - i, p[iMirror]) : 0;

            // 基于当前点为中心扩展寻找回文
            while (sb.charAt(i - 1 - p[i]) == sb.charAt(i + 1 + p[i]))
                p[i]++;

            // 如果扩展后的右边界值大于当前右边界值则更新
            if (i + p[i] > r) {
                c = i;
                r = i + p[i];
            }

            // 寻找最大值与中心点
            if (p[i] > maxLen) {
                maxLen = p[i];
                centerIndex = i;
            }
        }

        int beginIndex = (centerIndex - 1 - maxLen) / 2;

        return s.substring(beginIndex, beginIndex + maxLen);
    }

实现:不真正添加#字符

要想不添加#字符,那么只需要模拟添加#字符后的操作
例如:
原始字符:abcba

a b c b a
0 1 2 3 4

添加#字符后

# a # b # c # b # a #
0 1 2 3 4 5 6 7 8 9 10

观察添加#字符后的字符串可知,#左边的字符在原始字符串的索引为#的索引/2,#右边的字符在原始字符串的索引为(#的索引-1)/2
例如:
索引为4的#,它的左边字符是b,那么b在原始字符串的索引为4-1/2=1,它的右边字符是c,那么c在原始字符串的索引为4/2=2

假设当前点为'c'则原始索引i=2,添加#字符后索引为i'=5,要对左右两边进行回文搜索。
那么添加#字符后进行回文搜索的过程如下

左边索引 左边索引对应的字符 右边索引对应的字符 右边索引
4 # # 6
3 b b 7
2 # # 8
1 a a 9
0 # # 10

通过将其还原为对应的字符可实现不真正添加#字符也能运用上述算法

左边索引 左边索引对应的字符 右边索引对应的字符 右边索引
4/2=2 c c (6-1)/2=2
3/2=1 b b (7-1)/2=3
2/2=1 b b (8-1)/2=3
1/2=0 a a (9-1)/2=4
0/2=0 a a (10-1)/2=4
    public String longestPalindrome2(String s) {
        if (s == null || s.length() < 2)
            return s;

        // 添加头^尾$两个不同的字符用于消除边界判断
        String temp = "^" + s + "$";

        int c = 0, r = 0, len = s.length() * 2 + 1 + 2, centerIndex = 0, maxLen = 0;
        int[] p = new int[len];

        for (int i = 1; i < len - 1; i++) {
            int iMirror = 2 * c - i;

            p[i] = r > i ? Math.min(r - i, p[iMirror]) : 0;

            // 基于当前点为中心扩展寻找回文
            int left = i - 1 - p[i];
            int right = i + 1 + p[i];
            while (temp.charAt(left / 2) == temp.charAt((right - 1) / 2)) {
                p[i]++;
                left--;
                right++;
            }

            // 如果扩展后的右边界值大于当前右边界值则更新
            if (i + p[i] > r) {
                c = i;
                r = i + p[i];
            }

            // 寻找最大值与中心点
            if (p[i] > maxLen) {
                maxLen = p[i];
                centerIndex = i;
            }
        }

        int beginIndex = (centerIndex - 1 - maxLen) / 2;

        return s.substring(beginIndex, beginIndex + maxLen);
    }

原文链接:https://lierabbit.cn/2018/05/...


LieRabbit
920 声望2.6k 粉丝

有些梦虽然遥不可及,但并不是不可能实现,只要我足够的强