MANACHER最長迴文算法

博客已經搬家,請前往http://gqqnbig.me/ 閱讀格式良好的文章。

本文將一步一步構造Manacher算法,心急的一定看不懂!請先練習下面的習題。

探索最長迴文串性質

題1:已知字符串以center爲中心對稱,求完整的字符串。
abcd???
   |
center
abcdcba
   |
center

題2:接上題,abcdcba後面還有一些字符,以center2爲中心,最大對稱半徑[ref]半徑大於等於1。[/ref]爲7,求完整的字符串。1

答根據center2的對稱性質,可以知道字符串爲

abcdcba???abcdcb??

又根據center2最大對稱半徑爲7,而不是8,所以c後面一定不是b(標識爲b)。center3的對稱半徑確定!而且,center關於center2的對稱點center3的索引號爲center2*2-center=8*2-3=13。對稱半徑爲center2+7-center3=8+7-13=2。2

題3:稍微調整上題,若center2在a,對稱半徑爲8,求完整的字符串和center3的對稱半徑。3

答:字符串爲

babcdcbabcdcbab?

center3=center2*2-center=7*2-4=10。對稱半徑爲center2+8-center3=7+8-10=5。這樣對嗎?4


看起來半徑不正確!應該同center一樣都是4。問題出在哪裏呢?明明用的是跟題2一樣的公式啊!因爲題2跟題3情況不同。題2 center左邊超出了center2的對稱範圍;而題3 center左邊沒超出center2的對稱範圍。center3的半徑確定。題4:同樣,center在x,半徑爲2,center2在a,半徑爲5。求完整的字符串和center3半徑。5


答:根據center2的對稱性補全center2範圍內的字符。又因爲center2的對稱半徑爲5而不是6,索引11位的字符不是c。

ccdxdbab7dxdc11?12????
所以center3的半徑就是2了嗎?不一定哦!因爲我們只知道索引11位的字符不是c,但它可能是b,這樣就與索引7位的b匹配,對稱半徑達到3。同樣地,我們也不知道索引12位的字符是什麼。所以,在center和center2的左邊界重合時,我們只知道center3半徑的最小值。根據這三道例題,我們已經總結出迴文字符的半徑性質:
  1. 當center1範圍左超center2範圍(簡稱左超),center3的半徑可由計算得出,爲確切值。
  2. 當center1範圍內含於center2範圍(簡稱內含),center3的半徑等於其關於center2的對稱點半徑,爲確切值。
  3. 當center1左邊界與center2左邊界重合(簡稱接邊),center2半徑大於等於其關於center2的對稱點半徑,值不確定。
在情況1、2中,求的是確切值,所以即使center3內含於多個對稱範圍,每個對稱範圍求出的半徑都一定是一樣的。在情況3中,所有對稱範圍的計算值的最大值是center3半徑的最小值。

基本代碼

下面給出基本代碼。[code lang="java"]public static int getPalindromeLength(String s){if (s.length() == 0)return 0;StringBuilder sb = new StringBuilder(s.length() * 2 + 1);sb.setLength(sb.capacity());for (int i = 0; i < s.length(); i++)sb.setCharAt(i * 2 + 1, s.charAt(i));s = sb.toString();int[] radii = new int[s.length()];for (int center = 0; center < s.length() - 1; center++) // O(n){boolean notSure = true;// 檢查center是否在某個對稱區域的右半邊for (int center2 = center - 1; center2 >= center / 2; center2--){if (center2 + radii[center2] > center) // center在以center2爲中心的對稱區域的右半邊{int c1 = center2 * 2 - center;assert c1 >= 0;if (c1 - radii[c1] < center2 - radii[center2])// 左超,半徑確定{radii[center] = center2 + radii[center2] - center;notSure = false;break;}else if (c1 - radii[c1] > center2 - radii[center2]) // 內含,半徑確定{radii[center] = radii[c1];notSure = false;break;}else// 接邊,半徑可能變化radii[center] = Math.max(radii[c1], radii[center]);}}if (notSure){// ccxbabxbabxcc// 0123456789012/* * 索引6的x關於索引4的a對稱,根據索引2的x得對稱半徑1。 但實際上索引6的x是整個字符串的對稱中心。 */int r = radii[center];while (center - r >= 0 && center + r < s.length() && s.charAt(center - r) == readChar(s, center + r))r++;radii[center] = r;}}int maxRadius = radii[0];for (int i = 0; i < radii.length; i++)maxRadius = Math.max(maxRadius, radii[i]);return (maxRadius - 1);}private static char readChar(String s, int index)//這個方法可用來查看計算複雜度{System.out.println("讀取字符" + index);return s.charAt(index);}[/code]

現在分析複雜度,令比較運算爲耗時間的操作。該代碼通過notSure變量,僅當i關於center2的對稱點接邊的時候纔讀取字符來比較。前面性質分析中說的是左側接邊,根據對稱性也是右側接邊。進入if (notSure)分支後繼續讀取更右邊的字符,然後記錄在radii裏。以後,從radii裏讀就可以了。建議親手運行代碼看看,會發現程序不會重讀前面讀過的字符,不會讀了012345,然後又讀2345。既然程序只讀取字符2n次,那麼比較進行了n次,所以時間複雜度是O(n)。網上很多代碼與上面的代碼不太一樣,有id、mx什麼的,尤其有一個變量記錄什麼最右邊的位置,不太好懂。讀者如果理解了上面的基本代碼,那麼可以再看下下面的變形代碼,變形代碼與網上的代碼比較像。變形代碼的複雜度同樣是O(n)。[code lang="java" title= language=]public static int getPalindromeLength2(String s){if (s.length() == 0)return 0;StringBuilder sb = new StringBuilder(s.length() * 2 + 1);sb.setLength(sb.capacity());for (int i = 0; i < s.length(); i++)sb.setCharAt(i * 2 + 1, s.charAt(i));s = sb.toString();int[] radii = new int[s.length()];int rCenter2 = 0;// 右邊界最大的cfor (int i = 0; i < s.length() - 1; i++) // O(n){// 檢查center是否在某個對稱區域的右半邊if (rCenter2 + radii[rCenter2] > i) // i在以center2爲中心的對稱區域的右半邊{int c1 = rCenter2 * 2 - i;assert c1 >= 0;if (c1 - radii[c1] < rCenter2 - radii[rCenter2])// 左超,半徑確定radii[i] = rCenter2 + radii[rCenter2] - i;else// 不左超。但如果接邊,半徑可能變化radii[i] = radii[c1];}// 如果右最大的c都不包括i,那麼其他c更不會包括了。if (i + radii[i] == rCenter2 + radii[rCenter2])// 接邊{int r = radii[i];while (i - r >= 0 && i + r < s.length() && s.charAt(i - r) == readChar(s, i + r))r++;radii[i] = r;// 接邊後超右邊比較,半徑可能更大!if (i + radii[i] > rCenter2 + radii[rCenter2])rCenter2 = i;}}int maxRadius = radii[0];for (int i = 0; i < radii.length; i++)maxRadius = Math.max(maxRadius, radii[i]);return (maxRadius - 1);}[/code]

參考資料

[CiteWeb author="xiangzhai" url="https://github.com/xiangzhai/leetcode/blob/master/question/longest-palindromic-substring-part-ii.md" title="最長迴文子字符串 第二部" publisher="" date="2014-02-14" accessdate="2015-02-06"]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章