給定一個字符串 s,找到 s 中最長的迴文子串。你能夠假設 s 的最大長度爲 1000。算法
示例 1:數組
輸入: "babad"app
輸出: "bab"ide
注意: "aba" 也是一個有效答案。post
示例 2:ui
輸入: "cbbd"編碼
輸出: "bb"spa
解法 1:中心擴散
思路很簡單:遍歷每個索引,以這個索引爲中心,往兩邊擴散,看最多能擴散多遠。具體的作法是利用「迴文串」中心對稱的特色,在枚舉子串的過程當中進行剪枝。要注意一個細節:迴文串的長度多是奇數,也多是偶數。設計
咱們徹底能夠設計一個方法,兼容以上兩種狀況:3d
一、若是傳入重合的索引編碼,進行中心擴散,此時獲得的最長迴文子串的長度是奇數;
二、若是傳入相鄰的索引編碼,進行中心擴散,此時獲得的最長迴文子串的長度是偶數。
Python: class Solution: def longestPalindrome(self, s): size = len(s) if size == 0: return '' # 至少就是 1 longest_palindrome = 1 longest_palindrome_str = s[0] for i in range(size): # 返回當前最長迴文子串、和這個最長迴文子串的長度 palindrome_odd, odd_len = self.__center_spread(s, size, i, i) palindrome_even, even_len = self.__center_spread(s, size, i, i + 1) # 當前找到的最長迴文子串 cur_max_sub = palindrome_odd if odd_len >= even_len else palindrome_even if len(cur_max_sub) > longest_palindrome: longest_palindrome = len(cur_max_sub) longest_palindrome_str = cur_max_sub return longest_palindrome_str def __center_spread(self, s, size, left, right): """ 當 left = right 的時候,表示迴文中心是一條線,此時迴文串的長度是奇數 當 right = left + 1 的時候,表示迴文中心是任意一個字符,此時迴文串的長度是偶數 """ l = left r = right while l >= 0 and r < size and s[l] == s[r]: l -= 1 r += 1 return s[l + 1:r], r - l - 1
public class Solution { public String longestPalindrome(String s) { int len = s.length(); if (len == 0) { return ""; } int longestPalindrome = 1; String longestPalindromeStr = s.substring(0, 1); for (int i = 0; i < len; i++) { String palindromeOdd = centerSpread(s, len, i, i); String palindromeEven = centerSpread(s, len, i, i + 1); String maxLenStr = palindromeOdd.length() > palindromeEven.length() ? palindromeOdd : palindromeEven; if (maxLenStr.length() > longestPalindrome) { longestPalindrome = maxLenStr.length(); longestPalindromeStr = maxLenStr; } } return longestPalindromeStr; } private String centerSpread(String s, int len, int left, int right) { int l = left; int r = right; while (l >= 0 && r < len && s.charAt(l) == s.charAt(r)) { l--; r++; } // 這裏要特別當心,跳出 while 循環的時候,是第 1 個知足 s.charAt(l) != s.charAt(r) 的時候 // 因此,不能取 l,不能取 r return s.substring(l + 1, r); } }
string longestPalindrome(string s) { if (s.length() < 1) { return ""; } int start = 0, end = 0; for (int i = 0; i < s.length(); i++) { int len1 = expandAroundCenter(s, i, i);//一個元素爲中心 int len2 = expandAroundCenter(s, i, i + 1);//兩個元素爲中心 int len = max(len1, len2); if (len > end - start) { start = i - (len - 1) / 2; end = i + len / 2; } } return s.substr(start, end - start + 1); } int expandAroundCenter(string s, int left, int right) { int L = left, R = right; while (L >= 0 && R < s.length() && s[L] == s[R]) {// 計算以left和right爲中心的迴文串長度 L--; R++; } return R - L - 1; }
解法 2:動態規劃
解決這類 「最優子結構」 問題,考慮使用 「動態規劃」。咱們只要找準 「狀態」 的定義和 「狀態轉移方程」 就能夠了。
在下面的說明中,s[i, j] 表示原始字符串的一個子串,i、j 分別是索引,使用左閉、右閉區間表示左右端點能夠取到。
一、定義狀態,這裏動態規劃的數組是二維的。
dp[i][j] 表示子串 s[i, j](包括區間左右端點)是否構成迴文串,是一個二維布爾型數組。即若是子串 s[i, j] 是迴文串,那麼 dp[i][j] = true。
二、狀態轉移。
若是 s[i, j] 是一個迴文串,例如 「abccba」,那麼 s[i + 1, j - 1] 也必定是一個迴文串,即若是 dp[i][j] == true 成立,必定有 dp[i + 1][j - 1] = true。
反過來,若是已知 dp[i + 1, j - 1],就能夠經過比較 s[i] 和 s[j] 而且考慮 dp[i + 1, j - 1] 進而獲得 dp[i, j]。
整理一下:dp[i, j] = (s[i] == s[j] and dp[i + 1, j - 1]),不過,此時咱們要保證 [i + 1, j - 1] 可以造成區間,所以有 i + 1 <= j - 1,整理得 i - j <= -2,或者 j - i >= 2。
具體編碼細節在代碼的註釋中已經體現。
Python: class Solution(object): def longestPalindrome(self, s): size = len(s) if size <= 1: return s # 二維 dp 問題 # 狀態:dp[i,j]: s[i:j] 包括 i,j ,表示的字符串是否是迴文串 dp = [[False for _ in range(size)] for _ in range(size)] longest_l = 1 res = s[0] for i in range(1, size): for j in range(i): # 狀態轉移方程:若是頭尾字符相等而且中間也是迴文 # 或者中間的長度小於等於 1 if s[j] == s[i] and (j >= i - 2 or dp[j + 1][i - 1]): dp[j][i] = True if i - j + 1 > longest_l: longest_l = i - j + 1 res = s[j:i + 1] return res Java: public class Solution { public String longestPalindrome(String s) { int len = s.length(); if (len == 0) { return ""; } boolean[][] dp = new boolean[len][len]; int longestPalindrome = 1; String longestPalindromeStr = s.substring(0, 1); // 00 // 01 判斷了 11,11 有值了 // 012 判斷了 22,進而判斷 12,02 要依據 12 // 因此,dp 的填寫是從後向前進行的,這一點必定要很是清楚 // abcdcbd // j i // 若是 d[j,i] 爲真,那麼 dp[j+1,i-1] 也必定爲真 // [j+1,i-1] 構成區間(不能縮成一個點,縮成一個點的狀況,以前判斷過)的條件是 j + 1 < i - 1 , // 即 i > j + 2 for (int i = 1; i < len; i++) { for (int j = i; j >= 0; j--) { // 注意 i <= j + 2 || dp[j + 1][i - 1] 這種寫法的技巧 if (s.charAt(i) == s.charAt(j) && (i <= j + 2 || dp[j + 1][i - 1])) { // 先後很重要,不要不過腦子 dp[j][i] = true; if (i - j + 1 > longestPalindrome) { longestPalindrome = i - j + 1; longestPalindromeStr = s.substring(j, i + 1); } } } } return longestPalindromeStr; } }
解法 3:Manacher 算法
維基百科中對於 Manacher 算法是這樣描述的:
[Manacher(1975)] 發現了一種線性時間算法,能夠在列出給定字符串中從字符串頭部開始的全部迴文。而且,Apostolico, Breslauer & Galil (1995) 發現,一樣的算法也能夠在任意位置查找所有最大回文子串,而且時間複雜度是線性的。所以,他們提供了一種時間複雜度爲線性的最長迴文子串解法。替代性的線性時間解決 Jeuring (1994), Gusfield (1997)提供的,基於後綴樹(suffix trees)。也存在已知的高效並行算法。
在尚未實現算法以前,咱們先要弄清楚算法的運行流程,即給咱們一個具體的字符串,咱們經過稿紙演算的方式,應該如何獲得給定字符串的最長子迴文串。
理解 Manacher 算法最好的辦法,實際上是根據一些關於 Manacher 算法的文章,本身寫寫畫畫,最好能產生一些輸出,畫畫圖,舉一些具體的例子,這樣 Manacher 算法就不難搞懂了。
Manacher 算法本質上仍是中心擴散法,只不過它使用了相似 KMP 算法的技巧,充分挖掘了已經進行迴文斷定的子串的特色,使得算法高效。
迴文串可分爲奇數迴文串和偶數迴文串,它們的區別是:奇數迴文串關於它的「中點」知足「中心對稱」,偶數迴文串關於它「中間的兩個點」知足「中心對稱」。咱們在具體斷定一個字符串是不是迴文串的時候,經常會不自覺地考慮到它們之間的這個小的差異。
第 1 步:預處理,添加分隔符
咱們先給出具體的例子,看看如何添加分隔符。
例1:給字符串 "bob" 添加分隔符 "#"。
答:"bob" 添加分隔符 "#" 之後獲得:"#b#o#b#"。
再看一個例子:
例2:給 "noon" 添加分隔符 "#"。
答:"noon" 添加分隔符 "#" 之後獲得:"#n#o#o#n#"。
我想你已經看出來分隔符是如何添加的,下面是 2 點說明。
一、分隔符是字符串中沒有出現過的字符,這個分隔符的種類只有一個,即你不能同時添加 "#" 和 "?" 做爲分隔符;
二、在字符串的首位置、尾位置和每一個字符的「中間」都添加 11 個這個分隔符,能夠很容易知道,若是這個字符串的長度是 len,那麼添加的分隔符的個數就是 len + 1,獲得的新的字符串的長度就是 2len + 1,顯然它必定是奇數。
爲何要添加分隔符?
一、首先是正確性:添加了分隔符之後的字符串的迴文性質與原始字符串是同樣的。
二、實際上是避免奇偶數討論,對於使用「中心擴散法」斷定迴文串的時候,長度爲奇數和偶數的斷定是不一樣的,添加分隔符能夠避免對奇偶性的討論。
第 2 步:獲得 p 數組
首先,咱們先來看一下如何填表。以字符串 "abbabb" 爲例,說明如何手動計算獲得 p 數組。假設咱們要填的就是下面這張表。
第 1 行 char 數組:這個數組就是待檢測字符串加上分隔符之後的字符構成的數組。
第 2 行 index 數組:這個數組是索引數組,咱們後面要利用到它,填寫即索引從 0 開始寫就行了。
下面咱們來看看 p 數組應該如何填寫。首先咱們定義迴文半徑。
迴文半徑:以 char[i] 做爲迴文中心,同時向左邊、向右邊進行擴散,直到不能構成迴文串或者觸碰到邊界爲止,能擴散的步數 + 1 ,即定義爲 p 數組索引的值,也稱之爲迴文半徑。
以上面的例子,咱們首先填。p[0],以 char[0] = '#'爲中心,同時向左邊向右擴散,走 1 步就碰到邊界了,所以「能擴散的步數」爲0,「能擴散的步數 + 1 = 1」,所以 p[0] = 1;
下面填寫 p[1] ,以 char[1] = 'a' 爲中心,同時向左邊向右擴散,走 1 步,左右都是 "#",構成迴文子串,因而繼續同時向左邊向右邊擴散,左邊就碰到邊界了,所以「能擴散的步數」爲1,「能擴散的步數 + 1 = 2」,所以 p[1] = 2;
下面填寫 p[2] ,以 char[2] = '#' 爲中心,同時向左邊向右擴散,走 1 步,左邊是 "a",右邊是 "b",不匹配,所以「能擴散的步數」爲 00,「能擴散的步數 + 1 = 1」,所以 p[2] = 1;
下面填寫 p[3],以 char[3] = 'b' 爲中心,同時向左邊向右擴散,走 1 步,左右兩邊都是 「#」,構成迴文子串,繼續同時向左邊向右擴散,左邊是 "a",右邊是 "b",不匹配,所以「能擴散的步數」爲1,「能擴散的步數 + 1 = 2」,所以 p[3] = 2;
下面填寫 p[4],以 char[4]='#' 爲中心,同時向左邊向右擴散,能夠知道能夠同時走 4 步,左邊到達邊界,所以「能擴散的步數」爲4,「能擴散的步數 + 1 = 5」,所以 p[4] = 5。
分析到這裏,後面的數字不難填出,最後寫成以下表格:
char # a # b # b # a # b # b #
index 0 1 2 3 4 5 6 7 8 9 10 11 12
p 1 2 1 2 5 2 1 6 1 2 3 2 1
p-1
p-1 數組很簡單了,把 p 數組的數 -1 就好了。實際上直接把能走的步數記錄下來就行了。不過就是爲了給「迴文半徑」一個定義而已。
因而咱們獲得以下表格:
因而:數組 p -1 的最大值就是最長的迴文子串,能夠在獲得 p 數組的過程當中記錄這個最大值,而且記錄最長迴文子串。
如何編寫程序獲得 p 數組?
經過 p 數組咱們就能夠找到迴文串的最大值,就能肯定最長迴文子串了。那麼下面咱們就來看如何編碼求 p 數組,須要設置兩個輔助變量 mx 和 id ,它們的含義分別以下:
id :從開始到如今使用中心擴散法獲得的最長迴文子串的中心的位置;
mx:從開始到如今使用中心擴散法獲得的最長迴文子串能延伸到的最右端的位置。
數組 p 的值就與它們兩個有關,這個算法的最核心的一行以下:
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
能夠這麼說,這行要是理解了,那麼馬拉車算法基本上就沒啥問題了,那麼這一行代碼拆開來看就是:
若是 mx > i, 則 p[i] = min(p[2 * id - i], mx - i),不然, p[i] = 1。
這裏 2 * id - i 是 i 關於 id 的對稱索引。
咱們經過分類討論,就能夠獲得這個等式。一開始,咱們是不能偷懶的,老老實實使用中心擴散法來逐漸獲得 p 數組的值,同時記錄 id 和 mx。當咱們要考察的索引 i 超過了 mx 的時候,以下圖,咱們就能夠偷點懶了。根據迴文串的特色,i 關於 mx 的對稱點附近的狀況咱們已經計算出來了,所以,咱們能夠分以下兩種狀況討論。
討論從一箇中間的狀況開始:
一、首先當 i 位於 id 和 mx 之間時,此時 id 以前的 p 值都已經計算出來了,咱們利用已經計算出來的 p 值來計算當前考慮的位置的 p 值。
由於是迴文串,所以在 mx 的對稱點與 id 這個區間內,必定有一個點與 j 相等,這個點的索引就是 2 * id - i。
當 id < i < mx 的時候:
引入 j,若是 j 的迴文串很短,在 mx 關於 id 的對稱點以前結束。
此時 j = 2 * id - i,
if i < mx:
p[i] = min(p[2 * id - i], mx - i);
當 j 的範圍很小的時候,取 p[2 * id - i] ,此時 p[i] = p[j]。
二、當 mx - i > p[j] 的時候,以 s[j] 爲中心的迴文子串包含在以 s[id] 爲中心的迴文子串中,因爲 i 和 j 對稱,以 s[i] 爲中心的迴文子串必然包含在以 s[id] 爲中心的迴文子串中,因此必有 p[i] = p[j],見下圖。
三、當 p[j] >= mx - i 的時候,以 s[j] 爲中心的迴文子串不必定徹底包含於以 s[id] 爲中心的迴文子串中,可是基於對稱性可知,下圖中兩個綠框所包圍的部分是相同的,也就是說以 s[i] 爲中心的迴文子串,其向右至少會擴張到 mx 的位置,也就是說 p[i] >= mx - i。至於 mx 以後的部分是否對稱,就只能老老實實去匹配了。
四、對於 mx <= i 的狀況,沒法對 p[i] 作更多的假設,只能從 p[i] = 1 開始,而後再去匹配了。
/** * 使用 Manacher 算法 */ public class Solution3 { /** * 建立分隔符分割的字符串 * * @param s 原始字符串 * @param divide 分隔字符 * @return 使用分隔字符處理之後獲得的字符串 */ private String generateSDivided(String s, char divide) { int len = s.length(); if (len == 0) { return ""; } if (s.indexOf(divide) != -1) { throw new IllegalArgumentException("參數錯誤,您傳遞的分割字符,在輸入字符串中存在!"); } StringBuilder sBuilder = new StringBuilder(); sBuilder.append(divide); for (int i = 0; i < len; i++) { sBuilder.append(s.charAt(i)); sBuilder.append(divide); } return sBuilder.toString(); } public String longestPalindrome(String s) { int len = s.length(); if (len == 0) { return ""; } String sDivided = generateSDivided(s, '#'); int slen = sDivided.length(); int[] p = new int[slen]; int mx = 0; // id 是由 mx 決定的,因此不用初始化,只要聲明就能夠了 int id = 0; int longestPalindrome = 1; String longestPalindromeStr = s.substring(0, 1); for (int i = 0; i < slen; i++) { if (i < mx) { // 這一步是 Manacher 算法的關鍵所在,必定要結合圖形來理解 // 這一行代碼是關鍵,能夠把兩種分類討論的狀況合併 p[i] = Integer.min(p[2 * id - i], mx - i);
string longestPalindrome(string s) { int len = s.length(); if (len < 1) { return ""; } // 預處理 string s1; for (int i = 0; i < len; i++) { s1 += "#"; s1 += s[i]; } s1 += "#"; len = s1.length(); int MaxRight = 0; // 當前訪問到的全部迴文子串,所能觸及的最右一個字符的位置 int pos = 0; // MaxRight對應的迴文串的對稱軸所在的位置 int MaxRL = 0; // 最大回文串的迴文半徑 int MaxPos = 0; // MaxRL對應的迴文串的對稱軸所在的位置 int* RL = new int[len]; // RL[i]表示以第i個字符爲對稱軸的迴文串的迴文半徑 memset(RL, 0, len * sizeof(int)); for (int i = 0; i < len; i++) { if (i < MaxRight) {// 1) 當i在MaxRight的左邊 RL[i] = min(RL[2 * pos - i], MaxRight - i); } else {// 2) 當i在MaxRight的右邊 RL[i] = 1; } // 嘗試擴展RL[i],注意處理邊界 while (i - RL[i] >= 0 // 能夠把RL[i]理解爲左半徑,即迴文串的起始位不能小於0 && i + RL[i] < len // 同上,即迴文串的結束位不能大於總長 && s1[i - RL[i]] == s1[i + RL[i]]// 迴文串特性,左右擴展,判斷字符串是否相同 ) { RL[i] += 1; } // 更新MaxRight, pos if (RL[i] + i - 1 > MaxRight) { MaxRight = RL[i] + i - 1; pos = i; } // 更新MaxRL, MaxPos if (MaxRL <= RL[i]) { MaxRL = RL[i]; MaxPos = i; } } return s.substr((MaxPos - MaxRL + 1) / 2, MaxRL - 1); }