【字符串】Manacher

Manacher算法的本质是计算以字符串中的“每个字符”和“每两个相邻字符之间的空隙”作为对称中心的最大回文串的长度。所以利用这个性质可以解决一系列与子串是否是回文串、子串有多少是回文串的问题。

namespace Manacher {

static const int MAXN = 1e6;

int n;
char s[MAXN + 10];          // 1-index
int len[2 * MAXN + 10];     // 所有满足 i == l + r 的子串[l, r]中的最大回文长度(最长回文串的长度的一半的上整)

void manacher (char *s) {
    n = strlen (s + 1);
    int lm = 0, rm = -1;      // 最右回文串的位置为[lm, rm]
    for (int i = 2; i <= 2 * n; ++i) {
        int lmid = i / 2, rmid = i - lmid;
        if (rmid > rm) {
            len[i] = (i % 2 == 0);  // 突破最右回文串的边界,调用暴力算法
        } else {
            int j = 2 * (lm + rm) - i;  // 未突破最右回文串的边界, j 和 i 在该串中对称
            len[i] = min (len[j], rm - rmid + 1); // 回文长度至少为 len[j] ,但回文长度不能突破该串
        }
        while (1 <= lmid - len[i] && rmid + len[i] <= n
                && s[lmid - len[i]] == s[rmid + len[i]]) {
            ++len[i];
        }
        if (rmid + len[i] - 1 > rm) {
            lm = lmid - len[i] + 1;
            rm = rmid + len[i] - 1;
        }
    }
}

// 所有满足 i == l + r 的子串[l, r]中的最长回文串的长度
int longest_palindrome_length (int i) {
    return 2 * len[i] - (i % 2 == 0);
}

// 所有满足 i == l + r 的子串[l, r]中的最长回文串的范围
pair<int, int> longest_palindrome_range (int i) {
    int lmid = i / 2, rmid = i - lmid;
    int l = lmid - len[i] + 1, r = rmid + len[i] - 1;
    return {l, r};
}

// 判断子串[l, r]是否为回文串
bool is_palindrome (int l, int r) {
    return longest_palindrome_length (l + r) >= (r - l + 1);
}

}

using namespace Manacher;

心情复杂:过去这么多年都没有好好学习过这个算法,每次都是抄模板,终于碰壁了一次。

想理解上面的算法是怎么工作的,需要先理解朴素算法。这里的朴素算法是指用 \(O(n^2)\) 时间复杂度计算出字符串中每个对称中心的最长拓展长度。

朴素算法是分别处理长度为奇数和长度为偶数的字符串,如下:

const int MAXN = 1e6;

int n;
char s[MAXN + 10];          // 1-index
int len[2 * MAXN + 10];     // 所有满足 i == l + r 的子串[l, r]中的最大回文长度(最长回文串的长度的一半的上整)

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

然后观察发现其实他们大体上算法是可以合并化简的:

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

其他可以替代的办法:字符串哈希、回文自动机、后缀数组

如果只是求最长回文串的长度和位置,而不包含“有多少个不同的回文子串”之类的信息的话,字符串哈希是可以用二分答案的长度然后枚举每个回文中心暴力判断的。

posted @ 2024-04-07 11:58  purinliang  阅读(7)  评论(0编辑  收藏  举报