Manacher 算法

Posted patt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Manacher 算法相关的知识,希望对你有一定的参考价值。

利用回文串的「镜像」特点减少计算。

引理 0

设 $S$ 是一个长度为 $n+1$ 回文串,下标从 $0$ 开始;$T = S[l, r]$ 是 $S$ 的子串。$T$ 是回文串当且仅当 $S[n-r, n-l]$ 是回文串。

先考虑长度为奇数的回文子串(简称为「奇回文子串」),可以求出以每个下标为中心的最长奇回文子串的长度。

用 $P_i$ 表示以下标 $i$ 为中心的最长回文子串,用 $f(i)$ 表示 $P_i$ 的长度,即 $f(i) = |P_i|$ 。

用 $L_i, R_i$ 分别表示 $P_i$ 的左半部分和右半部分。用 $l_i,r_i$ 分别表示 $P_i$ 的左右端点的下标。

引理 1

设 $i$,$j$ 是两个下标。

(1) 若 $j > i$ 且 $r_j le r_i $ 则 $ P_j subseteq P_i $;

(2) 若 $j < i$ 且 $r_j le r_i $ 则 $R_j subseteq P_i$ 。


试着观察这种做法中的冗余计算。

字符串下标从 0 开始,$S[0, n)$ 。
假设当前以第 $i$ 为重心,考虑在 $i$ 之前是否有「镜子」$j$ 使得 $i$ 对着 $j$ 能照出自己。换言之是否存在 $j<i$ 使得以 $j$ 为中心的回文子串能够「波及」$i$ 。用式子表示就是 $f(j) ge 2(i-j) + 1 $ 或者 $j + f(j)/2 ge i$。$i$ 关于 $j$ 的镜像为 $2j -i$。

我们希望 $j$ 不仅能「照出」$i$ 还要能「照出」$i$ 右边尽量多的字符,换言之「照得尽量远」。用式子表示就是 $ j + f(j)/2$ 尽可能大。

回文串在镜像操作下保持不变。
若 $j + f(j)/2 ge i$ 那么区间 $[j-f(j)/2, j+f(j)/2]$ 中以 $i$ 为中心的最长回文子串区间$[j - f(j)/2 , j + f(j)/2]$ 中以 $2j-i$ 为中心的最长回文子串

这个性质我一直绕不过来。


对字符串进行镜像操作

示意图
技术分享图片

字符串的镜像操作相当于序列反转(reverse),因此回文串在镜像操作下保持不变。

$S[j-f(j)/2, j+f(j)/2]$ 中的任意子串都有一个关于 $j$ 的镜像串(即反转串)。

Manacher 算法原理示意图

技术分享图片

复杂度

观察上图。

可 $O(1)$ 地计算出 $|P_i igcap P_j|$ ;若 $P_i igcap P_j$ 能向两侧扩展,那么右边界(border, frontier)将增大;右边界是单调不减的,因此扩展的总复杂度为 $O(n)$ 。于是 Manacher 算法的复杂度为 $O(n)$ 。

实现

void manacher (char str [], int h[], int n) {
    int m = 0;
    static char buf[M]; // M是字符串最大长度的两倍。
    //以'#'开头
    for (int i = 0; i < n; ++i){
        buf[m++] = '#', buf[m++] = str[i];
    }
    buf[m++] = '#'; // 以'#'结尾
    buf[m] = ''; // 必须补零!

    // r:右边界,mid:与r对应的中点;
    for (int i = 0, r = 0, mid = 0; i < m; ++i) { // mid 不必初始化,可随意赋一个初值
        h[i] = i < r ? std::min(r - i, h[2 * mid - i]) : 0;
        while (h[i] <= i && buf[i - h[i]] == buf[i + h[i]])
            ++h[i];
        if (r < i + h[i]) r = i + h[i], mid = i;
    }
}

加了一个也许有点用的小小的优化

void manacher (char str [], int h[], int n) {
    int m = 0;
    static char buf[M]; // M是字符串最大长度的两倍。
    //以'#'开头
    for (int i = 0; i < n; ++i){
        buf[m++] = '#', buf[m++] = str[i];
    }
    buf[m++] = '#'; // 以'#'结尾
    buf[m] = ''; // 必须补零!
    
    // r:右边界,mid:与r对应的中点;
    for (int i = 0, r = 0, mid = 0; i < m; ++i) { // mid 不必初始化,可随意赋一个初值
        h[i] = i < r ? std::min(r - i, h[2*mid - i]) : 0;
        if(h[i] == r - i) { // 一个小小的优化
            while (h[i] <= i && buf[i - h[i]] == buf[i + h[i]])
                ++h[i];
            if (r < i + h[i]) r = i + h[i], mid = i;
        }
    }
}

数组 $h$ 的性质

将原字符串长度记为 $n$,则数组 $h$ 长为 $2n+1$ 。

原字符串的最长回文子串的长度即 $h$ 的最大值减 $1$ 。

对于 $ 0 le i le 2n $,当 $i$ 为偶数时 $h[i] - 1$ 表示原字符串中「右半边起点的下标为 $i/2$」的最长回文串的长度;当 $i$ 为奇数时 $h[i] - 1$ 表示原字符串中「中心点下标为 $lfloor i/2 floor$」的最长回文串的长度。

以上是关于Manacher 算法的主要内容,如果未能解决你的问题,请参考以下文章

Manacher 入门+模板 回文串专用算法

最长回文子串---Manacher算法

什么是Manacher(马拉车)算法-java代码实现

P3805 模板manacher算法(马拉车)

[BZOJ3676][APIO2014]回文串(Manacher+SAM)

Manacher算法详解