Manacher 算法学习笔记

前言

给定一个字符串,求出其最长回文子串。例如:
s="abcd",最长回文长度为 1;
s="ababa",最长回文长度为 5;
s="abccb",最长回文长度为 4,即 bccb。
以上问题的传统思路大概是,遍历每一个字符,以该字符为中心向两边查找。其时间复杂度为 ,效率很差。

1975 年,一个叫 Manacher 的人发明了一个算法,Manacher 算法(中文名:马拉车算法),该算法可以把时间复杂度提升到 。下面来看看马拉车算法是如何工作的。


传统思路

i自增,从左到右,然后每个i为中心,取得最长的回文长度并记录起止节点。最多要进行两次n的遍历,时间复杂度为

/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
    if (s == null || s.length < 1) { // 传入空直接返回''
        return '';
    }
    let start = 0;
    let end = 0;
    for (let i = 0; i < s.length; i++) {
        let len1 = expandAroundCenter(s, i, i); // 奇回文
        let len2 = expandAroundCenter(s, i, i + 1); // 偶回文
        // 分别以i为中心点取最长的奇偶回文长度
        let len = Math.max(len1, len2);
        if (len > (end - start)) { // 若比上一段取得回文长
            start = i - Math.floor((len - 1) / 2);
            end = i + Math.floor(len / 2);
        }
    }
    // 根据取得的最长回文左右两个节点取字符串
    return s.substring(start, end + 1);
};
// 已中心扩展判断最长回文长度
function expandAroundCenter(s, left, right) {
    var L = left,
    R = right;
    while (L >= 0 && R < s.length && s.charAt(L) == s.charAt(R)) {
        L--;
        R++;
    }
    return R - L - 1;
}

Manacher的思路

首先因为处理奇回文和偶回文会比较繁琐,需要分开处理。因此在首尾和各字符间插入#,将长度统一转换为奇数(算法里常用的解决奇偶繁琐问题的处理手段)
然后定义一个辅助数组p[],其中p[i]对应为以i为中心的最长回文长度。


Manacher 算法学习笔记_第1张图片
辅助数组

p[i]-1就是原字符串中最长回文串的长度。
然后整个工作的重点就是求解p数组

Manacher 算法学习笔记_第2张图片
图片.png

设置两个变量,mx 和 id 。mx 代表以 id 为中心的最长回文的右边界,也就是mx = id + p[id]。

假设我们现在求p[i],也就是以 i 为中心的最长回文半径,如果i < mx,如上图,那么:
if (i < mx) p[i] = min(p[2 * id - i], mx - i);
2 * id - i为 i 关于 id 的对称点,即上图的 j 点,而p[j]表示以 j 为中心的最长回文半径,因此我们可以利用p[j]来加快查找。


代码

shutup and show me the code

#include   
#include 
#include   

using namespace std;

char s[1000];
char s_new[2000];
int p[2000];

int Init()
{
    int len = strlen(s);
    s_new[0] = '$';
    s_new[1] = '#';
    int j = 2;

    for (int i = 0; i < len; i++) {
        s_new[j++] = s[i];
        s_new[j++] = '#';
    }

    s_new[j] = '\0';  // 别忘了哦
    
    return j;  // 返回 s_new 的长度
}

int Manacher()
{
    int len = Init();  // 取得新字符串长度并完成向 s_new 的转换
    int max_len = -1;  // 最长回文长度

    int id;
    int mx = 0;

    for (int i = 1; i < len; i++) {
        if (i < mx) {
            p[i] = min(p[2 * id - i], mx - i);
            // 需搞清楚上面那张图含义, mx 和 2*id-i 的含义
        }
        else {
            p[i] = 1;
        }
        while (s_new[i - p[i]] == s_new[i + p[i]]) {
            // 不需边界判断,因为左有'$',右有'\0'
            p[i]++;
        }
        // 我们每走一步 i,都要和 mx 比较,我们希望 mx 尽可能的远,
        // 这样才能更有机会执行 if (i < mx)这句代码,从而提高效率
        if (mx < i + p[i]) {
            id = i;
            mx = i + p[i];
        }

        max_len = max(max_len, p[i] - 1);
    }

    return max_len;
}

int main() {
    while (printf("请输入字符串:\n")) {
        scanf("%s", s);
        printf("最长回文长度为 %d\n\n", Manacher());
    }
    return 0;
}

直接贴代码,核心部分就是
if (i < mx) { p[i] = min(p[2 * id - i], mx - i); }
这句公式。根据自增的i算出每个p[i]取其中的max。
整个算法中,i是自增的,每次循环要么在扩展右边界(代码中的变量mx),要么直接得出结论,所以算法时间可以到O(n)。

P.S.分享一篇公众号漫画
这个漫画也是逐步解释了算法实现的一步步过程,比较有意思。


心得总结

首先求最长回文的方法先要掌握普通的中心扩展探索的方法,马拉车算法在这个前提条件下加上了右边界的判断,在没有覆盖到的时候通过中心扩展同时更新右边界,最后得出的p[i]数组里取出最大值即为要求的最长回文长度。


参考

Manacher 算法-刘毅

你可能感兴趣的:(Manacher 算法学习笔记)