題目:給一個字符串,找出它的最長的迴文子序列的長度。
例如,如果給定的序列是“BBABCBCAB”,則輸出應該是7,“BABCBAB”是在它的最長迴文子序列。
輸入:
aaaa
1212asdfdsa1144121
輸出:
4
7
這裏我們還是將其封裝成函數調用
何謂迴文序列
迴文序列就是正向和反向完全一樣的序列,比如 asdfdsa
和 aaaa
接下來我們由淺及深,一步一步來說一下 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),優勢在於避免了奇偶數討論的問題,簡化了邊界判斷,還記錄了當前字符串的“迴文狀態”,利用之前的迴文狀態來求當前迴文狀態 ,體現了動態規劃的思想