Manacher算法详细解析 (马拉车算法)

本文详细阐述了Manacher算法在求解最长回文子串问题中的应用,对比了暴力算法的高时间复杂度,展示了Manacher算法如何通过预处理和利用已知回文长度减少计算,实现O(n)的时间复杂度。关键步骤包括维护最右臂长和中心点,以及利用对称性快速计算新的回文长度。

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

应用场景:

        如求最长回文子串的题目,或者结合其他算法求解算法题目时可以用到。

        求最长回文子串题目:给你一个字符串s,询问s最长回文子串的长度是多少?(如abaa的最长回文子串是aba,长度为3。)

        结合其他算法的题目:

                        Problem M. Mediocre String Problem(马拉车+拓展KMP)

算法解析:

        在讲解Manacher之前,先考虑如何用暴力算法求解最长回文子串题目

        暴力算法思路:其实暴力算法思路很简单,我们只需要利用for循环将字符串从头到尾每个字符进行遍历。遍历时,将每个字符设为回文子串中心点,并朝左右方向同时扩展,扩展到左右字符不相同时,即得出以该字符为回文子串中心时的最大回文子串长度。最后将以每个字符为中心的最大回文子串长度求个最大值,就是最后的结果了。(P.s.: 扩展时要执行俩种扩展,一种是假设回文子串为奇数的扩展, 一种是假设回文子串为偶数的扩展,如cbabc 和 cbaabc, cbabc 中以下标2的a为中心时,比较第2位和第2位、第1位和第3位、第0位和第4位,并以此类推。cbaabc中以下表2的a为中心时,比较第2位和第3位、第1位和第4位、第0位和第5位,并以此类推。)

        例:给字符串 babcbabcbaccba
        每个字符为回文子串中心时的最大回文子串长度序列为:1 3 1 7 1 9 1 5 1 1 1 1 1 1

        最后求最大值,得出的最长回文子串的长度便是9(abcbabcba)。

        暴力算法缺点:时间复杂度O(n*n)过高。

       

        Manacher算法思路:在上述暴力算法中,我们可以得到每个字符为回文子串中心时的最大回文子串长度,简称为臂长。那Manacher算法的核心思路就是利用之前求得的臂长来减少时间复杂度。

        算法预处理:算法首先需要预处理字符串,如将aba 变成#a#b#a#,  将abba变成#a#b#b#a#, 这是利用2*x+1的结果始终为奇数的特性,将原字符串都变为奇数长度,因为这样我们就不用像暴力算法中的扩展一样需要特别考虑长度为偶数的回文子串了,例如#a#b#a# 当b为回文子串中心点时,长度为配对的个数3(# = #, a = a, # = #),而#a#b#b#a# 当下标4的#为回文子串中心点时,长度为配对的个数4(b = b, # = #, a = a, # = #), 这样的话无论是奇数长度的回文串aba还是偶数长度的abba都可以统一考虑顺利求解了。

        核心思路:设Len[i] 为扩展后的的字符串中的字符i为回文子串中心时的最长回文子串长度的半径,而这个半径也恰恰等于字符串扩展前的最长回文子串长度,例如#a#b#a#, Len[b] = 3,3即是扩展后的字符串的最长回文子串长度的半径(#=#,a=a, #=#),也是扩展前的最长回文子串长度(aba)。那么对于求出最长回文子串的这道题目其实就是求出每个字符的Len[i]并从其中挑出最大值。上面说过,Manacher算法的核心思路就是利用之前求得的臂长( 即之前求出的Len值) 来减少时间复杂度,也就是说通过前面求出的Len值来加速求出当前下标i的 Len[i],快速求出所有的Len 值就是该算法的目的。

我们拿经典样例举个例子:babcbabcbaccba

        假设我们已经求出了前面若干个Len值,即Len[0...i-1],  当要求第11位的 a 的Len[i] 时,我们如何快速求出?

        首先我们需要一直维护一个最右臂长 r, 再维护拥有最右臂长的中心点下标值mid,r = mid + Len[mid], 形象点说就是,维护前面字符中手臂伸的最靠右的,(l, r) 代表着以mid为回文子串中心的回文子串的范围。

      目前已知(l,r)是回文串,也是已知达到最右覆盖的。我们可以快速的求出Len[i]的值,而快速求它需要考虑俩种可能性。

  •        一种是当 i < r 时,当i 处于(l,r)回文串的半径内时,我们就可以利用回文串的特性先寻找 i 的对称点 i' , 对称的i'也等于2*mid-i ,因为我们已经知道对称点i'的Len值(臂长),我们可以得出一个结论Len[i] = min(r - i, Len[2*mid-i]), 这里会有一个疑问,即为什么我们不直接令Len[i] = Len[2*mid-i],而要和r - i 取个最小值呢?,那是因为能够确保的半径值不超过r-i,你不能确保i'镜像的臂长都在(l,r)内。而如果i'镜像的臂长都在(l,r)内,那i点的臂长至少也是镜像i'臂长的长度,超出r的部分,进行暴力比较即while (new_str[i+Len[i]] == new_str[i-Len[i]]) Len[i]++;来更新i点的臂长即可。
  •        另一种就是当 i >= r 时,Len[i]直接设置为默认值1,因为这时的 i 无法利用镜像i' 来获取臂长的至少值,i的右边均是未知值,都需要暴力比较,即while (new_str[i+Len[i]] == new_str[i-Len[i]]) Len[i]++;

      这样就可以快速算出当前i的Len值,每算完一个Len[i]后,我们都需要维护最右臂长 r 和 下标mid 值,即 if((i+Len[i]) > r) {r = i + Len[i]; mid = i} , 因为维护的这个区间是一个回文串,后续可以使用对称点的特性来迅速求出Len[i]值。

时间复杂度解析:

        暴力算法中我们需要对 i 一直拓展到拓展不了为止。而manacher算法可以帮助我们直接跳过拓展 i i + min(r-i, Len[2*mid-i])这部分,从 i + min(r-i, Len[2*mid-i])+1开始拓展。

        所以时间复杂度为O(n), n为拓展后的字符串长度。由于对于每个位置,扩展要么从当前的最右侧臂长 r 开始,要么只会进行一步,而 r 最多向前走 O(n)步,因此算法的复杂度为 O(n)。

int longestPalindrome(string s) {
    string new_str = "";
    new_str += "@";
    for (int i = 0;i < str.length(); i++) {
        new_str += "#";
        new_str += str[i];
    }
    new_str += "#";
    new_str += "$";
    cout << new_str << endl;
    //new_str 存储 扩充后的字符串,如 @#a#b#a#$
    int len = new_str.length();
    int r = 0, mid = 0, ans = 0;//r 表示目前最右回文子串半径,mid则是该字串的中心下标,ans存储最长回文子串长度。
    for (int i = 1;i < len-1; i++) {
        if (i < r) {
           Len[i] = min(r - i, Len[2*mid-i]);
        }
        else Len[i] = 1;

        while (new_str[i+Len[i]] == new_str[i-Len[i]]) {
            Len[i]++;
        }
        if ((i+Len[i]) > r) {
            r = i + Len[i];
            mid = i;
        }
        ans = max(ans, (Len[i] - 1));
    }
    return ans;
}

### 马拉算法 (Manacher's Algorithm)C++ 实现 马拉算法的核心在于通过对原字符串进行预处理,使得无论是奇数长度还是偶数长度的回文都可以被统一处理为奇数长度的形式[^4]。以下是完整的 C++ 实现代码: ```cpp #include <iostream> #include <string> #include <vector> using namespace std; // Manacher's Algorithm to find the longest palindromic substring string preprocess(const string& s) { if (s.empty()) return "^$"; string ret = "^"; for (char c : s) { ret += "#" + string(1, c); } ret += "#$"; return ret; } void manachersAlgorithm(const string& inputStr) { string str = preprocess(inputStr); // Preprocess the original string by adding '#' between characters. int n = str.length(); vector<int> P(n, 0); // Array to store palindrome radius at each position. int center = 0; // Center of the current palindrome being expanded. int mirror = 0; // Mirror index of `i` around `center`. int rightBoundary = 0;// Right boundary of the current palindrome. for (int i = 1; i < n - 1; ++i) { mirror = 2 * center - i; // Calculate mirrored index about the current center. if (rightBoundary > i) { // If within the bounds of a known palindrome, P[i] = min(rightBoundary - i, P[mirror]); // Use symmetry property and avoid unnecessary comparisons. } // Attempt to expand palindrome centered at `i`. while (str[i + 1 + P[i]] == str[i - 1 - P[i]]) { P[i]++; } // Update the center and right boundary if necessary. if (i + P[i] > rightBoundary) { center = i; rightBoundary = i + P[i]; } } // Find the maximum element in P array which corresponds to the longest palindrome. int maxLen = 0; int centerIndex = 0; for (int i = 1; i < n - 1; ++i) { if (P[i] > maxLen) { maxLen = P[i]; centerIndex = i; } } cout << "Longest Palindrome Substring: "; for (int j = centerIndex - maxLen; j <= centerIndex + maxLen; ++j) { if (str[j] != '#') cout << str[j]; // Ignore added special character '#'. } cout << endl; } int main() { string inputStr; cout << "Enter the string: "; cin >> inputStr; manachersAlgorithm(inputStr); return 0; } ``` #### 关键点解析 1. **字符串预处理** 原始字符串通过在字符间插入特殊字符(如 `#`),将其转换成一个新的字符串形式,这样无论原始字符串是奇数长度还是偶数长度,最终都会变成奇数长度。 2. **中心扩展优化** 使用镜像对称性质减少不必要的计算量,在某些情况下可以直接利用之前已经计算好的结果来加速当前位置的回文半径计算过程[^5]。 3. **边界条件处理** 当前索引超出右边界时,则需要重新从头开始逐个匹配字符以找到新的有效范围;如果未越界则可借助已有数据快速得出结论。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值