1. 程式人生 > >Manacher 算法

Manacher 算法

wid class $1 還要 長度 master block 反轉 group

利用回文串的「鏡像」特點減少計算。

引理 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 \bigcap P_j|$ ;若 $P_i \bigcap 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] = '\0'; // 必須補零!

    // 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] = '\0'; // 必須補零!
    
    // 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 \rfloor$」的最長回文串的長度。

Manacher 算法