推薦理由:暴力解法太 naive,中心擴散不普適,Manacher 就更不普適了,是專門解這個問題的方法。而用動態規劃我認為是最有用的,可以幫助你舉一反三的方法。
補充說明:Manacher 算法有興趣的朋友們可以了解一下,有人就借助它的第一步字符串預處理思想,解決了 LeetCode 第 4 題。因此以上推薦僅代表個人觀點。
解決這類 “最優子結構” 問題,可以考慮使用 “動態規劃”:
1、定義 “狀態”;
2、找到 “狀態轉移方程”。
記號說明: 下文中,使用記號 s[l, r] 表示原始字符串的一個子串,l、r 分別是區間的左右邊界的索引值,使用左閉、右閉區間表示左右邊界可以取到。舉個例子,當 s = 'babad' 時,s[0, 1] = 'ba' ,s[2, 4] = 'bad'。
1、定義 “狀態”,這里 “狀態”數組是二維數組。
dp[l][r] 表示子串 s[l, r](包括區間左右端點)是否構成回文串,是一個二維布爾型數組。即如果子串 s[l, r] 是回文串,那么 dp[l][r] = true。
2、找到 “狀態轉移方程”。
首先,我們很清楚一個事實:
1、當子串只包含 11 個字符,它一定是回文子串;
2、當子串包含 2 個以上字符的時候:如果 s[l, r] 是一個回文串,例如 “abccba”,那么這個回文串兩邊各往里面收縮一個字符(如果可以的話)的子串 s[l + 1, r - 1] 也一定是回文串,即:如果 dp[l][r] == true 成立,一定有 dp[l + 1][r - 1] = true 成立。
根據這一點,我們可以知道,給出一個子串 s[l, r] ,如果 s[l] != s[r],那么這個子串就一定不是回文串。如果 s[l] == s[r] 成立,就接著判斷 s[l + 1] 與 s[r - 1],這很像中心擴散法的逆方法。
事實上,當 s[l] == s[r] 成立的時候,dp[l][r] 的值由 dp[l + 1][r - l] 決定,這一點也不難思考:當左右邊界字符串相等的時候,整個字符串是否是回文就完全由“原字符串去掉左右邊界”的子串是否回文決定。但是這里還需要再多考慮一點點:“原字符串去掉左右邊界”的子串的邊界情況。
1、當原字符串的元素個數為 33 個的時候,如果左右邊界相等,那么去掉它們以后,只剩下 11 個字符,它一定是回文串,故原字符串也一定是回文串;
2、當原字符串的元素個數為 22 個的時候,如果左右邊界相等,那么去掉它們以后,只剩下 00 個字符,顯然原字符串也一定是回文串。
把上面兩點歸納一下,只要 s[l + 1, r - 1] 至少包含兩個元素,就有必要繼續做判斷,否則直接根據左右邊界是否相等就能得到原字符串的回文性。而“s[l + 1, r - 1] 至少包含兩個元素”等價于 l + 1 < r - 1,整理得 l - r < -2,或者 r - l > 2。
綜上,如果一個字符串的左右邊界相等,以下二者之一成立即可:
1、去掉左右邊界以后的字符串不構成區間,即“ s[l + 1, r - 1] 至少包含兩個元素”的反面,即 l - r >= -2,或者 r - l <= 2;
2、去掉左右邊界以后的字符串是回文串,具體說,它的回文性決定了原字符串的回文性。
于是整理成“狀態轉移方程”:
dp[l, r] = (s[l] == s[r] and (l - r >= -2 or dp[l + 1, r - 1]))
或者
dp[l, r] = (s[l] == s[r] and (r - l <= 2 or dp[l + 1, r - 1]))
編碼實現細節:因為要構成子串 l 一定小于等于 r ,我們只關心 “狀態”數組“上三角”的那部分取值。理解上面的“狀態轉移方程”中的 (r - l <= 2 or dp[l + 1, r - 1]) 這部分是關鍵,因為 or 是短路運算,因此,如果收縮以后不構成區間,那么就沒有必要看繼續 dp[l + 1, r - 1] 的取值。
讀者可以思考一下:為什么在動態規劃的算法中,不用考慮回文串長度的奇偶性呢。想一想,答案就在狀態轉移方程里面。
具體編碼細節在代碼的注釋中已經體現。
?
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;}}
};
?