限于本人水平时间有限,本题虽然有时间复杂度为O(n)的Manacher算法,但是我并不看的懂= =,如果想了解本题的最佳算法请移步别的介绍Manacher算法的博客。
题目概述:
题目链接:点我做题
解题思路
一、暴力算法
这是我看到这个题第一时间想出的算法,具体来说,就是用两层循环遍历当前字符串的所有子串,然后利用前后指针法判断当前串是否为回文串,如果是,那么再比较一下这个串的长度和之前获得的最长相等子串长度哪个更长,如果它比之前获得的最长相等子串长,那就更新它的长度为最长相等子串的长度,代码如下:
class Solution {
public:
string longestPalindrome(string s)
{
int len = s.size();
if (len == 1)
{
return s;
}
int maxsize = 1;
int retleft = 0;
for (int left = 0; left < len; left++)
{
for (int right = left + 1; right < len; right++)
{
if (isPalindrome(s, left, right) == true)
{
int size = right - left + 1;
if (size > maxsize)
{
maxsize = size;
retleft = left;
}
}
}
}
return s.substr(retleft, maxsize);
}
bool isPalindrome(const string& s, int left, int right)
{
while (left < right)
{
if (s[left] != s[right])
{
return false;
}
left++;
right--;
}
return true;
}
};
时间复杂度分析:
两层循环中嵌套一个前后指针法,时间复杂度是
O
(
n
3
)
O(n^3)
O(n3)
空间复杂度分析:
没有开辟任何和字符串长度有关的空间,空间复杂度为
O
(
1
)
O(1)
O(1)
二、动态规划
要判断一个串是否为回文串,如果它两端的字符相等并且去掉它两端的字符后形成的串也是回文串,那么这个串就是回文串,不满足其中任何一条,这个串都不是回文串,把上面的思考转化为数学语言,定义:
p
(
i
,
j
)
=
t
r
u
e
,
i
f
(
"
s
[
i
]
.
.
.
s
[
j
]
"
是
回
文
子
串
)
p
(
i
,
j
)
=
f
a
l
s
e
,
i
f
(
"
s
[
i
]
.
.
.
s
[
j
]
"
不
是
回
文
子
串
)
p(i,j) = true, if ("s[i]...s[j]"是回文子串)\\ p(i,j) = false, if ("s[i]...s[j]"不是回文子串)
p(i,j)=true,if("s[i]...s[j]"是回文子串)p(i,j)=false,if("s[i]...s[j]"不是回文子串)
显然,根据我们的思考,
p
(
i
,
j
)
p(i,j)
p(i,j)有以下数学性质(状态转移方程):
p
(
i
,
j
)
=
p
(
i
+
1
,
j
−
1
)
∧
(
s
[
i
]
=
=
s
[
j
]
)
p(i,j) = p(i+1, j-1) \wedge(s[i]==s[j])
p(i,j)=p(i+1,j−1)∧(s[i]==s[j])
并且有初始条件:
p
(
i
,
i
)
=
t
r
u
e
p
(
i
,
i
+
1
)
=
t
r
u
e
,
i
f
(
s
[
i
]
=
=
s
[
i
+
1
]
)
p
(
i
,
i
+
1
)
=
f
a
l
s
e
,
i
f
(
s
[
i
]
!
=
s
[
i
+
1
]
)
p(i, i) = true\\ p(i,i+1) = true,if(s[i] == s[i+1])\\ p(i,i+1) =false,if(s[i]!=s[i+1])
p(i,i)=truep(i,i+1)=true,if(s[i]==s[i+1])p(i,i+1)=false,if(s[i]!=s[i+1])
有了这些条件,我们可以设计一个动态规划来解决这个问题,用一个二维数组
d
p
dp
dp来表示串是否为回文子串,
d
p
[
i
]
[
j
]
=
p
(
i
,
j
)
dp[i][j] = p(i,j)
dp[i][j]=p(i,j),显然一个字符的子串一定是回文子串,所以先走一个for循环让
d
p
[
i
]
[
i
]
=
t
r
u
e
dp[i][i]=true
dp[i][i]=true,然后嵌套两层循环,
L
L
L在外层循环,以
L
L
L代表这次循环时子串的长度,
l
e
f
t
left
left在内层循环,
l
e
f
t
left
left表示这次当前子串的起点,根据
L
L
L和
l
e
f
t
left
left也可以计算出串的右端点
r
i
g
h
t
=
l
e
f
t
+
L
−
1
right = left + L -1
right=left+L−1,首先检查
r
i
g
h
t
right
right是否越界,如果越界了就没有讨论必要了,直接
b
r
e
a
k
break
break跳出left这层的循环。然后检查
L
L
L的长度是不是2,如果是2的话就要走第二条初始条件,令
d
p
[
l
e
f
t
]
[
r
i
g
h
t
]
=
(
s
[
l
e
f
t
]
=
=
s
[
r
i
g
h
t
]
)
dp[left][right]=(s[left]==s[right])
dp[left][right]=(s[left]==s[right]),否则就走状态转移方程:
d
p
[
l
e
f
t
]
[
r
i
g
h
t
]
=
(
d
p
[
l
e
f
t
+
1
]
[
r
i
g
h
t
−
1
]
)
&
&
(
s
[
l
e
f
t
]
=
=
s
[
r
i
g
h
t
]
)
dp[left][right] = (dp[left + 1][right - 1]) \\\&\& (s[left] == s[right])
dp[left][right]=(dp[left+1][right−1])&&(s[left]==s[right])
我们的长度是从
L
=
2
L=2
L=2开始走的,所以正好可以借助循环一层一层求出dp的每一个有意义的值(指
l
e
f
t
<
=
r
i
g
h
t
left<=right
left<=right的情况),求完
d
p
[
l
e
f
t
]
[
r
i
g
h
t
]
dp[left][right]
dp[left][right],判断一下
d
p
[
l
e
f
t
]
[
r
i
g
h
t
]
dp[left][right]
dp[left][right]是否为真且
L
L
L是否大于当前最长回文串长度,如果大于,更新当前最大回文串长度和当前最大回文串的左端点,最后用substr函数返回.
代码:
class Solution {
public:
string longestPalindrome(string s)
{
int len = s.size();
if (len == 1)
{
return s;
}
vector<vector<bool>> dp(len, vector<bool>(len));
//p[i,j] = true表明串"s[i]...s[j]"是回文串
//反之不是回文串
//显然一个串是回文串的充要条件是 两端相等且中间部分是回文串
//p[i, j] = p[i+1, j-1] && s[i] == s[j]
//有初始条件
//p[i,i]=true 一个字符显然是回文串
//p[i,i+1]=true, if s[i] == s[i+1];false, else
//先初始化 使得dp[i][i] == true
for (int i = 0; i < len; i++)
{
dp[i][i] = true;
}
//L表示子串长度 以i为起点 判断所有的[i, i+l-1]是否是回文串
int maxsize = 1;
int ansleft = 0;
for (int L = 2; L <= len; L++)
{
for (int i = 0; i < len; i++)
{
int j = i + L - 1;
if (j >= len)
{
break;
}
//判断是不是回文串 分两种情况
//当长度是2时 就看两个字符相不相等
//当长度大于2时 就看dp[i+1, j-1]
//&& s[i] == s[j]是否为真
if (L == 2)
{
dp[i][j] = (s[i] == s[j]);
}
else
{
dp[i][j] = (dp[i + 1][j - 1]) && (s[i] == s[j]);
}
if (dp[i][j] && L > maxsize)
{
maxsize = L;
ansleft = i;
}
}
}
string ret = s.substr(ansleft, maxsize);
return ret;
}
};
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
三、中心拓展法
观察到上一个动态规划的方法中的状态转移方程:
d
p
[
l
e
f
t
]
[
r
i
g
h
t
]
=
(
d
p
[
l
e
f
t
+
1
]
[
r
i
g
h
t
−
1
]
)
&
&
(
s
[
l
e
f
t
]
=
=
s
[
r
i
g
h
t
]
)
dp[left][right] = (dp[left + 1][right - 1]) \&\&\\ (s[left] == s[right])
dp[left][right]=(dp[left+1][right−1])&&(s[left]==s[right])
突然意识到我们可以遍历每个子串的同时让它向外拓展得到最长回文串,拓展可以用前后指针法,一个left一个right指向待检查的是否相等的字符,控制left和right不要越界即可,注意,为了能遍历所有回文串的情况(奇数长度和偶数长度),我们拓展回文串的时候要拓展以当前位置为中心的回文串和以当前位置(奇数长度)和以当前位置和当前位置的下一位置为中心的回文串(偶数长度),代码如下:
class Solution {
public:
string longestPalindrome(string s)
{
//拓展法
//根据上一个动态规划法的状态转移方程
//p[i,j] = p[i+1, j-1] && s[i] == s[j]
//我们可以遍历每个结点 去看每个结点最多能拓展成多长的子串
//这里要注意 每个结点有两种拓展方法 一种是以自己为中心直接拓展
//一种是以自己和自己后面一个结点 两个结点为中心拓展
//这样才能囊括所有情况
int ansleft = 0;
int ansright = 0;
int len = s.size();
for (int curleft = 0; curleft < len; curleft++)
{
auto [left1, right1] = extend(s, curleft, curleft);
auto [left2, right2] = extend(s, curleft, curleft + 1);
//与往轮最长长度比较更新最长回文串的left和right
if (right1 - left1 > ansright - ansleft)
{
ansleft = left1;
ansright = right1;
}
if (right2 - left2 > ansright - ansleft)
{
ansleft = left2;
ansright = right2;
}
}
return s.substr(ansleft, ansright - ansleft + 1);
}
pair<int, int> extend(const string& s, int left, int right)
{
//这里left和right是指向待检查是否为相等字符的位置
//所以如果跳出while循环了说明当前位置拓展失败
//返回上一位置
while (left >= 0 && right < s.size()
&& s[left] == s[right])
{
left--;
right++;
}
return {left + 1, right - 1};
}
};
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)