Longest Palindromic Substring
Solution 1
双向维护状态的动态规划。
a
n
s
[
i
]
[
j
]
ans[i][j]
ans[i][j]表示从
i
i
i到
j
j
j的子串是否为回文串,状态维护如下:
{
a
n
s
[
i
]
[
j
]
:
=
t
r
u
e
i
=
j
a
n
s
[
i
]
[
j
]
:
=
t
r
u
e
j
−
i
=
1
,
s
[
i
]
=
s
[
j
]
a
n
s
[
i
]
[
j
]
:
=
a
n
s
[
i
−
1
]
[
j
+
1
]
j
−
i
>
1
,
s
[
i
]
=
s
[
j
]
,
a
n
s
[
i
−
1
]
[
j
−
1
]
=
t
r
u
e
\begin{cases} ans[i][j] := true & i = j \\ ans[i][j] := true & j - i = 1 ,\quad s[i] = s[j] \\ ans[i][j] := ans[i - 1][j + 1] & j - i > 1,\quad s[i] = s[j] ,\quad ans[i - 1][j - 1] = true \end{cases}
⎩⎪⎨⎪⎧ans[i][j]:=trueans[i][j]:=trueans[i][j]:=ans[i−1][j+1]i=jj−i=1,s[i]=s[j]j−i>1,s[i]=s[j],ans[i−1][j−1]=true
在维护构成中需要注意一点,两重循环的顺序要保证当前考察子串的内部状态已知,因此需要先确定尾边界,再迭代头边界,以保证内部子串的状态已经计算。
- 时间复杂度: O ( n 2 ) O(n^2) O(n2), n n n为字符串的长度
- 空间复杂度: O ( n 2 ) O(n^2) O(n2), n n n为字符串的长度
class Solution {
public:
string longestPalindrome(string s) {
vector<vector<bool>> ans(s.size());
// 初始化情形:每个字符本身是一种回文,最终的答案初始化为原始字串的第一个字符
for (int i = 0; i < s.size(); ++i) {
ans[i] = vector<bool>(s.size());
ans[i][i] = true;
}
int lenMax = 1;
string ansStr = s.substr(0, 1);
// 这里的一个处理:先迭代尾边界,再迭代头边界,这样能保证我们考察更长子串时,更短的子串已经被确认过(考察0到3时,1到2要确保考察到)
for (int j = 0; j < s.size(); ++j) {
for (int i = 0; i < j; ++i) {
if (j - i == 1) {
ans[i][j] = (s[i] == s[j]);
}
else {
ans[i][j] = (s[i] == s[j]) && ans[i + 1][j - 1];
}
if (ans[i][j] && j - i + 1 > lenMax) {
lenMax = j - i + 1;
ansStr = s.substr(i, j - i + 1);
}
// cout << i << " " << j << " " << ans[i][j] << " " << lenMax << " " << ansStr << endl;
}
}
return ansStr;
}
};
Solution 2
一个非常有趣的思路:如果一个子串是回文,将整个字符串翻转,回文串本身将会构成“公共子串”,我们的目标就是寻找“最长公共子串”。但是有一种可能是,字符串的头尾存在镜像对称的部分,这样的部分会在翻转过后被找到座位公共子串,这个时候就要确定一下两个字符串中的公共子串部分的索引是否对应同一个部分(注意,翻转字符串中公共子串的头索引对应原字符串中公共子串的尾索引)。
- 时间复杂度: O ( n 2 ) O(n^2) O(n2), n n n为字符串的长度
- 空间复杂度: O ( n 2 ) O(n^2) O(n2), n n n为字符串的长度
class Solution {
public:
string longestPalindrome(string s) {
string sRev(s);
reverse(sRev.begin(), sRev.end());
cout << sRev << endl;
vector<vector<int>> ans(s.size());
for (int i = 0; i < s.size(); ++i) {
ans[i] = vector<int>(sRev.size(), 0);
}
for (int j = 0; j < sRev.size(); ++j) {
if (s[0] == sRev[j]) {
ans[0][j] = 1;
}
}
for (int i = 0; i < sRev.size(); ++i) {
if (s[i] == sRev[0]) {
ans[i][0] = 1;
}
}
int lenMax = 1;
string ansStr = s.substr(0, 1);
for (int i = 1; i < s.size(); ++i) {
for (int j = 1; j < sRev.size(); ++j) {
// cout << i << " " << j << " ";
if (s[i] == sRev[j]) {
ans[i][j] = ans[i - 1][j - 1] + 1;
}
else {
ans[i][j] = 0;
}
// 这种逻辑下,有可能出现首位相同的情形,需要检查索引是否一致
if (ans[i][j] > lenMax && (i == sRev.size() - (j - ans[i][j] + 1) - 1 )) {
lenMax = ans[i][j];
ansStr = s.substr(i - lenMax + 1, lenMax);
}
// cout << ans[i][j] << " " << lenMax << " " << ansStr << endl;
}
}
return ansStr;
}
};
Solution 3
(来自官方)中心扩展法,其实是非常暴力的思路:以一个字符为中心,向两侧扩展检查回文性。对于偶数长度的回文子串,这个中心在两个字符的中心。官方题解中的单偶数中心处理比较巧妙,因此实现了一下。
- 时间复杂度: O ( n 2 ) O(n^2) O(n2), n n n为字符串的长度
- 空间复杂度: O ( 1 ) O(1) O(1)
class Solution {
public:
string longestPalindrome(string s) {
int lenMax = 1;
string ansStr = s.substr(0, 1);
for (int i = 0; i < s.size(); ++i) {
int lenOdd = expandStringChecker(s, i, i);
int lenEven = expandStringChecker(s, i, i + 1); // 中心在两个字符中间
int len = max(lenOdd, lenEven);
if (len > lenMax){
lenMax = len;
ansStr = s.substr(i - (len - 1) / 2, len);
}
}
return ansStr;
}
private:
int expandStringChecker(string s, int left, int right) {
int iLeft = left, iRight = right;
while (iLeft >= 0 && iRight < s.size() && s[iLeft] == s[iRight]) {
iLeft --;
iRight ++;
}
return iRight - iLeft - 1;
}
};
Solution 4
Solution 1的Python实现。
class Solution:
def longestPalindrome(self, s: str) -> str:
ans = list()
for i in range(len(s)):
ans.append([False] * len(s))
ans[i][i] = True
len_max = 1
ans_str = s[0: 1]
for j in range(len(s)):
for i in range(j):
if j - i == 1:
ans[i][j] = (s[i] == s[j])
else:
ans[i][j] = (s[i] == s[j]) and ans[i + 1][j - 1]
if ans[i][j] and (j - i + 1 > len_max):
len_max = j - i + 1
ans_str = s[i: j+1]
# print(i, j, ans[i][j], len_max, ans_str)
return ans_str