題目描述
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
輸入與輸出
class Solution {
public:
string longestPalindrome(string s) {
}
};
樣例
- Input:"babad",Output:"bab",Note:"aba" is also a valid answer。
- Input:"cbbd",Output:"bb"。
題解與分析
解法一
鑒于 s 的最大長度為1000,可以考慮暴力解法,即枚舉每一個可能的中間位置,分別向兩側擴展,得到以該位置為中心的最長回文字串。枚舉過程中記錄最長回文子串的信息,最后返回子串即可。
該解法中需要注意的一點:回文子串的長度的奇偶性不確定,因此每個位置需要兩次擴展過程。
C++ 代碼如下:
class Solution {
public:
string longestPalindrome(string s) {
int size = s.size();
int start = -1, length = 0; // 維護最長回文子串相關信息
// 枚舉可能的中心位置
for (int i = 0; i < size; ++i) {
// 回文子串長度為奇數的情況
int left = i - 1, right = i + 1;
while (left >= 0 && right < size && s[left] == s[right])
--left, ++right;
if (right - left - 1 > length)
start = left + 1, length = right - left - 1;
// 回文子串長度為偶數的情況
left = i, right = i + 1;
while (left >= 0 && right < size && s[left] == s[right])
--left, ++right;
if (right - left - 1 > length)
start = left + 1, length = right - left - 1;
}
return s.substr(start, length);
}
};
該解法的時間復雜度為 O(n^2)。
解法二
與 3. Longest Substring Without Repeating Characters 類似,上述解法的時間復雜度高達 O(n^2) 的原因是沒有利用之前枚舉過程中得到的信息,導致大量重復的判斷。
下面介紹 Manacher 算法,該算法的時間復雜度為 O(n),缺點是只能處理長度為奇數的回文子串(可以預處理字符串使該算法也適用于偶數長度子串)。
預處理過程:在原字符串兩端與字符之間插入原字符串中未出現過的字符(本文中使用 '#'),例如 abacabba
處理后為 #a#b#a#c#a#b#b#a#
。這樣所有的回文子串均為奇數長度,例如 #a#b#a#
、#a#b#b#a#
。
設處理過的字符串為 Ma
,建立一個同等長度的 int 數組 Mp
。Mp[i]
用來記錄以位置i
為中心的最長回文子串的右半部分的長度(不包括位置i
本身),例如#a#b#b#a#
的右半部分為b#a#
,相應地,Mp[i] = 4
。通過上例還可以得到Mp
數組的另一個意義,它記錄了相應回文子串在原字符串對應的長度。
除此之外,建立變量Mx
用來記錄當前所有擴展過程中涉及到的最右側字符的位置,建立變量Id
記錄擴展到該位置時的中心位置。
下面分情況討論該算法如何利用之前枚舉過程中的信息,也是該算法的核心,對應代碼Mp[i] = Mx > i ? min(Mp[2 * Id - i], Mx - i) : 0;
。
- 首先判斷
Mx
與i
的位置關系: -
Mx > i
:說明之前某個回文子串曾經擴展到該位置右側,則該位置兩側字符中有一部分信息之前獲取過。這部分信息保存在Mp[2 * id - i]
中,其中2 * id - i
是i
關于Id
的對稱位置。進一步討論: -
Mp[2 * Id - i] <= Mx - i
:即以2 * id - i
為中心的最長回文子串在以id
為中心的最長回文子串內部,如下圖所示:
由于橙色部分是回文子串,那么兩個紅色部分是鏡像關系,如果左側紅色部分是最長回文子串,那么右側紅色部分也是對應位置的最長回文子串。即Mp[i] = Mp[2 * Id - i]
。 -
Mp[2 * Id - i] > Mx - i
:即以2 * id - i
為中心的最長回文子串的左側超出了以id
為中心的最長回文子串。如下圖所示:
與上種情況類似,但是以2 * id - i
為中心的最長回文子串中只有紅色部分在以id
為中心的最長回文子串內部,因此右側對應的紅色部分也是回文子串。但是在Mx
右側的字符尚未拓展,因此需要進一步拓展獲得最大回文子串。初始化Mp[i] = Mx - i
。 -
Mx <= i
:這種情況下i
右側的字符尚未拓展過,需要拓展獲得最長回文子串。初始化Mp[i] = 0
。
C++ 代碼如下:
class Solution {
public:
string longestPalindrome(string s) {
int size = s.size() * 2 + 1;
char *Ma = new char[size];
int *Mp = new int[size];
int Mx = -1, Id = -1;
int start, length = 0;
Ma[0] = '#';
for (int i = 0, p = 0; i < s.size(); ++i) {
Ma[++p] = s[i];
Ma[++p] = '#';
}
for (int i = 0; i < size; ++i) {
Mp[i] = Mx > i ? min(Mp[2 * Id - i], Mx - i) : 0;
while (i - Mp[i] - 1 >= 0 && i + Mp[i] + 1 < size && Ma[i - Mp[i] - 1] == Ma[i + Mp[i] + 1])
++Mp[i];
if (i + Mp[i] > Mx)
Mx = i + Mp[i], Id = i;
if (Mp[i] > length)
length = Mp[i], start = (i - Mp[i]) / 2;
}
delete[] Ma;
delete[] Mp;
return s.substr(start, length);
}
};
時間復雜度分析:上述代碼中,第一個 for 循環為 O(n),第二個 for 循環外層為 O(n),下面分析內部的 while 循環。由上述算法流程可知,每一次擴展操作都是在Mx
右側擴展,即每執行一次++Mp[i];
都會導致Mx
增加 1,即Mx
單調遞增。因為Mx
的取值范圍是[0, size)
,所以 while 循環內部代碼最多執行 O(n) 次。
該解法的時間復雜度為 O(n)。