**Manacher算法是一個用來查找一個字符串中的最長迴文子串(不是最長迴文序列)的線性算法。它的優勢就是把時間複雜度爲O(n*n)的暴力算法優化到了O(n)。首先先讓咱們來看看最原始的暴力擴展,分析其存在的弊端,以此來更好的理解Manacher算法。**java
暴力匹配算法的原理很簡單,就是從原字符串的首部開始,依次向尾部進行遍歷,每訪問一個字符,就以此字符爲中心向兩邊擴展,記錄該點的最長迴文長度。那麼咱們能夠想一想,這樣作存在什麼弊端,是否是能夠求出真正的最長迴文子串?ios
答案是顯然不行的,咱們從兩個角度來分析這個算法算法
1.不適用於偶數迴文串數組
咱們舉兩個字符串作例子,它們分別是 "aba","abba",咱們經過肉眼能夠觀察出,它們對應的最長迴文子串長度分別是3和4,然而咱們要是用暴力匹配的方法去對這兩個字符串進行操做就會發現,"aba" 對應的最長迴文長是 "131","abba" 對應的最長迴文長度是 "1111",咱們對奇數迴文串求出了正確答案,可是在偶數迴文串上並無獲得咱們想要的結果,經過屢次測試咱們發現,這種暴力匹配的方法不適用於偶數迴文串測試
2.時間複雜度O(n*n)優化
這裏的時間複雜度是一個平均時間複雜度,並不表明每個字符串都是這個複雜度,但由於每到一個新位置就須要向兩邊擴展比對,因此平均下來時間複雜度達到了O(n*n)。spa
咱們知道暴力匹配是沒法解決偶數迴文串的,可Manacher算法也是一種基於暴力匹配的算法,那它是怎麼來實現暴力匹配且又不出錯的呢?它用來應對偶數字符串的方法就是——作出預處理,這個預處理能夠巧妙的讓全部字符串都變爲奇數迴文串,不論它本來是什麼。操做實現也很簡單,就是將原字符串的首部和尾部以及每兩個字符之間插入一個特殊字符,這個字符是什麼不重要,不會影響最終的結果(具體緣由會在後面說),這一步預處理操做後的效果就是原字符串的長度從n改變成了2*n+1,也就獲得了咱們須要的能夠去作暴力擴展的字符串,而且從預處理後的字符串獲得的最長迴文字符串的長度除以2就是原字符串的最長迴文子串長度,也就是咱們想要獲得的結果。3d
這裏解釋一下爲何預處理後不會影響對字符串的擴展匹配code
好比咱們的原字符串是 "aa",假設預處理後的字符串是 "#a#a#",咱們在任意一個點,好比字符 '#',向兩端匹配只會出現 'a' 匹配 'a','#' 匹配 '#' 的狀況,不會出現原字符串字符與特殊字符匹配的狀況,這樣就能保證咱們不會改變原字符串的匹配規則。經過這個例子,你也能夠發現實際獲得的結果與上述符合。blog
Manacher算法的核心部分在於它巧妙的使人驚歎的加速,這個加速一下把時間複雜度提高到了線性,讓咱們從暴力的算法中解脫出來,咱們先引入概念,再說流程,最後提供實現代碼。
ManacherString:通過Manacher預處理的字符串,如下的概念都是基於ManasherString產生的。
迴文半徑和迴文直徑:由於處理後迴文字符串的長度必定是奇數,因此迴文半徑是包括迴文中心在內的迴文子串的一半的長度,迴文直徑則是迴文半徑的2倍減1。好比對於字符串 "aba",在字符 'b' 處的迴文半徑就是2,迴文直徑就是3。
最右迴文邊界R:在遍歷字符串時,每一個字符遍歷出的最長迴文子串都會有個右邊界,而R則是全部已知右邊界中最靠右的位置,也就是說R的值是隻增不減的。
迴文中心C:取得當前R的第一次更新時的迴文中心。因而可知R和C時伴生的。
半徑數組:這個數組記錄了原字符串中每個字符對應的最長迴文半徑。
步驟1:預處理原字符串
先對原字符串進行預處理,預處理後獲得一個新的字符串,這裏咱們稱爲S,爲了更直觀明瞭的讓你們理解Manacher的流程操做,咱們在下文的S中不顯示特殊字符(這樣並不影響結果)。
步驟2:R和C的初始值爲-1,建立半徑數組pArr
這裏有點與概念相差的小誤差,就是R實際是最右邊界位置的右一位。
步驟3:開始從下標 i = 0去遍歷字符串S
分支1:i > R ,也就是i在R外,此時沒有什麼花裏胡哨的方法,直接暴力匹配,此時記得看看C和R要不要更新。
分支2:i <= R,也就是i在R內,此時分三種狀況,在討論這三個狀況前,咱們先構建一個模型
L是當前R關於C的對稱點,i'是i關於C的對稱點,可知 i' = 2*C - i,而且咱們會發現,i'的迴文區域是咱們已經求過的,從這裏咱們就能夠開始判斷是否是能夠進行加速處理了
狀況1:i'的迴文區域在L-R的內部,此時i的迴文直徑與 i' 相同,咱們能夠直接獲得i的迴文半徑,下面給出證實
紅線部分是 i' 的迴文區域,由於整個L-R就是一個迴文串,迴文中心是C,因此i造成的迴文區域和i'造成的迴文區域是關於C對稱的。
狀況2:i'的迴文區域左邊界超過了L,此時i的迴文半徑則是i到R,下面給出證實
首先咱們設L點關於i'對稱的點爲L',R點關於i點對稱的點爲R',L的前一個字符爲x,L’的後一個字符爲y,k和z同理,此時咱們知道L - L'是i'迴文區域內的一段迴文串,故可知R’ - R也是迴文串,由於L - R是一個大回文串。因此咱們獲得了一系列關係,x = y,y = k,x != z,因此 k != z。這樣就能夠驗證出i點的迴文半徑是i - R。
狀況3:i' 的迴文區域左邊界剛好和L重合,此時i的迴文半徑最少是i到R,迴文區域從R繼續向外部匹配,下面給出證實
由於 i' 的迴文左邊界和L重合,因此已知的i的迴文半徑就和i'的同樣了,咱們設i的迴文區域有邊界的下一個字符是y,i的迴文區域左邊界的上一個字符是x,如今咱們只須要從x和y的位置開始暴力匹配,看是否能把i的迴文區域擴大便可。
總結一下,Manacher算法的具體流程就是先匹配 -> 經過判斷i與R的關係進行不一樣的分支操做 -> 繼續遍歷直到遍歷完整個字符串
咱們能夠計算出時間複雜度爲什麼是線性的,分支一的狀況下時間時間複雜度是O(n),分支二的前兩種狀況都是O(1),分支二的第三種狀況,咱們可能會出現O(1)——沒法從R繼續向後匹配,也可能出現O(n)——能夠從R繼續匹配,即便能夠繼續匹配,R的值也會增大,這樣會影響到後續的遍歷匹配複雜度,因此綜合起來整個算法的時間複雜度就是線性的,也就是O(n)。
整個代碼並非對上述流程的生搬硬套(那樣會顯得代碼冗長),代碼進行了精簡優化,具體如何我會在代碼中進行註釋
#include<iostream> #include<string> #include<cstring> #include<algorithm> #include<vector> #include<cmath> using namespace std; //算法主體 int maxLcsplength(string str) { //空字符串直接返回0 if (str.length() == 0) { return 0; } //記錄下原始字符串的長度,方便後面使用 int len = (int)(str.length() * 2 + 1); //開闢動態數組chaArr記錄manacher化的字符串 //開闢動態數組pArr記錄每一個位置的迴文半徑 char *chaArr = new char[len]; int* pArr = new int[len]; int index = 0; for (int i = 0; i < len;i++) { chaArr[i] = (i & 1) == 0 ? '#' : str[index++]; } //到此完成對原字符串的manacher化 //R是最右迴文邊界,C是R對應的最左迴文中心,maxn是記錄的最大回文半徑 int R = -1; int C = -1; int maxn = 0; //開始從左到右遍歷 for (int i = 0; i < len; i++) { //第一步直接取得可能的最短的迴文半徑,當i>R時,最短的迴文半徑是1,反之,最短的迴文半徑多是i對應的i'的迴文半徑或者i到R的距離 pArr[i] = R > i ? min(R - i, pArr[2 * C - i]) : 1; //取最小值後開始從邊界暴力匹配,匹配失敗就直接退出 while (i + pArr[i]<len && i + pArr[i]>-1) { if (chaArr[i + pArr[i]] == chaArr[i - pArr[i]]) { pArr[i]++; } else { break; } } //觀察此時R和C是否可以更新 if (i + pArr[i] > R) { R = i + pArr[i]; C = i; } //更新最大回文半徑的值 maxn = max(maxn, pArr[i]); } //記得清空動態數組哦 delete[] chaArr; delete[] pArr; //這裏解釋一下爲何返回值是maxn-1,由於manacherstring的長度和原字符串不一樣,因此這裏獲得的最大回文半徑實際上是原字符串的最大回文子串長度加1,有興趣的能夠本身驗證試試 return maxn - 1; } int main() { string s1 = ""; cout << maxLcsplength(s1) << endl; string s2 = "abbbca"; cout << maxLcsplength(s2) << endl; return 0; }
下面附上java代碼
public class Manacher { public static char[] manacherString(String str) { char[] charArr = str.toCharArray(); char[] res = new char[str.length() * 2 + 1]; int index = 0; for (int i = 0; i != res.length; i++) { res[i] = (i & 1) == 0 ? '#' : charArr[index++]; } return res; } public static int maxLcpsLength(String str) { if (str == null || str.length() == 0) { return 0; } char[] charArr = manacherString(str); int[] pArr = new int[charArr.length]; int C = -1; int R = -1; int max = Integer.MIN_VALUE; for (int i = 0; i != charArr.length; i++) { pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1; while (i + pArr[i] < charArr.length && i - pArr[i] > -1) { if (charArr[i + pArr[i]] == charArr[i - pArr[i]]) pArr[i]++; else { break; } } if (i + pArr[i] > R) { R = i + pArr[i]; C = i; } max = Math.max(max, pArr[i]); } return max - 1; } public static void main(String[] args) { String str1 = "abc123321cba"; System.out.println(maxLcpsLength(str1)); } }