题目链接
https://leetcode.com/problems/longest-palindromic-substring
解题思路:
求解最长公共子串问题
- 暴力求解,时间复杂度 o ( n 3 ) o(n^3) o(n3)
- 动态规划,时间复杂度 o ( n 2 ) o(n^2) o(n2)
- 二分+字符串hash算法,时间复杂度 o ( n l o g ( n ) ) o(nlog(n)) o(nlog(n))
- manacher算法,时间复杂度
o
(
n
)
o(n)
o(n)
本文着重介绍动态规划及manacher解法,二分+字符串hash解法思路见链接
动态规划求解:
设
d
p
[
i
]
[
j
]
=
1
dp[i][j]=1
dp[i][j]=1表示从
i
i
i到
j
j
j的字符串是一段回文字符串,反之若
d
p
[
i
]
[
j
]
=
0
dp[i][j]=0
dp[i][j]=0表示从
i
i
i到
j
j
j的字符串非回文字符串,那么有:
d
p
[
i
]
[
j
]
=
{
1
d
p
[
i
+
1
]
[
j
−
1
]
=
=
1
a
n
d
s
[
i
]
=
=
s
[
j
]
0
e
l
s
e
dp[i][j]=\left\{ \begin{aligned} 1 &\ dp[i+1][j-1]==1 \ and \ s[i]==s[j] \\ 0 & \ else \\ \end{aligned} \right.
dp[i][j]={10 dp[i+1][j−1]==1 and s[i]==s[j] else
由于求解区间
[
i
,
j
]
[i,j]
[i,j]需要用到内部区间
[
i
+
1
,
j
−
1
]
[i+1,j-1]
[i+1,j−1]的信息,故直接从0到s.size()顺序迭代更新
i
i
i,
j
j
j不可取,所以我们需要改变更新方式。
这里采用的是子串的长度和区间的左端点相结合更新的方式,先预处理子串长度为1和2的情况,然后按照上述公式更新即可。时间复杂度
o
(
n
2
)
o(n^2)
o(n2)。
class Solution {
public:
int dp[1010][1010];
string longestPalindrome(string s) {
memset(dp,0,sizeof(dp));
for(int i=0;i<s.size();i++){
dp[i][i]=1;
if(i+1<s.size()&&s[i]==s[i+1]) dp[i][i+1]=1;
}
for(int L=3;L<=s.size();L++){
for(int i=0;i+L-1<s.size();i++){
int j=i+L-1;
if(s[i]==s[j]&&dp[i+1][j-1]==1)
dp[i][j]=1;
}
}
for(int L=s.size();L>=1;L--){
for(int i=0;i+L-1<s.size();i++){
int j=i+L-1;
if(dp[i][j]){
return s.substr(i,L);
}
}
}
return s;
}
};
manacher算法求解
首先预处理,在字符串的首末和中间都加上分隔符号#,保证输入的子串长度是奇数。
具体原因见https://segmentfault.com/a/1190000003914228
利用
m
x
mx
mx表示已经访问的所有回文子串中最右边的位置,
利用
p
o
s
pos
pos表示这个子串的回文中心,即有
m
x
=
p
o
s
+
p
[
p
o
s
]
mx=pos+p[pos]
mx=pos+p[pos],
p
[
p
o
s
]
p[pos]
p[pos]表示以
p
o
s
pos
pos为回文中心的字符串对应的回文半径,
其次求解字符串中第
i
i
i个位置对应的回文半径
p
[
i
]
p[i]
p[i]
在求解和更新
p
[
i
]
p[i]
p[i]时,需要注意:
-
利用 i i i关于回文中心点 p o s pos pos的对称点的信息来简化计算的复杂度,不如设该点为 j j j,那么有:当 i i i在 p o s pos pos和 m x mx mx中间时:
i − p o s = p o s − j i-pos=pos-j i−pos=pos−j
即 j = 2 ∗ p o s − i j=2*pos-i j=2∗pos−i
此时 p [ i ] p[i] p[i]的信息可以更新为 p [ i ] = m i n ( p [ j ] , m x − i ) p[i]=min(p[j],mx-i) p[i]=min(p[j],mx−i) -
若 i i i在 m x mx mx右边,即i超出了 p [ p o s ] p[pos] p[pos]对应的回文半径,这时候以i为中心的回文字符串就是其本身,所以 p [ i ] = 1 p[i]=1 p[i]=1
-
由于 i i i和 j j j关于 p o s pos pos对称,而 p [ i ] p[i] p[i]表示以 i i i为中心的子串的回文半径,显然仅靠 p [ j ] p[j] p[j]的信息是不够的,因为如果 i + p [ i ] > m x i+p[i]>mx i+p[i]>mx后,很可能出现 s [ i + p [ i ] ] = = s [ i − p [ i ] ] s[i+p[i]]==s[i-p[i]] s[i+p[i]]==s[i−p[i]],所以需要进一步更新 p [ i ] p[i] p[i]
回文半径
p
[
i
]
−
1
p[i]-1
p[i]−1中最大的值对应整个字符串最大的回文子串长度。
总体时间复杂度
o
(
n
)
o(n)
o(n)。
class Solution {
public:
string longestPalindrome(string s) {
if(s.size()<=1) return s;
string t="",q="";
for(int i=0;i<s.size();i++){
t+="#";
t+=s[i];
}
t+="#";
vector<int> ans=solve(t);
int maxv=-1,ind=-1;
for(int i=0;i<ans.size();i++){
if(ans[i]>maxv){
maxv=ans[i];
ind=i;
}
}
for(int i=ind-ans[ind]+1;i<=ind+ans[ind]-1;i++){
if(t[i]!='#') q+=t[i];
}
return q;
}
vector<int> solve(string t){
int pos=0,mx=0;
int len=t.size();
vector<int> p(len);
for(int i=0;i<len;i++){
if(i<mx) p[i]=min(p[2*pos-i],mx-i);//j=2*pos-i
else p[i]=1;
while(i-p[i]>=0&& i+p[i]<len && t[i+p[i]]==t[i-p[i]])//更新p[i]
p[i]++;
if(mx<i+p[i]-1){//更新最右边回文串对应的回文中心pos和右边界mx
pos=i;
mx=i+p[i]-1;
}
}
return p;
}
};