最长回文子串——马拉车算法(Manacher‘s Algorithm)总结

本文详细介绍马拉车算法(Manacher's Algorithm),这是一种用于寻找字符串中最长回文子串的有效算法,其核心在于通过预处理字符串并利用已计算的回文信息减少重复计算,达到线性时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

马拉车算法(Manacher’s Algorithm)

解决的问题是求最长回文子串,神奇之处在于将算法的时间复杂度精进到了O(N)。

重点就是高效地计算回文半径数组 p p p p p p 中每一个元素表示以该字符为中心的最长半径(不包括该中心位置),那么这个半径就是指的以该字符为中心的最长回文子串的长度,然后再通过 int start_index = (i-p[i]) / 2 推算出最长回文子串的起始位置,便可以截取最长回文子串了。

算法由来

在求解最长回文子串的问题时,可以通过中心扩展法来求解:

  • 从一个中心位置开始,向其左右两边扩展寻找最长回文。
  • 考虑到子串的长度可能为奇数也可能为偶数,
    • 中心位置在字符串字符上时,子串是奇数的;
    • 中心位置在两个字符之间时,子串是偶数的;
  • 设字符串长度为 n n n,那么需要考虑的中心位置共有 2 n − 1 2n-1 2n1,需要从每个中心位置向外扩展,寻找以该位置为中心的最长回文子串。

这种解法的时间复杂度是 O ( N 2 ) O(N^2) O(N2),那么能不能将时间复杂度再降低一点?做到线性?马拉车算法就完美地解决了这个问题。

Manacher 算法本质上还是 中心扩散法 ,只不过它使用了类似 KMP 算法的技巧,充分挖掘了已经进行回文判定的子串的特点,提高算法的效率。

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.length();
        if(n < 2) {
            return s;
        }
        int longest_start = 0;
        int longest_end = 0;
        int max_length = 0;
        for(int i=0; i<n; i++) {
            int len1 = expendaroundcenter(s, i, i);
            int len2 = expendaroundcenter(s, i, i+1);
            int temp_len = max(len1, len2);
            if(temp_len > max_length) {
                max_length = temp_len;
                longest_start = i - (temp_len - 1) / 2;
                longest_end = i + temp_len / 2;
            }
        }
        return s.substr(longest_start, longest_end - longest_start + 1);
    }

    int expendaroundcenter(string &s, int left, int right) {
        int L=left;
        int R=right;
        while(L>=0 && R<s.length() && s[R]==s[L]) {
            L--;
            R++;
        }
        return R-L-1;
    }
};

算法流程

以下内容转载自:1,并参考2稍作修改。

1、对原始字符串进行预处理(添加分隔符)

回文字符串以其长度来分,可以分为奇回文(其长度为奇数)、偶回文(其长度为偶数),一般情况下需要分两种情况来寻找回文,马拉车算法为了简化这一步,对原始字符串进行了处理,在每一个字符的左右两边都加上特殊字符(肯定不存在于原字符串中的字符),让字符串变成一个奇回文,长度由 n n n 变为了 2 n + 1 2n+1 2n+1。例如:

原字符串:abba,长度为 4

预处理后:#a#b#b#a#,长度为 9

原字符串:aba,长度为 3

预处理后:#a#b#a#,长度为 7

2、计算辅助数组 p p p(回文半径数组)

以字符串 cabbaf 为例,将预处理后的新字符串 #c#a#b#b#a#f# 变成一个字符数组 arr,定义一个辅助数组 int[] p,这是一个回文半径数组

p 的长度与 arr 等长,p[i] 表示以 arr[i] 字符为中心的最长回文半径(这里设定半径不包括中心位置),p[i]=0 表示只有arr[i] 字符本身是回文子串。

i       0 1 2 3 4 5 6 7 8 9  10 11 12
arr[i]  # c # a # b # b # a  #  f  #
p[i]    0 1 0 1 0 1 4 1 0 1  0  1  0

我们来比对分下一下最长回文半径和原字符串之间的关系。在上面例子中,最长回文子串是#a#b#b#a#,它以arr[6]为中心,半径是4,其代表的原始字符串是abba,而abba的长度为 4,4 就是就是它的长度,是字符串cabbaf中的最长回文子串,那么我们是不是可以得出最长回文半径和最长回文子串长度之间的关系?

让我们再多看几个例子,如 aba,转换后是 #a#b#a#,以字符 b 为中心的回文,半径是 3,3 就是是原字符串的最长回文子串长度。

i       0 1 2 3 4 5 6
arr[i]  # a # b # a #
p[i]    0 1 0 3 0 1 0

再例如effe,转换后是#e#f#f#e#,以最中间的#为中心的回文,半径是 4,4 也是原字符串的最长回文子串长度。

i       0 1 2 3 4 5 6 7 8
arr[i]  # e # f # f # e #
p[i]    0 1 0 1 4 1 0 1 0

因此,最后我们得到最长回文半径和最长回文子串长度之间的关系:int maxLength = p[i]。maxLength表示最长回文子串长度

3、计算最长回文子串起始索引

从辅助数组 p p p 中,可以获得最长回文子串的长度,以及其中心在 a r r arr arr 数组中的位置,想要截取出完整的最长回文子串,我们还需要知道它的起始索引值。

原字符串中最长回文子串的起始位置:
s t a r t = ( i − p [ i ] ) 2 start = \frac{(i-p[i])}{2} start=2(ip[i])

4、如何高效地计算数组 p p p

如果使用第一节中的中心扩展法来计算回文半径数组 p p p 的话,那么到现在为止,这个做法和“中心扩展法求解最长回文子串”没有什么差别,只不过将原始字符串扩展了一下而已,时间复杂度还是 O ( N 2 ) O(N^2) O(N2)的。

上面的代码不太智能的地方是,对新字符串每一个位置进行中心扩散,会导致原始字符串的每一个字符被访问多次,一个比较极端的情况就是:#a#a#a#a#a#a#a#a#。事实上,计算机科学家 Manacher 就改进了这种算法,使得在填写新的辅助数组 p 的值的时候,能够参考已经填写过的辅助数组 p 的值,使得新字符串每个字符只访问了一次,整体时间复杂度由 O ( N 2 ) O(N^2) O(N2) 改进到 O ( N ) O(N) O(N)

具体做法是:在遍历的过程中,除了循环变量 i 以外,我们还需要记录两个变量,它们是 maxRightcenter,它们分别的含义如下:

  • maxRight:记录当前向右扩展的最远边界,即从开始到现在使用“中心扩散法”能得到的回文子串,它能延伸到的最右端的位置 。对于 maxRight 我们说明 3 点:
    • 向右最远”是在计算辅助数组 p 的过程中,向右边扩散能走的索引最大的位置,注意:得到一个 maxRight 所对应的回文子串,并不一定是当前得到的“最长回文子串”,很可能的一种情况是,某个回文子串可能比较短,但是它正好在整个字符串比较靠后的位置;
    • 为什么 maxRight 很重要?因为扫描是从左向右进行的,maxRight 能够提供的信息最多,它是一个重要的分类讨论的标准,因此我们需要一个变量记录它。
    • maxRight 的下一个位置可能是被程序看到的,停止的原因有 2 点:(1)左边界不能扩散,导致右边界受限制也不能扩散,maxRight 的下一个位置看不到;(2)正是因为看到了 maxRight 的下一个位置,导致 maxRight 不能继续扩散。
  • centercenter 是与 maxRight 相关的一个变量,它是上述 maxRight 的回文中心的索引值。对于 center 的说明如下:
  • center 的定义如下:
    c e n t e r = a r g m a x { x + p [ x ] ∣ 0 ≤ x ≤ i } center = argmax\{x+p[x] | 0\leq x \leq i\} center=argmax{x+p[x]0xi}
    x+p[x] 指的就是我们定义的maxRight,它取最大值时的 x 就是当前 center 的值,i 表示的是循环变量,就是从左向右扫描预处理后数组 arr 的循环变量。

maxRightcenter 是一一对应的关系,即一个 center 的值唯一对应了一个 maxRight 的值:
m a x R i g h t = p [ c e n t e r ] + c e n t e r maxRight = p[center]+center maxRight=p[center]+center
因此 maxRightcenter 必须要同时更新。下面的讨论就根据循环变量 imaxRight 的关系展开讨论:

  • i ≥ m a x R i g h t i \geq maxRight imaxRight 时,这就是一开始以及刚刚把一个回文子串扫描完的情况,此时只能根据“中心扩散法”一个一个扫描,逐渐扩大maxRight

  • i < m a x R i g h t i < maxRight i<maxRight 时,根据新字符的回文子串的性质, i i i 关于 center 对称的那个索引(记为 mirror)的 p p p 数组值就很重要。

    我们先看 mirror 的值是多少,因为 center 是中心,imirror 关于 center 中心对称,因此 (mirror + i) / 2 = center,所以
    m i r r o r = 2 ∗ c e n t e r − i 。 mirror = 2 * center - i。 mirror=2centeri
    根据 p[mirror] 的数值从小到大,具体可以分为如下 3 种情况:

    • p[mirror] 的数值比较小,不超过 maxRight - i

      也就是说以 mirror 为中心的最长回文半径不超过 maxRight - i根据以 center 为中心的回文子串的对称性 i i i 为中心的最长回文子串和以 mirror 为中心的最长回文子串对称,所以 i i i 为中心的回文子串的最长回文半径与以 mirror 为中心的最长回文半径相等,此时,直接把数值抄过来即可,即
      p [ i ] = p [ m i r r o r ] 。 p[i] = p[mirror]。 p[i]=p[mirror]

    • p[mirror] 的数值恰好等于 maxRight - i:

      仍然是依据以 center 为中心的回文子串的对称性,但是仅能判断 p[i] 至少为 p[mirror],以 i i i 为中心的回文子串还有可能继续扩散,也就会导致 maxRight 也更新。

      因此,可以先把 p[mirror] 的值抄过来(就相当于把 maxRight - i 的值抄过来),然后继续“中心扩散法”,继续增加 maxRight

    • p[mirror] 的数值大于 maxRight - i:

      在这里插入图片描述

      i i i 为中心的回文子串最长可以扩展到 maxRight 处,所以此时:
      p [ i ] = m a x R i g h t − i p[i]=maxRight-i p[i]=maxRighti

    • 综合以上三种情况,当 i < maxRight 的时候,p[i] 可以参考 p[mirror] 的信息,以 maxRight - i 作为参考标准,p[i] 的值应该是保守的,即二者之中较小的那个值:
      p [ i ] = m i n ( m a x R i g h t − i , p [ m i r r o r ] ) ; p[i] = min(maxRight - i, p[mirror]); p[i]=min(maxRighti,p[mirror]);

      然后再向外扩展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值