【字串】Manacher

purinliang發表於2024-04-07

Manacher演算法的本質是計算以字串中的“每個字元”和“每兩個相鄰字元之間的空隙”作為對稱中心的最大回文串的長度。所以利用這個性質可以解決一系列與子串是否是迴文串、子串有多少是迴文串的問題。

namespace Manacher {
const int MAXN = 1.1e7;

int n;
char s[MAXN + 10];          // 1-index string
int len[2 * MAXN + 10];     // len[mid] means the longest extended length
// for every substr[l, r] that satisfies mid == l + r

void manacher (char *s) {
    n = strlen (s + 1);
    len[2] = 1;
    for (int i = 2, j = 0; i <= (n << 1); ++i) {
        int p = i >> 1, q = i - p;
        int r = ( (j + 1) >> 1) + len[j] - 1;
        len[i] = r < q ? 0 : min (r - q + 1, len[ (j << 1) - i]);
        while (p > len[i] - 1 && q + len[i] < n && s[p - len[i]] == s[q + len[i]])
            ++len[i];
        if (q + len[i] - 1 > r)
            j = i;
    }
}
}

using namespace Manacher;

心情複雜:過去這麼多年都沒有好好學習過這個演算法,每次都是抄模板,終於碰壁了一次。

想理解上面的演算法是怎麼工作的,需要先理解樸素演算法。這裡的樸素演算法是指用 \(O(n^2)\) 時間複雜度計算出字串中每個對稱中心的最長擴充長度。

樸素演算法是分別處理長度為奇數和長度為偶數的字串,如下:

const int MAXN = 1e6;

int n;
char s[MAXN + 10];          // 1-index string
int len[2 * MAXN + 10];     // len[lrs] means the longest extended length
// for every substr[l, r] that satisfies lrs == l + r

void bruteforce (char *s) {
    n = strlen (s + 1);
    for (int lrs = 2; lrs <= 2 * n; lrs += 2) {
        len[lrs] = 1;
        int lmid = lrs / 2, rmid = lrs - lmid;
        // lrs = 2, [1, 1]
        // lrs = 4, [2, 2] -> [1, 3]
        // lrs = 6, [3, 3] -> [2, 4] -> [1, 5]
        while (1 <= lmid - len[lrs] && rmid + len[lrs] <= n
                && s[lmid - len[lrs]] == s[rmid + len[lrs]]) {
            ++len[lrs];
        }
    }
    for (int lrs = 3; lrs <= 2 * n; lrs += 2) {
        len[lrs] = 0;
        int lmid = lrs / 2, rmid = lrs - lmid;
        // lrs = 3, [1, 2]
        // lrs = 5, [2, 3] -> [1, 4]
        // lrs = 7, [3, 4] -> [2, 5] -> [1, 6]
        while (1 <= lmid - len[lrs] && rmid + len[lrs] <= n
                && s[lmid - len[lrs]] == s[rmid + len[lrs]]) {
            ++len[lrs];
        }
    }
}

/**
Return the max length of palindrome substr[l, r] that satisfy l + r == lrs
*/
int max_length_palindrome (int lrs) {
    return 2 * len[lrs] - (lrs % 2 == 0);
}

bool is_palindrome (int l, int r) {
    int lrs = l + r;
    return max_length_palindrome (lrs) >= (r - l + 1);
}

然後觀察發現其實他們大體上演算法是可以合併化簡的:

void bruteforce (char *s) {
    n = strlen (s + 1);
    for (int lrs = 2; lrs <= 2 * n; ++lrs) {
        len[lrs] = (lrs % 2 == 0);
        int lmid = lrs / 2, rmid = lrs - lmid;
        while (1 <= lmid - len[lrs] && rmid + len[lrs] <= n
                && s[lmid - len[lrs]] == s[rmid + len[lrs]]) {
            ++len[lrs];
        }
    }
}

其他可以替代的辦法:字串雜湊、迴文自動機、字尾陣列

如果只是求最長迴文串的長度和位置,而不包含“有多少個不同的迴文子串”之類的資訊的話,字串雜湊是可以用二分答案的長度然後列舉每個迴文中心暴力判斷的。

相關文章