最长回文子串
给出字符串s,求s的最长的回文子串
法一
从左向右遍历,将每个字符都可以尝试作为中心点,从中心向左右两边扩散回文串,如果左右相等则继续扩散
会出现两种情况:可能是类似 aba 的字符串,也可能是类似 abba 的情况
复杂度O(n^2)
法二 动态规划
用dp[i][j]表示从下标i到j的子串是否为回文串
初始化:dp[i][i]为True
- 用j从左往右遍历每个字符s[j]
- 对于某个j,用i从右往左遍历其左侧的每个字符s[i](保证i始终小于j)
- 状态转移方程:
若i==j
,dp[i][i]=True
若j-i==1
,dp[i][j]=(s[i]==s[j])
若j-i>1
, dp[i][j]=(s[i]==s[j]) and dp[i+1][j-1] - 若需要输出这个最长回文子串,则每次更新dp[i][j]时,若刷新了最长的记录,同步更新当前最长的回文子串长度maxLen=j-i+1和对应的起点i、终点j
- 复杂度O(n^2)
法三 马拉车算法
复杂度O(n),源于两个核心改进:
- 在每个字符之间都插入一个"#",这样长度为奇数和偶数的回文都同义处理,不用分类
- 利用历史信息,避免重复工作
具体而言,从左到右处理每个字符s[i]
,维护数组p[i]
代表以s[i]
为中心的最长回文串的半径。
假定当前已经知道左侧一部分p[i]
(对应不同回文串),那么其中最有用的那个回文串是[右端点最大的那个回文串],我们记这个最有用的回文串的中心点s[c]
(为什么要维护这个点?因为这个回文串右端点最远,给出了最多的对称信息,我们可以利用其更新当前靠右的最新p[i]
!),那么我们可以利用s[c]
左半边的p[i']
信息推知s[c]
右半边的未知p[i]
例如当前最长回文串的中心在
c=11
,由于其左右两边对称,那么右边的p[13]
=左边的p[9]
=1;
但是这种对称性的利用有限制条件:仅在p[c]
指定的最大回文范围内(即图中的L
到R
之间)对称,超出这个范围后,我们仍然需要手动判断右侧的p[i]
例如对于p[7]
和p[15]
,我们只能保证两条绿色实线划定的范围必定是对称的(至少有p[15]>=R-i=5
),然而p[i']=7
,回文范围超过了左边界L
,因此不一定有p[i]=p[i']=7
,我们对于i=15
检查字符s[21]='c'
果然不等于s[9]='b'
,也就是说只有p[15]=5
而非p[15]=p[7]=7
上述过程的核心逻辑为:
if P[ i’ ] < R – i,(i
关于c
的左侧对称点i'
的回文长度没有超出绿色范围)
then P[ i ] ← P[ i’ ]
else P[ i ] ≥ R - i. (此时要穿过R
逐个字符判定p[i]
最大究竟是多少)
代码:
def manacher(A: str):
s = '#' + '#'.join(list(A)) + '#'
cIdx = 0# 当前的最有用的回文子串的中心点
R = -1# 目前已知的最长回文子串的右边界
p =[0 for i in range(len(s))]
ans_cIdx=0# 最长回文串的中心点
max_length = 0# 最长回文串长度
for i in range(1,len(s)-1):
if i < R:
p[i] = min(R-i,p[2*cIdx-i])# i关于c的左对称点为2*cIdx-i
else:
p[i] = 1# 此处是右端新的单个字符
#两边扩散,寻求更长的回文串
while i-p[i]>=0 and i+p[i]<len(p) and s[i-p[i]] == s[i+p[i]]:
p[i] += 1
if R < i + p[i]:
R = i + p[i]
cIdx = i
if p[i]-1 > max_length:#当前最长的回文子串
max_length = p[i]-1
ans_cIdx = i
tmp = s[ans_cIdx-p[ans_cIdx]+1:ans_cIdx+p[ans_cIdx]]
ans = "".join(tmp.split("#"))
print(max_length)
print(ans)
return max_length