求最長迴文子串——Manacher算法詳解

迴文子串問題

迴文子串問題通常會給出一個字符串,然後找出這個字符串中最長的迴文子串。
迴文串即爲正讀和反讀一致的字符串,比如”aa",“abba”,"abcba"等。判別一個字符串是否爲迴文串很容易想到的方法是:

設立兩個遊標,分別在串的最左和最右,讓這兩個遊標向對方逼近的同時比較這兩個遊標的值

但是這裏我們要討論的是迴文子串,所以上述方法暴露出一個嚴重的缺陷:難以確定右遊標(因爲左遊標右邊的所有字符都可以是右遊標)。所以這裏我們需要轉換思維,不從兩邊逼近,而是從中間向兩邊擴散,這時候就要區分串的個數是奇數還是偶數了。

規律1:
奇數串,如aabaa,中間的字符可以是任意的,以這個字符爲軸,軸兩邊的字符是一一對應相等的。
偶數串,如baab,可以假設中間有一條軸,兩邊的字符必須一一對應。

準備過程

令n爲字符串的長度,最容易想到的方法就是枚舉每一個可能的字符串,一共有n^2 數量級的子串,判別每一個子串是否是迴文串需要n數量級的時間,所以時間複雜度爲O(n^3)
我們當然不推薦這樣的方法,但是可以作爲引導,來彌補這種算法的缺陷。爲了便於理解,我們引入“對稱軸”的概念,如下圖:
圖中被綠圈圈住的在這裏稱之爲軸
以軸爲界,軸兩邊的字符是關於軸鏡像對稱的,奇數串(字符個數爲奇數)的軸爲中間的字符,偶數串的軸爲中間的兩個字符。

規律2:
字符串的每一個字符(偶數串爲兩個連續字符)都可能是最長迴文子串的軸

由上面的這個規律可以知道,我們只需要遍歷一遍字符串,將每一個字符(偶數串爲兩個連續字符)作爲軸,尋找最長的迴文串,最後這些迴文串中最長的即爲整個字符串中的最長迴文子串。
遍歷一遍是n數量級的時間,尋找回文子串也是n數量級,所以時間複雜度是O(n^2)
但是這樣問題又來了,偶數串和奇數串的處理方法不同,需要分別處理。這時候我們就要用一個小技巧,用‘#’插入字符串中字符的間隔中,這樣就將所有字符串都轉換成奇數串,如下:
將所有字符串都轉換爲奇數串
這樣我們就可以用一套方法來處理所有的字符串了,而且時間複雜度爲O(n^2)。但是我們還要追求更好的算法,這時候就需要更高一級的技術了,接下來就需要介紹一個時間複雜度爲n數量級的算法。

Manacher算法

在接下來所要涉及的均爲奇數串,首先來介紹以下幾個概念:

:因爲接來下的所有子串都爲奇數串,所以軸就確定爲一個迴文串最中間的字符
半徑:一個迴文串的軸 左/右邊 字符的個數(包括軸),比如aabaa的半徑爲3,aca的半徑爲2,c的半徑爲1。在算法中用 p[i] 表示,i爲軸的位置。

再來介紹幾個特殊點:
j是i關於id的對稱點,nx是mx關於id的對稱點
其中,mx是以id爲軸的最長迴文串的邊界,j是i關於id的對稱點,nx是mx關於id的對稱點
如果i比mx大的話,那麼這個問題就變成了剛纔提到的O(n^2)時間複雜度的查找問題,但是假如i比mx小,那麼Manacher算法就可以省去很多不必要的檢驗。
根據迴文串的性質很容易得到以下結論:

規律3
如果nx ~ id區間內存在迴文子串,那麼id ~ mx同樣的位置也會有同樣的迴文子串

規律4
p[i] - 1是原字符串中最長迴文串的長度

同時爲了避免進行越界判別,可以在字符串首位加上兩個不會在字符串中出現的不同的字符,比如‘$’和‘\0’,至於爲什麼選擇‘\0’,因爲‘\0’本身就是用作截斷字符串。

我們遍歷的是i,遍歷i的時候會發生以下3種情況:

  • 以j爲軸的迴文串在id爲軸的迴文串內部,簡稱內部
  • 以j爲軸的迴文串在id爲軸的迴文串外部,簡稱外部
  • 以j爲軸的迴文串邊界與以id爲軸的迴文串邊界重合,簡稱重合

對這三種情況進行分類討論。

內部的情況

以j爲軸的迴文串在id爲軸的迴文串內部
得出的結論是:p[i]=p[j](不清楚p[i]定義的可以翻到上面重新看半徑的定義)。
數學的證明方法這裏寫起來比較囉嗦,本來這篇文章也不是爲了講解證明過程,以後如果有人有興趣的話再說,這裏簡要說明一下證明思路:
反證法
假如以i爲軸迴文串長度加上了橙色的一截,超過了以j爲軸的字符串,甚至有可能超過了大回文串時,我們截取這個迴文串在大回文串中的一部分,將這段長度對應到軸j上,由規律3可以得知,j軸迴文串加上了這一部分長度的新串依然是一個迴文串,但是這樣的話就與已知條件矛盾(以j爲軸迴文串的半徑爲p[j])。反之,如果p[i]<p[j]的話更不可能了(見規律3)

外部的情況

假如j軸迴文串的一部分超過了大回文串,那麼i軸迴文串的半徑就爲mx-i,有p[i] = mx - i,如下圖:
外部的情況
簡要證明一下,首先i軸迴文串不可能更短,因爲圖中綠色部分一定是一個迴文串(由藍色部分是迴文串規律3這兩個條件可以得出)。然後i軸迴文串不可能更長,因爲我們已知的條件是id軸迴文串最長爲nx~mx,那麼說明超過這段的串一定不是迴文串,那麼假如i軸迴文串超過了大回文串,由規律3迴文串的性質很容易得到id軸迴文串會變得更長,這與已知條件相悖。

重合的情況

假如j軸迴文串正好和邊界重合,即p[j] = mx - i,那麼p[i] >= p[j],如下圖:
重合的情況
簡要證明一下,首先p[i]不可能更小(見規律3),然後需要證明p[i]可能變得更大。我們已知的是j軸迴文串正好和大回文串邊界重合,我們可以得出以下兩個結論

j軸迴文串兩側的字符不相等
大回文串兩側的字符不相等

數學上我們學過a ≠ b,b ≠ c,並不能推出a ≠ c,所以i軸迴文串兩側的字符是否相等還得進一步判斷。

總結

Manacher算法的核心思想就是儘可能找到一個軸id,讓mx > i,這樣就可以用以上的3種方法來進行快速判別,同時因爲整個算法只需要遍歷一遍字符串,大回文串是一直在外擴的,並不會進行回退,所以整個算法的時間複雜度爲O(n)。

代碼實現

		char str[1050];
		str[0] = '$';
		str[1] = '#';

		int len = s.size();
		int sl = 2;	// sl爲處理後字符串的長度

		for (int i = 1; i < len; i++)
		{
			str[sl++] = s[i];
			str[sl++] = '#';
		}
		str[sl] = '\0';
		
		int mx = 0, id = 0;
		int p[1050];
		memset(p, 0, sizeof(p));
		int ml = 1;

		for (int i = 1; i < sl; i++)
		{
			if (i < mx)
				p[i] = min(p[2 * id - 1], mx - i);	// 2*id-1即爲j的位置
			else	
				p[i] = 1;

			while (str[i - p[i]] == str[i + p[i]]) {
				p[i]++;
			}

			if (mx < i + p[i]) {	// 儘可能找到一個大的id和mx
				id = i;
				mx = i + p[i];
			}

			ml = max(ml, p[i] - 1);	// 更新最大值
		}
		return ml;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章