【字符串】Manacher 算法

Manacher算法:高效寻找最长回文子串

Manacher 算法:在线性时间内寻找最长回文子串

Manacher 算法(马拉车算法)是一种用于在线性时间 O(n) 内找到字符串中最长回文子串的高效算法。它巧妙地利用了回文串的对称性,避免了暴力解法 O(n²) 的时间复杂度。


📚 1. 问题背景

问题:给定一个字符串 s,找出其中最长的回文子串。

回文串:正读和反读都相同的字符串,例如 "aba""abba"

暴力解法

  • 枚举每个可能的中心点。
  • 从中心向两边扩展,检查是否为回文。
  • 时间复杂度:O(n²)

Manacher 算法:通过预处理和巧妙利用回文对称性,将时间复杂度优化到 O(n)


🧩 2. 核心思想与预处理

2.1 预处理:统一奇偶回文

Manacher 算法的一个关键步骤是预处理字符串,以统一处理奇数长度和偶数长度的回文串。

  • 问题:奇数回文(如 "aba")有明确的中心字符,而偶数回文(如 "abba")的中心在两个字符之间。
  • 解决:在原字符串的每个字符之间以及首尾插入一个不会在原字符串中出现的分隔符(通常用 '#'),并在首尾也加上分隔符。

示例

原字符串:    a   b   a   a   b   a
预处理后:  # a # b # a # a # b # a #

效果

  • 所有回文串的长度都变成了奇数
  • 原来的奇数回文 "aba" 变成 #a#b#a#,中心是 'b'
  • 原来的偶数回文 "abba" 变成 #a#b#b#a#,中心是中间的 '#'
  • 这样,我们只需要处理一种情况(奇数长度回文),大大简化了逻辑。

2.2 定义 P 数组

  • P[i] 表示在预处理后的字符串中,以位置 i 为中心的最长回文半径
  • 半径:从中心 i 到回文串边界的长度(包含中心)。
  • 回文长度P[i] 就是该回文串的长度(因为预处理后都是奇数长度)。

示例

字符串:     #  a  #  b  #  a  #  a  #  b  #  a  #
索引 i:     0  1  2  3  4  5  6  7  8  9 10 11 12
P[i] 值:    1  2  1  2  1  4  1  4  1  2  1  2  1
  • i=5 时,P[5]=4,表示以 s[5]='a' 为中心的最长回文是 #a#a#a#,半径为 4,长度为 4。
  • 注意:这个回文对应原字符串中的 "aba"

🧠 3. 核心算法:利用回文对称性

Manacher 算法的精髓在于,它利用了已知回文串的对称性来加速计算。

3.1 关键变量

  • center:当前已知的、能覆盖最右端的回文串的中心。
  • right:当前已知的、能覆盖的最右边界(right = center + P[center])。

3.2 算法流程

遍历预处理后的字符串的每个位置 i

  1. 初始化 P[i]

    • 如果 i < right,说明 icenter 的回文覆盖范围内。
    • 找到 i 关于 center 的对称点 j = 2 * center - i
    • 利用对称性,P[i] 的初始值可以设为 min(P[j], right - i)
      • P[j]:对称点 j 的回文半径。
      • right - ii 到右边界 right 的距离。
      • 取最小值是因为 i 的回文不能超过 center 回文的右边界。
    • 如果 i >= right,说明 i 不在任何已知回文的覆盖范围内,P[i] 初始化为 1(至少是自身)。
  2. 尝试扩展

    • P[i] 的初始值基础上,尝试从 i - P[i]i + P[i] 开始,继续向两边扩展,只要字符相等就增加 P[i]
  3. 更新 centerright

    • 如果 i + P[i] > right,说明以 i 为中心的回文扩展到了比之前更远的右边界。
    • 更新 center = iright = i + P[i]

🧪 4. 详细示例

以字符串 "abaab" 为例:

  1. 预处理s = "#a#b#a#a#b#" (为简化,我们只考虑到 #a#b#a#a#b#

  2. 遍历过程

i字符centerrightj (对称点)P[i] 初始值扩展后 P[i]是否更新 center/right
0#01-11是 (center=0, right=1)
1a01-112是 (center=1, right=3)
2#130min(P[0]=1, 3-2=1)=11
3b13-112是 (center=3, right=5)
4#352min(P[2]=1, 5-4=1)=11
5a351min(P[1]=2, 5-5=0)=0 -> 14是 (center=5, right=9)
6#594min(P[4]=1, 9-6=3)=11
7a593min(P[3]=2, 9-7=2)=22
8#592min(P[2]=1, 9-8=1)=11
9b591min(P[1]=2, 9-9=0)=0 -> 12
10#590min(P[0]=1, 9-10<0)=11
  1. 结果
    • P[5] = 4 是最大值。
    • 回文长度为 4,对应原字符串中的 "abba"(注意:P[i] 的值就是回文长度)。
    • 原字符串中的起始位置可以通过 (i - P[i]) / 2 计算。

💻 5. C++ 实现

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

class Manacher {
public:
    /**
     * 找到字符串 s 中最长回文子串
     * @param s 输入字符串
     * @return 最长回文子串
     */
    string longestPalindrome(string s) {
        if (s.empty()) return "";

        // 1. 预处理:插入 '#' 分隔符
        string processed = preprocess(s);
        int n = processed.length();

        // 2. 创建 P 数组,P[i] 表示以 i 为中心的最长回文半径
        vector<int> P(n, 0);
        int center = 0, right = 0; // 当前最右回文的中心和右边界

        int maxLen = 0;        // 记录最长回文的长度
        int maxCenter = 0;     // 记录最长回文的中心

        // 3. 遍历预处理后的字符串
        for (int i = 0; i < n; ++i) {
            // 3.1 利用对称性初始化 P[i]
            int mirror = 2 * center - i; // i 关于 center 的对称点

            if (i < right) {
                // i 在 center 的回文范围内
                P[i] = min(right - i, P[mirror]);
            } else {
                // i 不在任何已知回文的覆盖范围内
                P[i] = 1;
            }

            // 3.2 尝试扩展回文
            // 注意:扩展时需要检查边界
            while (i + P[i] < n && i - P[i] >= 0 && 
                   processed[i + P[i]] == processed[i - P[i]]) {
                P[i]++;
            }

            // 3.3 更新 center 和 right
            if (i + P[i] > right) {
                center = i;
                right = i + P[i];
            }

            // 3.4 更新最长回文记录
            if (P[i] > maxLen) {
                maxLen = P[i];
                maxCenter = i;
            }
        }

        // 4. 根据 P 数组的结果,提取原字符串中的最长回文
        // maxLen 是回文长度(在预处理字符串中)
        // 起始位置在原字符串中为 (maxCenter - maxLen) / 2
        // 长度为 maxLen - 1 (因为预处理字符串中的长度比原字符串中的实际回文长度多1)
        int start = (maxCenter - maxLen) / 2;
        return s.substr(start, maxLen - 1);
    }

private:
    /**
     * 预处理函数:在字符串的每个字符之间以及首尾插入 '#'
     * @param s 原字符串
     * @return 预处理后的字符串
     */
    string preprocess(const string& s) {
        string result = "#";
        for (char c : s) {
            result += c;
            result += '#';
        }
        return result;
    }
};

// 测试函数
int main() {
    Manacher manacher;

    vector<string> testCases = {"abaab", "abacabad", "racecar", "abcdef", "a", ""};

    for (const string& test : testCases) {
        string result = manacher.longestPalindrome(test);
        cout << "输入: \"" << test << "\" -> 最长回文: \"" << result << "\"" << endl;
    }

    return 0;
}

📊 6. 复杂度分析

  • 时间复杂度O(n)
    • 虽然有嵌套循环,但 right 指针只会向右移动,最多移动 n 次。每个字符的扩展操作总和是 O(n)
  • 空间复杂度O(n)
    • 用于存储预处理后的字符串和 P 数组。

✅ 7. 总结

Manacher 算法通过两个关键技巧实现了线性时间复杂度:

  1. 预处理:通过插入分隔符,将奇偶回文统一为奇数长度,简化了问题。
  2. 利用对称性:利用已知回文串的对称性,避免了对每个中心都进行完全的暴力扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值