Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.
這道題讓我們求最長迴文子串,首先說下什麼是迴文串,就是正讀反讀都一樣的字串,比如 "bob", "level", "noon" 等等。那麼最長迴文子串就是在一個字串中的那個最長的迴文子串。LeetCode中關於迴文串的題共有五道,除了這道,其他的四道為 Palindrome Number, Validate Palindrome, Palindrome Partitioning,Palindrome Partitioning II,我們知道傳統的驗證迴文串的方法就是兩個兩個的對稱驗證是否相等,那麼對於找回文字串的問題,就要以每一個字元為中心,像兩邊擴散來尋找回文串,這個演算法的時間複雜度是O(n*n),可以通過OJ,就是要注意奇偶情況,由於迴文串的長度可奇可偶,比如"bob"是奇數形式的迴文,"noon"就是偶數形式的迴文,兩種形式的迴文都要搜尋,對於奇數形式的,我們就從遍歷到的位置為中心,向兩邊進行擴散,對於偶數情況,我們就把當前位置和下一個位置當作偶數行迴文的最中間兩個字元,然後向兩邊進行搜尋,參見程式碼如下:
解法一:
class Solution { public: string longestPalindrome(string s) { if (s.size() < 2) return s; int n = s.size(), maxLen = 0, start = 0; for (int i = 0; i < n - 1; ++i) { searchPalindrome(s, i, i, start, maxLen); searchPalindrome(s, i, i + 1, start, maxLen); } return s.substr(start, maxLen); } void searchPalindrome(string s, int left, int right, int& start, int& maxLen) { while (left >= 0 && right < s.size() && s[left] == s[right]) { --left; ++right; } if (maxLen < right - left - 1) { start = left + 1; maxLen = right - left - 1; } } };
我們也可以不使用子函式,直接在一個函式中搞定,我們還是要定義兩個變數start和maxLen,分別表示最長迴文子串的起點跟長度,在遍歷s中的字元的時候,我們首先判斷剩餘的字元數是否小於等於maxLen的一半,是的話說明maxLen無法再變長了,直接break掉。否則就要繼續判斷,我們用兩個變數left和right分別指向當前位置,然後我們先要做的是向右遍歷跳過重複項,這個操作很必要,比如對於 noon,i在第一個o的位置,如果我們以o為最中心往兩邊擴散,是無法得到長度為4的迴文串的,只有先跳過重複,此時left指向第一個o,right指向第二個o,然後再向兩邊擴散。而對於 bob,i在第一個o的位置時,無法向右跳過重複,此時left和right同時指向o,再向兩邊擴散也是正確的,所以可以同時處理奇數和偶數的迴文串,之後的操作就是更新maxLen和start了,跟上面的操作一樣,參見程式碼如下:
解法二:
class Solution { public: string longestPalindrome(string s) { if (s.size() < 2) return s; int n = s.size(), maxLen = 0, start = 0; for (int i = 0; i < n;) { if (n - i <= maxLen / 2) break; int left = i, right = i; while (right < n - 1 && s[right + 1] == s[right]) ++right; i = right + 1; while (right < n - 1 && left > 0 && s[right + 1] == s[left - 1]) { ++right; --left; } if (maxLen < right - left + 1) { maxLen = right - left + 1; start = left; } } return s.substr(start, maxLen); } };
此題還可以用動態規劃Dynamic Programming來解,根Palindrome Partitioning II 拆分迴文串之二的解法很類似,我們維護一個二維陣列dp,其中dp[i][j]表示字串區間[i, j]是否為迴文串,當i = j時,只有一個字元,肯定是迴文串,如果i = j + 1,說明是相鄰字元,此時需要判斷s[i]是否等於s[j],如果i和j不相鄰,即i - j >= 2時,除了判斷s[i]和s[j]相等之外,dp[j + 1][i - 1]若為真,就是迴文串,通過以上分析,可以寫出遞推式如下:
dp[i, j] = 1 if i == j
= s[i] == s[j] if j = i + 1
= s[i] == s[j] && dp[i + 1][j - 1] if j > i + 1
這裡有個有趣的現象就是如果我把下面的程式碼中的二維陣列由int改為vector<vector<int> >後,就會超時,這說明int型的二維陣列訪問執行速度完爆std的vector啊,所以以後儘可能的還是用最原始的資料型別吧。
解法三:
class Solution { public: string longestPalindrome(string s) { if (s.empty()) return ""; int dp[s.size()][s.size()] = {0}, left = 0, right = 0, len = 0; for (int i = 0; i < s.size(); ++i) { for (int j = 0; j < i; ++j) { dp[j][i] = (s[i] == s[j] && (i - j < 2 || dp[j + 1][i - 1])); if (dp[j][i] && len < i - j + 1) { len = i - j + 1; left = j; right = i; } } dp[i][i] = 1; } return s.substr(left, right - left + 1); } };
最後要來的就是大名鼎鼎的馬拉車演算法Manacher's Algorithm,這個演算法的神奇之處在於將時間複雜度提升到了O(n)這種逆天的地步,而演算法本身也設計的很巧妙,很值得我們掌握,參見我另一篇專門介紹馬拉車演算法的部落格Manacher's Algorithm 馬拉車演算法,程式碼實現如下:
解法四:
class Solution { public: string longestPalindrome(string s) { string t ="$#"; for (int i = 0; i < s.size(); ++i) { t += s[i]; t += '#'; } int p[t.size()] = {0}, id = 0, mx = 0, resId = 0, resMx = 0; for (int i = 0; i < t.size(); ++i) { p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1; while (t[i + p[i]] == t[i - p[i]]) ++p[i]; if (mx < i + p[i]) { mx = i + p[i]; id = i; } if (resMx < p[i]) { resMx = p[i]; resId = i; } } return s.substr((resId - resMx) / 2, resMx - 1); } };
類似題目:
Longest Palindromic Subsequence
參考資料:
https://leetcode.com/problems/longest-palindromic-substring/