题目描述
给定一个字符串,找出其中最长的回文子串
解题思路
暴力法
最容易想到的就是暴力破解,求出每一个子串,之后判断是不是回文,找到最长的那个
求每一个子串时间复杂度O(N^2), 判断子串是不是回文O(N),两者是相乘关系,所以时间复杂度为O(N^3)。
动态规划O(N ^ 2)
设状态dp[j][i]表示索引j到索引i的子串是否是回文串。则转移方程为:
则dp[j][i]为true时表示索引j到索引i形成的子串为回文子串,且子串起点索引为j,长度为i - j + 1。
算法时间复杂度为O(N ^ 2)。
中心扩展法O(N ^ 2)
假设一个字符串str是回文串,那么它一定是左右对称的,所以该字符串以某个字符str[mid]为中心的前缀和后缀一定是相同的。例如,对于回文串"aba",以’b’为中心,它的前缀和后缀都是’a’。故,可以枚举字符串的每个字符作为回文串的中心位置,左右扩展得到回文串,然后比较得到最长的长度。
注意:这里需要考虑到一个特殊情况,即存在"YXaaXY"这样长度为偶数的特殊子串,这里可以将"aa"视为’a’。
具体见代码:
public static String getLongestPhraseByExpMid(char[] s)
{
int len = s.length;
if (len == 1 || len == 0)
return String.valueOf(s);
int max_len = 1, start = -1; // 最长回文string的起始位置,max_len是最长回文长度
for (int i = 0; i < len; i++)
{
int l = i - 1, r = i + 1; // left, right
// 把一个字符作为中心
while (l >= 0 && r <= len-1 && s[l] == s[r]){
--l; ++r;
}
if (r-l-1 > max_len)
{
max_len = r-l-1;
start = l + 1;
}
if (i != len-1 && s[i] == s[i+1])
{
l = i-1;
r = i+2;
// 两个相同字符作为中心
while (l >= 0 && r <= len - 1 && s[l] == s[r]){
--l; ++r;
}
if (r-l-1 > max_len)
{
max_len = r-l-1;
start = l+1;
}
}
}
if(start == -1) // 当没有回文子串的时候,随便返回一个就行,为保证不会越界,返回第一个就行
return "" + s[0];
return String.valueOf(s).substring(start, start+max_len);
}
Mancher法
首先,Manacher算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用#号。下面举一个例子:
Len数组简介与性质
Manacher算法用一个辅助数组Len[i]表示以字符T[i]为中心的最长回文字串的最右字符到T[i]的长度,比如以T[i]为中心的最长回文字串是T[l,r],那么Len[i]=r-i+1。
Len数组中表示的是字符串对应位置最长回文子串的半径。
对于上面的例子,可以得出Len[i]数组为:
Len数组的性质:Len[i]-1就是该回文子串在原字符串S中的长度。
证明:
字符串T由字符串S转换而来
=》在字符串T中,所有的回文子串都为奇数
=》在字符串T中,以T[i]为中心的最长回文子串,其长度为2*Len[i]-1
且 字符串T中所有回文子串中,分隔符#的数量一定比其他字符的数量多1
=》字符串T中所有回文子串中,分隔符#的数量为Len[i],其余Len[i]-1个字符来自原字符串S
=》在S中以T[i]为中心的回文子串长度为Len[i]-1
求解思路
我们再引入一个辅助变量MaxRight,表示当前访问到的所有回文子串,所能触及的最右一个字符的位置。另外还要记录下MaxRight对应的回文串的对称轴所在的位置,记为pos,它们的位置关系如下。
我们从左往右地访问字符串来求RL,假设当前访问到的位置为i,即要求RL[i],在对应上图,i必然是在po右边的(obviously)。但我们更关注的是,i是在MaxRight的左边还是右边。我们分情况来讨论。
当i在MaxRight的左边
情况1)可以用下图来刻画:
我们知道,图中两个红色块之间(包括红色块)的串是回文的;并且以i为对称轴的回文串,是与红色块间的回文串有所重叠的。我们找到i关于pos的对称位置j,这个j对应的RL[j]我们是已经算过的。根据回文串的对称性,以i为对称轴的回文串和以j为对称轴的回文串,有一部分是相同的。这里又有两种细分的情况。
以j为对称轴的回文串比较短,短到像下图这样。
这时我们知道RL[i]至少不会小于RL[j],并且已经知道了部分的以i为中心的回文串,于是可以令RL[i]=RL[j]。但是以i为对称轴的回文串可能实际上更长,因此我们试着以i为对称轴,继续往左右两边扩展,直到左右两边字符不同,或者到达边界。
以j为对称轴的回文串很长,这么长:
这时,我们只能确定,两条蓝线之间的部分(即不超过MaxRight的部分)是回文的,于是从这个长度开始,尝试以i为中心向左右两边扩展,,直到左右两边字符不同,或者到达边界。
不论以上哪种情况,之后都要尝试更新MaxRight和pos,因为有可能得到更大的MaxRight。
具体操作如下:
step 1: 令RL[i]=min(RL[2*pos-i], MaxRight-i)
// 2 * postion - i => j
// rad[postion]-i+postion => MaxRight - i
step 2: 以i为中心扩展回文串,直到左右两边字符不同,或者到达边界。
step 3: 更新MaxRight和pos
2)当i在MaxRight的右边
遇到这种情况,说明以i为对称轴的回文串还没有任何一个部分被访问过,于是只能从i的左右两边开始尝试扩展了,当左右两边字符不同,或者到达字符串边界时停止。然后更新MaxRight和pos。
public static int getLongestPhraseByManacher(String string){
// 1.构造新的字符串
// 为了避免奇数回文和偶数回文的不同处理问题,在原字符串中插入'#',将所有回文变成奇数回文
StringBuilder newStr = new StringBuilder();
newStr.append("#");
for (int i=0;i<string.length();i++){
newStr.append(string.charAt(i));
newStr.append("#");
}
// rad[i]表示以i为中心的回文的最大半径,i至少为1,即该字符本身
int [] rad = new int[newStr.length()];
// maxRight表示已知的回文中,最右的边界的坐标
int maxRight = -1;
// postion表示已知的回文中,拥有最右边界的回文的中点坐标
int postion = -1;
// 2.计算所有的rad
// 这个算法是O(n)的,因为right只会随着里层while的迭代而增长,不会减少。
for (int i=0;i<newStr.length()-1;i++){
// 2.1.确定一个最小的半径
int r=1;
if (i<=maxRight){
// 2 * postion - i => j
// rad[postion]-i+postion => MaxRight - i
r = Math.min(rad[postion]-i+postion, rad[2 * postion - i]);
}
// 2.2.尝试更大的半径
while (i-r >= 0 && i+r < newStr.length()
&& newStr.charAt(i-r) == newStr.charAt(i+r)){
r++;
}
// 2.3.更新边界和回文中心坐标
if (i + r - 1 > maxRight){
maxRight = i + r - 1;
postion = i;
}
rad[i] = r;
}
// 3.扫描一遍rad数组,找出最大的半径
int maxLength = 0;
for (int r : rad) {
if (r > maxLength) {
maxLength = r;
}
}
return maxLength - 1;
}