PHP實現 Manacher 最大回文子串算法

題目:給一個字符串,找出它的最長的迴文子序列的長度。
例如,如果給定的序列是“BBABCBCAB”,則輸出應該是7,“BABCBAB”是在它的最長迴文子序列。

輸入:
aaaa
1212asdfdsa1144121
輸出:
4
7

這裏我們還是將其封裝成函數調用

何謂迴文序列

迴文序列就是正向和反向完全一樣的序列,比如 asdfdsaaaaa

接下來我們由淺及深,一步一步來說一下 Manacher 算法,這裏我們只說 PHP 的實現

判斷迴文序列

通過 PHP 很容易實現,只需要判斷正向反向是否相同就行了

function huiwen($str)
{
    $str2 = implode(array_reverse(str_split($str)), "");

    if ($str == $str2) {
        echo "$str, yes";
    } else {
        echo "$str, no";
    }
}

接下來我們就來講解最大回文子串如何去求

第一版代碼

求迴文子串,毫無疑問需要每個字符遍歷一遍,分別求出來各個字符的迴文長度,然後選出最長的那一個
代碼如下:

function palindrome($str)
{   
    $n = strlen($str);
    $pos = 0;
    $max = 0;

    for ($i = 0; $i < $n; $i++) { 
        for ($j = 0; ($i - $j >= 0) && ($i + $j < $n); $j++) { 
            if ($str[$i - $j] != $str[$i + $j]) {
                break;
            }
            if ($j > $max) {
                $max = $j;
                $pos = $i;
            }
        }
    }
    var_dump(substr($str, $pos - $max, $max * 2 + 1));
}

可以看到,這個代碼是有bug的,因爲迴文子串可能是奇數長度,也可能是偶數長度,因爲我們是以一個字符爲中心來求的,所以用這種方法只能求出奇數長度的迴文子串,接下來我們來改進一下

第二版改進

因爲我們只能求出奇數長度的迴文子串,因此我們需要把字符串改進一下

首先通過在每個字符的兩邊都插入一個特殊的符號,將所有可能的奇數或偶數長度的迴文子串都轉換成了奇數長度。比如 abba 變成 #a#b#b#a#, aba變成 #a#b#a#,這樣我們再來看一下代碼:

function palindrome($str)
{   
    $pos = 0;
    $max = 0;

    $newStr = "#" . implode(str_split($str), "#") . "#";
    $n = strlen($newStr);

    for ($i = 0; $i < $n; $i++) { 
        for ($j = 0; ($i - $j >= 0) && ($i + $j < $n); $j++) { 
            if ($newStr[$i - $j] != $newStr[$i + $j]) {
                break;
            }
            if ($j > $max) {
                $max = $j;
                $pos = $i;
            }
        }
    }

    $r = substr($newStr, $pos - $max, $max * 2);
    $res = str_replace("#", "", $r);
    var_dump($res);
}

這個樣子我們就寫好了基本的迴文子串算法了,但是在這個算法中,我們用了兩層 for 循環,並且需要判斷當前字符的位置是否越界,效率較低,接下來我們來看 Manacher 算法

Manacher 算法實現

首先,爲了進一步減少編碼的複雜度,可以在字符串的開始和結尾加入另一個特殊字符,這樣就不用特殊處理越界問題,這裏我們在開頭和結尾分別加入 @\0,如 abba 變成 @#a#b#b#a#\0

接下來,我們引入一個輔助序列 $p[] 來記錄各個位置的迴文長度(注意:我們這裏記錄的迴文長度是單向的長度,比如 12321 我們記錄的迴文長度爲 3,實際迴文長度是 $p[$i] * 2 - 1
比如字符串 $s[] 與輔助序列 $p[] 的對應關係如下:

  • S # 1 # 2 # 2 # 1 #
  • P 1 2 1 2 5 2 1 2 1

最後,核心代碼在於這一句:
$p[$i] = $mx > $i ? min($p[$j], $mx - $i) : 1;
通過這步操作我們可以避免很多不必要的匹配,我們結合下面的代碼來理解這句操作
$mx 是最大回文序列最右側邊界的座標,$i 是當前要計算的位置,$j$i 相對於最大回文序列中間座標 $pos 的對稱點
由於迴文序列的性質,迴文序列是對稱的,也就是說
如果:
$mx > $i
那麼
$p[$i] >= $mx > $i ? min($p[$j], $mx - $i) : 1;
可以這麼理解:
如果 $mx > $i,這時 當前位置當前最大回文序列 的右半部分裏面,根據迴文序列的對稱性,可以得出,當前位置 $i 的迴文長度一定大於等於與之對稱 $j 的迴文長度,所以說直接從 $j 的迴文長度開始計算
但是如果 $mx > $i,這時 當前位置當前最大回文序列 之外,無法判斷 $mx 以後字符的對稱性,因此從 1 開始

function palindrome($str)
{   
    // 最大回文序列中間座標
    $pos = 0;
    // 最大回文長度
    $max = 0;
    // 迴文序列最右邊界座標
    $mx = 0;

    $p = array("0" => 1, "1" => 1);

    $newStr = "@#" . implode(str_split($str), "#") . "#\0";
    $n = strlen($newStr);

    for ($i = 2; $newStr[$i] != "\0"; $i++) {
        // $i 相對於最大回文序列中間座標 $pos 的對稱點
        $j = $pos - $i > 0 ? $pos - $i : 1;
        $p[$i] = $mx > $i ? min($p[$j], $mx - $i) : 1; 
        while ($newStr[$i - $p[$i]] == $newStr[$i + $p[$i]]) {
            $p[$i]++;
        }

        if ($p[$i] > $max) {
            $max = $p[$i];
            $pos = $i;
            $mx = $i + $max;
        }
    }

    $r = substr($newStr, $pos - $max + 1, $max * 2 - 1);
    $res = str_replace(array("#", "@", "\0"), "", $r);
    var_dump($res);
}

Manacher 算法的時間複雜度爲O(n),優勢在於避免了奇偶數討論的問題,簡化了邊界判斷,還記錄了當前字符串的“迴文狀態”,利用之前的迴文狀態來求當前迴文狀態 ,體現了動態規劃的思想

發佈了29 篇原創文章 · 獲贊 5 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章