最长回文子串——马拉车算法详解

本文详细介绍马拉车算法(Manacher's Algorithm),这是一种用于寻找字符串中最长回文子串的有效方法。文章通过实例解释了该算法的基本原理,包括字符间插入特殊字符、计算半径数组以及如何确定最长回文子串,并提供了Python实现。
部署运行你感兴趣的模型镜像

马拉车算法(Manacher‘s Algorithm)是用来解决求取一个字符串的最长回文子串问题的。此算法充分利用了回文字符串的性质,将算法复杂度降到了线性,非常值得一学。

我将网上所有讲解马拉车算法的文章基本看了一遍,总结出了最通俗易懂的介绍,同时用 python 进行了实现。

题目

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

所谓回文字符串,指的是无论从左往右读还是从右往左读,结果都是一样的,也叫做对称字符串。

比如 “google” 的最长回文子串为 “goog”。

马拉车算法

这个算法的总框架是,遍历所有的中心点,寻找每个中心点对应的最长回文子串,然后找到所有中心点对应的最长回文子串,与求取一个字符串的最长回文子串中的第4个方法思想类似。

但是,第4个方法的复杂度为 O(n2) O ( n 2 ) ,而马拉车算法对其进行了改进,将复杂度变为了线性。

1、字符之间插入特殊字符

回文串的中心点有两种,如果长度为奇数,则回文串中心为最中间的那个字符,如 “aba” 的 “b”;如果长度为偶数,则回文串中心为最中间的两个字符的分界,如 “abba” 的 “bb”。为了统一,马拉车算法首先将字符串的每个字符之间(包括首尾两端)插入一个特殊符号,如#,这个符号必须是原字符串中所没有的。

比如我们的原字符串为

s = "google"

那么插入#号之后,变为了

ss = "#g#o#o#g#l#e#"

这样做之后,字符串的长度肯定是奇数,因为插入的#号的个数一定等于字符个数+1,因此总长度是偶数+奇数=奇数。这样,循环时便不用考虑原字符串长度的奇偶性了。

2、计算半径数组 p

接下来,我们需要想办法计算出一个数组 p,这个数组的长度与处理后的字符串 ss 等长,其中 p[i] 表示以 ss[i] 为中心的最长回文子串的半径(不包括 p[i] 本身),暂且把它成为半径数组。如果 p[i] = 0,则说明回文子串就是 ss[i] 本身。

比如 “#a#b#” 的半径数组为 [0, 1, 0, 1, 0]。

为了在搜索回文子串时避免总是判断是否越界,我们在 ss 的首尾两端加上两个不同的特殊字符,保证这两个特殊字符不会出现在 ss 中。比如为 $^。则 ss 变为了

ss = "$#g#o#o#g#l#e#^"

数组 p 的最大半径,就是我们要寻找的最长回文子串的半径。因此只要计算出了数组 p,最后答案就呼之欲出了。

如何计算数组 p

一般的方法,是以中心点为中心,挨个将半径逐步扩张,直至字符串不再是回文字符串。但是这样做,整体的算法复杂度为 O(n2) O ( n 2 ) 。马拉车算法的关键之处,就在于巧妙的应用了回文字符串的性质,来计算数组 p。

马拉车算法在计算数组 p 的整个流程中,一直在更新两个变量:

  • id:回文子串的中心位置
  • mx:回文子串的最后位置

使用这两个变量,便可以用一次扫描来计算出整个数组 p,关键公式为:

p[i] = min(mx-i, p[2 * id - i])

我们用图示来理解这个公式,如下图:
这里写图片描述
当前,我们已经得到了 p[0…i-1],想要计算出 p[i] 来。红1为以 j 为中心的回文子串,红2为以 i 为中心的回文子串,红3为以 id 为中心的回文子串(首尾两端分别为mx的对称点和mx)。

那么,如果 mx 在 i 的右边,则我们可以通过已经计算出的 p[j] 来计算 p[i],其中 j 与 i 的中心点为 id。这里分两种情况:

  • 先直接令 p[i] 的回文子串就等于 p[j] 的回文子串,即红2长度等于红1,然后判断红2的末尾是否超过了 mx,如果没有超过,则说明 p[i] 就等于 p[j]。
    为什么呢?
    因为以 id 为中心的回文子串为红3,包含了红1和红2,而且红1和红2以 id 为中心,那么一定有红2=红1。并且已经知道,红1是以 j 为中心的最长子串,那么红2也肯定是以 i 为中心的最长子串。
  • 如果红2的末尾超过了 mx,那么就只能让 p[i] = mx - i了,即我可以保证至少半径到 mx 这个位置,是可以回文的,但是一旦往右超出了 mx,就不能保证了,剩下的只能用笨方法慢慢扩张来得到最长回文子串。

那如果红2的左边超出了mx的对称点,怎么办?不会出现这种情况的,因为红1的右边不会超过mx。如果超过了mx,那么在上一次迭代中,id应该更新为j,mx应该更新为 j+p[j]。在迭代中,会始终保证 mx 是所有已经得到的回文子串末端最靠右的位置。

另外,如果 mx 不在 i 的右边呢?那就利用不了红3的对称性了,只能使用笨方法慢慢扩张了。

3、数组 p 中的最大值,即为最长回文子串的半径

根据半径数组 p 的定义,如果最大值对应位置为 i,则最大回文子串为 ss[i - p[i] : i + p[i] + 1]

python 实现

马拉车的代码如下,其中 center 即为 id,且特殊字符使用的是 \0,\1,\2。

def longestPalindrome5(s):
    """
    :type s: str
    :rtype: str

    马拉车算法。Manacher发明出来的。
    时间复杂度为O(n)。
    """
    if len(s) <= 1:
        return s

    # 每个字符之间插入 \1
    ss = '\0\1' + '\1'.join([x for x in s]) + '\1\2'
    p = [0] * len(ss)
    center = 0
    mx = 0
    max_str = ''
    for i in range(1, len(p)-1):

        if i < mx:
            j = 2 * center - i # i 关于 center 的对称点
            p[i] = min(mx-i, p[j])

        # 尝试继续向两边扩展,更新 p[i]
        while ss[i - p[i] - 1] == ss[i + p[i] + 1]: # 不必判断是否溢出,因为首位均有特殊字符,肯定会退出
            p[i] += 1

        # 更新中心
        if i + p[i] > mx:
            mx = i + p[i]
            center = i

        # 更新最长串
        if 1 + 2 * p[i] > len(max_str):
            max_str = ss[i - p[i] : i + p[i] + 1]

    return max_str.replace('\1', '')

您可能感兴趣的与本文相关的镜像

Python3.10

Python3.10

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

<think>好的,我现在需要帮助用户理解马拉算法(Manacher's Algorithm)的实现方法和原理,以及如何用它优化最长回文子串检测。用户之前已经了解过中心扩展法,现在想升级到更高效的算法。我需要参考提供的引用内容,特别是引用1、3和4中的信息,来构建一个清晰的解释。 首先,我得回忆马拉算法的核心思想。根据引用3,马拉算法通过预处理字符串和处理奇偶回文的情况,将时间复杂度降到了线性O(n)。引用4中的代码示例展示了如何插入分隔符(如#号)来统一处理奇偶长度的回文,这是关键的一步。同时,算法利用回文半径和对称性来避免重复计算,这应该和动态规划或中心扩展法中的某些优化有关。 接下来,我需要拆解算法的步骤。预处理部分,插入特殊字符将字符串转换为奇数长度,这样可以统一处理奇偶情况。例如,字符串"abba"变成"#a#b#b#a#",每个字符周围都有#,这样无论原回文是奇数还是偶数长度,转换后都变成奇数长度,方便处理。 然后,维护几个关键变量:当前最大右边界R,对应的中心C,以及每个位置的回文半径数组P。当遍历每个字符时,利用对称性(mirror)来确定初始回文半径,这样可以减少不必要的扩展次数。例如,如果当前字符i在R的左侧,可以利用其关于C的对称点mirror的P值来初始化P[i],避免从零开始扩展。 在代码实现部分,需要处理不同的case,比如i在R内或外的情况,然后进行中心扩展。引用4中的Java代码展示了如何处理这些情况,包括更新C和R的位置,以及记录最大回文长度和对应的中心点。我需要将这段代码转换为C++,并确保正确性,比如处理字符串预处理和回文半径的计算。 此外,用户可能对算法的时间复杂度分析感兴趣。马拉算法的时间复杂度是O(n),因为每个字符的扩展操作总体上是线性的,每个字符最多被扩展一次。这需要详细解释,确保用户明白为什么这个算法比中心扩展法更高效。 同时,用户的问题提到了优化方法和代码示例,所以需要将理论解释与代码结合,逐步说明每个部分的作用。比如,预处理字符串的部分,如何构造新的字符串,然后初始化P数组,遍历每个字符,利用对称性确定初始半径,然后进行扩展,更新最大边界和中心。 可能还需要对比其他方法,如动态规划和中心扩展,突出马拉算法的优势,如线性时间和较低的空间复杂度(O(n))。引用2中提到的动态规划空间复杂度较高,而马拉算法在空间上只需要一个数组,因此更优。 测试用例也是重要的,需要提供几个示例来验证代码的正确性,比如"babad"应该返回"bab"或"aba",而"cbbd"应该返回"bb"。这有助于用户理解代码的实际效果。 最后,确保回答结构清晰,分步骤讲解,原理与实现并重,引用相关的参考文献,并在末尾添加相关问题,引导用户深入思考。比如,如何修改代码以返回最长回文子串本身,或者如何处理特殊字符的情况等。</think>### 马拉算法(Manacher's Algorithm)详解 --- #### 一、算法核心原理 马拉算法通过**预处理字符串**和**利用回文对称性**,将时间复杂度优化到$O(n)$。其核心优化点包括: 1. **预处理插入分隔符**:将任意字符串转换为**奇数长度**,统一处理奇偶回文 - 例如:`"abba" → "#a#b#b#a#"` 2. **维护回文半径数组**:记录每个位置的最长回文半径$P[i]$ 3. **动态右边界维护**:通过当前最大右边界$R$和中心点$C$减少重复计算[^3] --- #### 二、算法步骤分解 ##### 步骤1:字符串预处理 ```cpp string preprocess(const string& s) { string t = "^#"; for (char c : s) { t += c; t += '#'; } t += "$"; // 避免边界判断 return t; } ``` *作用*:原字符串`"abc"`转换为`^#a#b#c#$`,所有回文子串长度变为奇数[^4] --- ##### 步骤2:核心算法实现 ```cpp string manacher(const string& s) { if (s.empty()) return ""; string t = preprocess(s); int n = t.size(); vector<int> P(n, 0); // 回文半径数组 int C = 0, R = 0; // 当前最大回文中心及右边界 int max_len = 0; // 最大回文长度 int center = 0; // 最长回文的中心 for (int i = 1; i < n-1; ++i) { int mirror = 2 * C - i; // 计算i的对称点 // 关键优化:利用对称性初始化P[i] if (i < R) { P[i] = min(R - i, P[mirror]); } // 中心扩展 while (t[i + P[i] + 1] == t[i - P[i] - 1]) { P[i]++; } // 更新右边界和中心 if (i + P[i] > R) { C = i; R = i + P[i]; } // 记录最大值 if (P[i] > max_len) { max_len = P[i]; center = i; } } // 计算结果在原字符串中的位置 int start = (center - max_len) / 2; return s.substr(start, max_len); } ``` --- #### 三、关键优化技术解析 | 技术 | 实现方式 | 优化效果 | |------|----------|----------| | 对称性利用 | 通过镜像点`mirror`获取初始半径 | 避免重复计算 | | 动态边界维护 | 实时更新$C$和$R$ | 限制扩展次数 | | 预处理统一处理 | 插入分隔符`#` | 消除奇偶差异 | --- #### 四、时间复杂度分析 - **预处理阶段**:$O(n)$ - **主循环阶段**:每个字符最多被扩展一次,总扩展次数不超过$n$次 → **整体$O(n)$**[^3] --- #### 五、测试用例验证 ```cpp int main() { cout << manacher("babad") << endl; // 输出 "bab" 或 "aba" cout << manacher("cbbd") << endl; // 输出 "bb" cout << manacher("a") << endl; // 输出 "a" return 0; } ``` --- #### 六、与其他算法对比 | 算法 | 时间复杂度 | 空间复杂度 | 适用场景 | |------|------------|------------|----------| | 暴力法 | $O(n^3)$ | $O(1)$ | 教学演示 | | 中心扩展 | $O(n^2)$ | $O(1)$ | 常规应用 | | 动态规划 | $O(n^2)$ | $O(n^2)$ | 子问题分析 | | **马拉算法** | $O(n)$ | $O(n)$ | 大规模数据处理[^2] | --- ### 七、算法特性总结 1. **线性时间复杂度**:适用于处理百万级长度的字符串 2. **空间效率高**:仅需一个长度$n$的辅助数组 3. **边界处理技巧**:通过添加`^`和`$`符号避免特殊判断[^4] --- ### 相关问题 1. 如何处理字符串中的特殊字符(如`#`本身)? 2. 如何修改算法同时返回所有最长回文子串? 3. 为什么预处理时选择`#`作为分隔符? 4. 如何证明该算法的时间复杂度是严格的$O(n)$?[^3]
评论 10
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值