最长回文子串manacher算法

最长回文子串

子串的含义是:在原串中连续出现的字符串片段。回文的含义是:正着看和倒着看相同,如abba和yyxyy。

基础题

给出一个长度不超过1000的字符串,判断它是不是回文(顺读,逆读均相同)的。

https://www.nowcoder.com/practice/df00c27320b24278b9c25f6bb1e2f3b8?tpId=69&&tqId=29674&rp=1&ru=/activity/oj&qru=/ta/hust-kaoyan/question-ranking

思路:从两端开始对比,如果相等,则开始缩小范围,继续对比;如果不等则返回false

public static boolean Palindrome(char []array){
		if(array.length==0||array.length==1)
			return true;
		int i=0;
		int j=array.length-1;
		while(i<j)
		{
			if(array[i]==array[j])
			{
				i++;
				j--;
			}
			else
				return false;
		}
		if(i>=j)
			return true;
		else
			return false;		
	}

升级题

输入一个字符串,求出其中最大的回文子串

1. Brute-force 解法

对于最长回文子串问题,最简单粗暴的办法是:找到字符串的所有子串,遍历每一个子串以验证它们是否为回文串。一个子串由子串的起点和终点确定,因此对于一个长度为n的字符串,共有n^2个子串。这些子串的平均长度大约是n/2,因此这个解法的时间复杂度是O(n^3)。

2. 改进的方法

显然所有的回文串都是对称的。长度为奇数回文串以最中间字符的位置为对称轴左右对称,而长度为偶数的回文串的对称轴在中间两个字符之间的空隙。可否利用这种对称性来提高算法效率呢?答案是肯定的。我们知道整个字符串中的所有字符,以及字符间的空隙,都可能是某个回文子串的对称轴位置。可以遍历这些位置,在每个位置上同时向左和向右扩展,直到左右两边的字符不同,或者达到边界。对于一个长度为n的字符串,这样的位置一共有n+n-1=2n-1个,在每个位置上平均大约要进行n/4次字符比较,于是此算法的时间复杂度是O(n^2)。

3. Manacher 算法(中文名:马拉车算法)

由于回文分为偶回文(比如 bccb)和奇回文(比如 bcacb),而在处理奇偶问题上会比较繁琐,所以这里我们使用一个技巧,具体做法是,在字符串首尾,及字符间各插入一个字符(前提这个字符未出现在串里)。

举个例子:s="abbahopxpo",转换为s_new="$#a#b#b#a#h#o#p#x#p#o#"(这里的字符 $ 只是为了防止越界,下面代码会有说明),如此,s 里起初有一个偶回文abba和一个奇回文opxpo,被转换为#a#b#b#a#和#o#p#x#p#o#,长度都转换成了奇数。

定义一个辅助数组int p[],其中p[i]表示以 i 为中心的最长回文的半径,例如:

i012345678910111213141516171819
s_new[i]$#a#b#b#a#h#o#p#x#p#
p[i] 1212521212121214121
可以看出,p[i] - 1正好是原字符串中最长回文串的长度


下面计算p[i],该算法增加两个辅助变量id和mx,其中id表示最大回文子串中心的位置,mx则为id+p[id],也就是最大回文子串的边界。

这个算法的关键点就在这里了:如果mx > i,那么p[i] >= MIN(p[2 * id - i], mx - i);对于 mx <= i 的情况,无法对 p[i]做更多的假设,只能p[i] = 1,然后再去匹配了。2 * id - i为 i 关于 id 的对称点(j=id+(id-i)=2*id-i),即上图的 j 点,而p[j]表示以 j 为中心的最长回文半径因此我们可以利用p[j]来加快查找

其实就是p[i] >= MIN(p[j], mx - i),就是求二者的最小值,当情况一时,p[j]=p[i]<mx-i;当情况二时,p[j]>mx-i;

if(mx > i)
{
      p[i] = (p[2*id - i] < (mx - i) ? p[2*id - i] : (mx - i));
}
else
{
       p[i] = 1;
}
根据回文的性质,p[i]的值基于以下三种情况得出:

(1)j 回文串全部在 id 的内部,  例如:  hcabaefeabacg

当 mx - i > P[j] 的时候,以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中,所以必有 P[i] = P[j],见下图。

根据代码,此时p[i] = p[j],那么p[i]还可以更大么?答案亦是不可能!见下图:
假设右侧新增的红色部分是p[i]可以增加的部分,那么根据回文的性质,a 等于 b ,也就是说 j 的回文应该再加上 a 和 b ,矛盾,所以假设不成立,故p[i] = p[j],也不可以再增加一分。

(2)j 的回文串有一部分在 id 的之外,  例如: abcdcbamabcdcbn

当 P[j] > mx - i 的时候,以S[j]为中心的回文子串不完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] >= mx - i。至于mx之后的部分是否对称,就只能一个一个匹配了。




上图中,黑线为 id 的回文,i 与 j 关于 id 对称,红线为 j 的回文。那么根据代码此时p[i] = mx - i,即紫线。那么p[i]还可以更大么?答案是不可能!见下图:
假设右侧新增的紫色部分是p[i]可以增加的部分,那么根据回文的性质,a 等于 d ,也就是说 id 的回文不仅仅是黑线,而是黑线 + 两条紫线,矛盾,所以假设不成立,故p[i] = mx - i,不可以再增加一分。

(3)j 回文串左端正好与 id 的回文串左端重合,例如:mabcdbcaxacbdcbax

根据代码,此时p[i] = p[j]p[i] = mx - i,并且p[i]还可以继续增加,所以需要

while (s_new[i - p[i]] == s_new[i + p[i]]) 
    p[i]++;
	public static char[] initChr(char[] ch) {//初始化字符串
		// TODO Auto-generated method stub
		char s_new[]=new char[2*ch.length+1];
//		s_new[0]='$';
		s_new[0]='#';
		int j=1;
		for(int i=0;i<ch.length;i++)
		{
			s_new[j++]=ch[i];
			s_new[j++]='#';			
		}		
		return s_new;
	}

public static int getLongestPalindrome(String A) {//求最长回文子串的长度
		char[] ch=A.toCharArray();
		char s_new[]=initChr(ch);
		int len=s_new.length;
		int id=0;
		int mx=0;
		int max_len=0;
		int p[]=new int[len];
		for(int i=0;i<len;i++)
		{
			if(i<mx)
			{
				p[i]=Math.min(p[2*id-i],mx-i);
			}else
				p[i]=1;
			//情况三:扩展回文半径,注意加边界判断
			while(i-p[i]>=0 && i+p[i]<len && s_new[i-p[i]]==s_new[i+p[i]])
			{
				p[i]++;
			}
			//更新id、mx
			if(i+p[i]>mx)
			{
				id=i;
				mx=i+p[i];
			}
			max_len= Math.max(max_len,p[i]);
		}
		return max_len-1; 
    }

以上for循环中为此算法的核心,掌握住可以在此扩展。

变形1:最长回文子串

以上是求长度,再稍加变形,即可求出最长回文子串,找出并记录最长回文子串时的中心位置id和此时回文半径长度p[i],然后以此中心向前后扩展即为最长回文子串
public static String getLongestPalindrome2(String A) {//求最长回文子串
		char[] ch=A.toCharArray();
		char s_new[]=initChr(ch);
		int len=s_new.length;
		int id=0;
		int mx=0;
		int max_len=0;
		int max_index=0;
		int p[]=new int[len];
		for(int i=0;i<len;i++)
		{
			if(i<mx)
			{
				p[i]=Math.min(p[2*id-i],mx-i);
			}else
				p[i]=1;
			//情况三:扩展回文半径,注意加边界判断
			while(i-p[i]>=0 && i+p[i]<len && s_new[i-p[i]]==s_new[i+p[i]])
			{
				p[i]++;
			}
			//更新id、mx
			if(i+p[i]>mx)
			{
				id=i;
				mx=i+p[i];
			}
			if(max_len<p[i])
			{
				max_len=p[i];
				max_index=id;
			}
			
		}
		max_len= max_len-1;
		char maxSubChr[] = new char[(2*max_len+1)/2];
		for(int i=max_index-max_len,j=0;i<=max_index+max_len;i++)
			if(s_new[i]!='#')
			{
				maxSubChr[j]=s_new[i];
				j++;
			}
		return new String(maxSubChr); 
    }

变形2:添加回文串

通过添加字符的方式使得新的字符串整体变成回文串,但是只能在原串的结尾添加字符,请返回在结尾添加的最短字符串。
核心思路:最右回文半径到达字符串最右边界的位置时,跳出循环,找到此时的中心id和此中心的回文半径(p[i]-1)

public static String addToPalindrome(String A) {//结尾添加最短字符串,使其变为回文串
		char []ch = A.toCharArray();
		char s_new []=initChr(ch);
		int id=0;
		int mx=0;
		int p[]=new int[s_new.length];
		int i=0;
		for(i=0;i<s_new.length;i++)
		{
			if(i<mx)
			{
				p[i]=Math.min(p[2*id-i], mx-i);
			}else
				p[i]=1;
			while(i+p[i]<s_new.length && i-p[i] >=0 && s_new[i-p[i]]==s_new[i+p[i]])
			{
				p[i]++;
			}
			if(i+p[i]>mx)
			{
				id=i;
				mx=i+p[i];
			}
			//最右回文半径到达字符串最右边界的位置时,跳出循环,找到此时的中心id和此中心的回文半径(p[i]-1)
			if(mx==s_new.length)
			{
				break;
			}
		}
		char final_ch []=new char[(id-(p[i]-1))/2];
		for(int j=id-p[i],k=0;j>=0;j--)
		{
			if(s_new[j]!='#')
			{
				final_ch[k++]=s_new[j];				
			}
		}
		return new String(final_ch);	        
	 }

复杂度

Manacher 算法的时间复杂度O(n).
根据(1)(2)(3),很容易推出 Manacher 算法的最坏情况,即为字符串内全是相同字符的时候,同理,我们也很容易知道最佳情况下的时间复杂度,即字符串内字符各不相同的时候。
尽管代码里面有两层循环,由于内层的循环只对尚未匹配的部分进行,因此对于id两侧对称的字符而言,只会进行计算一次p[i],因此时间复杂度是O(n)。
因为i与j关于id对称,这个j对应的p[j]我们是已经算过的。根据回文串的对称性,以i为对称轴的回文串和以j为对称轴的回文串,有一部分是相同的,不用再次计算的,也就是 p[i]=Math.min(p[2*id-i], mx-i) 语句的作用。


附加算法

时间复杂度和空间复杂度为O(n^2),开辟了一个二维数组
可以采用动态规划,列举回文串的起点或者终点来解最长回文串问题,无需讨论串长度的奇偶性。
public int longestPalindrome(String s) {
     int n=s.length();
     boolean[][] pal=new boolean[n][n];
     //pal[i][j] 表示s[i...j]是否是回文串
     int maxLen=0;
     for (int i=0;i<n;i++){  // i作为终点
         int j=i;    //j作为起点
         while (j>=0){
             if (s.charAt(j)==s.charAt(i)&&(i-j<2||pal[j+1][i-1])){
                 pal[j][i]=true;
                maxLen=Math.max(maxLen, i-j+1);
             }
             j--;
         }
     }
     return maxLen;
    }


参考
https://segmentfault.com/a/1190000003914228
http://www.61mon.com/index.php/archives/181/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值