Manacher 算法學(xué)習(xí)筆記

前言

給定一個(gè)字符串,求出其最長回文子串。例如:
s="abcd",最長回文長度為 1;
s="ababa",最長回文長度為 5;
s="abccb",最長回文長度為 4,即 bccb。
以上問題的傳統(tǒng)思路大概是,遍歷每一個(gè)字符,以該字符為中心向兩邊查找。其時(shí)間復(fù)雜度為 O(n^2),效率很差。

1975 年,一個(gè)叫 Manacher 的人發(fā)明了一個(gè)算法,Manacher 算法(中文名:馬拉車算法),該算法可以把時(shí)間復(fù)雜度提升到 O(n)。下面來看看馬拉車算法是如何工作的。


傳統(tǒng)思路

i自增,從左到右,然后每個(gè)i為中心,取得最長的回文長度并記錄起止節(jié)點(diǎn)。最多要進(jìn)行兩次n的遍歷,時(shí)間復(fù)雜度為O(n^2)

/**
 * @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為中心點(diǎn)取最長的奇偶回文長度
        let len = Math.max(len1, len2);
        if (len > (end - start)) { // 若比上一段取得回文長
            start = i - Math.floor((len - 1) / 2);
            end = i + Math.floor(len / 2);
        }
    }
    // 根據(jù)取得的最長回文左右兩個(gè)節(jié)點(diǎn)取字符串
    return s.substring(start, end + 1);
};
// 已中心擴(kuò)展判斷最長回文長度
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的思路

首先因?yàn)樘幚砥婊匚暮团蓟匚臅?huì)比較繁瑣,需要分開處理。因此在首尾和各字符間插入#,將長度統(tǒng)一轉(zhuǎn)換為奇數(shù)(算法里常用的解決奇偶繁瑣問題的處理手段)
然后定義一個(gè)輔助數(shù)組p[],其中p[i]對(duì)應(yīng)為以i為中心的最長回文長度。


輔助數(shù)組

p[i]-1就是原字符串中最長回文串的長度。
然后整個(gè)工作的重點(diǎn)就是求解p數(shù)組

圖片.png

設(shè)置兩個(gè)變量,mx 和 id 。mx 代表以 id 為中心的最長回文的右邊界,也就是mx = id + p[id]。

假設(shè)我們現(xiàn)在求p[i],也就是以 i 為中心的最長回文半徑,如果i < mx,如上圖,那么:
if (i < mx) p[i] = min(p[2 * id - i], mx - i);
2 * id - i為 i 關(guān)于 id 的對(duì)稱點(diǎn),即上圖的 j 點(diǎn),而p[j]表示以 j 為中心的最長回文半徑,因此我們可以利用p[j]來加快查找。


代碼

shutup and show me the code

#include <iostream>  
#include <cstring>
#include <algorithm>  

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 的轉(zhuǎn)換
    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]]) {
            // 不需邊界判斷,因?yàn)樽笥?$',右有'\0'
            p[i]++;
        }
        // 我們每走一步 i,都要和 mx 比較,我們希望 mx 盡可能的遠(yuǎn),
        // 這樣才能更有機(jī)會(huì)執(zhí)行 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("請(qǐng)輸入字符串:\n")) {
        scanf("%s", s);
        printf("最長回文長度為 %d\n\n", Manacher());
    }
    return 0;
}

直接貼代碼,核心部分就是
if (i < mx) { p[i] = min(p[2 * id - i], mx - i); }
這句公式。根據(jù)自增的i算出每個(gè)p[i]取其中的max。
整個(gè)算法中,i是自增的,每次循環(huán)要么在擴(kuò)展右邊界(代碼中的變量mx),要么直接得出結(jié)論,所以算法時(shí)間可以到O(n)。

P.S.分享一篇公眾號(hào)漫畫
這個(gè)漫畫也是逐步解釋了算法實(shí)現(xiàn)的一步步過程,比較有意思。


心得總結(jié)

首先求最長回文的方法先要掌握普通的中心擴(kuò)展探索的方法,馬拉車算法在這個(gè)前提條件下加上了右邊界的判斷,在沒有覆蓋到的時(shí)候通過中心擴(kuò)展同時(shí)更新右邊界,最后得出的p[i]數(shù)組里取出最大值即為要求的最長回文長度。


參考

Manacher 算法-劉毅

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容