题目:Leetcode 第5题
难度:中等
问题描述:给你一个字符串
s
,找到s
中最长的回文子串示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:输入:s = "cbbd"
输出:"bb"
提示:
1 <= s.length <= 1000
s 仅由数字和英文字母组成
这道题的解答方法非常多,动态规划 或者中心扩散解法都行。但今天借着这道题我们来来了解一下Manacher算法(*^_^*)
Manachar算法主要是处理字符串中关于回文串的问题的,它可以在 O(n) 的时间处理出以字符串中每一个字符为中心的回文串半径,由于将原字符串处理成两倍长度的新串,在每两个字符之间加入一个特定的特殊字符,因此原本长度为偶数的回文串就成了以中间特殊字符为中心的奇数长度的回文串了。
Manacher算法提供了一种巧妙的办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用#号。 ----------------来自百度百科
一个字符串一般可以分为两种:奇数字符串和偶数字符串。所以有的时候需要分类讨论。现在通过一种方法我们可以把字符串的数量确定为奇数-----添加其他字符。这道题我们可以添加字符‘#’ 。
"aba" - ->"#a#b#a#"(长度是7)
"abba" - ->"#a#b#b#a#"(长度是9)
这里再来引用一个变量叫回文半径,通过添加特殊字符,原来字符串长度无论是奇数还是 偶数最终都会变为奇数,而且因为特殊字符的引用,回文子串的第一个和最后一个字符一定是你添加的那个特殊字符。为什么呢?
1,一个奇数字符串 添加的字符数量一定为偶数,又奇数加上偶数 结果一定为奇数、
2,偶数字符串同理。
那么什么是回文半径呢?添加字符后字符串长度一定为奇数,那么在一个回文字符串中最中间的数到字符串的最右边(或者左边)的长度就叫做回文半径。
知道了回文半径,再来继续解答这道题。看下图:
我们要求i 处的回文半径 可以利用 对称性 利用 j 求处 i 。那么j 的回文半径怎么求呢? 我们可以利用动态规划的思想。我们设 p[i]为各点的回文半径的大小。
当 i 在 maxright 左边时 分两种情况:
1,i 的对称点 j 的回文半径 小于maxRight-i 则 p[i]=p[j]
2,反之,则p[i] =maxright - i 因为 超出maxRight 的部分没办法确定,等待之后的计算。
当 i 在 maxRight 右边时 则p[i]无法再用上述的方法了,就得一个个的判断了。
看代码:
string longestPalindrome(string s) {
//首先添加字符‘#’
string t;
for(char c:s)
{
t+='#';
t+=c;
}
t+='#';
//添加字符后的长度
int maxlen=t.size();
//p 用来记录各点的回文半径
vector<int> p(maxlen);
//right(某个回文串延伸到的最右边下标)
//center(right所属回文串中心下标)
//res(记录遍历过的最大回文串中心下标)
//len(记录遍历过的最大回文半径)
int center=0,res=0,right=0,len=0;
for(int i=0;i<maxlen;i++)
{
//如果 i 在right 左边
if(i<right)
{
//2*center-i就是 i 相对 center 的对称点 j
//如果j 点的半径 小于 right - i 那么 p[i]=p[j]即可
if(p[2*center-i]<right-i){
p[i]=p[2*center-i];
}
else{
//反之,p[i]=right-i
p[i]=right-i;
//进一步计算 i 点的半径大小
while(i-p[i]>=0 && i+p[i]<maxlen && t[i-p[i]]==t[i+p[i]])
p[i]++;
}
}
else{
//如果i 在right 右边 则p[i]只能从 1 开始 一个个判断
p[i]=1;
while(i-p[i]>=0 && i+p[i]<maxlen && t[i-p[i]]==t[i+p[i]])
p[i]++;
}
//当i处的回文字符串长度超过right时 更新center 和right
if(i+p[i]>right){
right=i+p[i];
center=i;
}
//len 记录最大的半径 res记录最大半径的位置
if(p[i]>len){
len=p[i];
res=i;
}
}
//计算出res 处的最长子串
len-=1;
int start=(res-len)>>1;
return s.substr(start,len);
}
这里最后三行代码可能不太还好理解。其中resr就是最大回文子串(添加特殊字符之后的)中间的那个字符,len就是最 大回文半径,我们看上面的第一张图会发现,原字符串 s 中的最长子序列 的长度就为 len -1.然后 start 为(res-len)>>1 之所以>>1 是因为 要去掉 其中添加的字符数。
其实上面的三种判断是可以合并的,看代码:
//下面的语句只能确定i~right的回文情况。
p[i]=i<right ? min(p[2*center-i],right-i) : 1;
//进一步计算 i 点的半径大小
while(i-p[i]>=0 && i+p[i]<maxlen && t[i-p[i]]==t[i+p[i]])
p[i]++;
见最终代码:
string longestPalindrome(string s) {
//首先添加字符‘#’
string t;
for(char c:s)
{
t+='#';
t+=c;
}
t+='#';
//添加字符后的长度
int maxlen=t.size();
//p 用来记录各点的回文半径
vector<int> p(maxlen);
//right(某个回文串延伸到的最右边下标)
//center(right所属回文串中心下标)
//res(记录遍历过的最大回文串中心下标)
//len(记录遍历过的最大回文半径)
int center=0,res=0,right=0,len=0;
for(int i=0;i<maxlen;i++)
{
//下面的语句只能确定i~right的回文情况。
p[i]=i<right ? min(p[2*center-i],right-i) : 1;
//进一步计算 i 点的半径大小
while(i-p[i]>=0 && i+p[i]<maxlen && t[i-p[i]]==t[i+p[i]])
p[i]++;
//当i处的回文字符串长度超过right时 更新center 和right
if(i+p[i]>right){
right=i+p[i];
center=i;
}
//len 记录最大的半径 res记录最大半径的位置
if(p[i]>len){
len=p[i];
res=i;
}
}
//计算出res 处的最长子串
len-=1;
int start=(res-len)>>1;
return s.substr(start,len);
}