Manacher算法

首先,根据一个问题引入:

给定一个字符串str,返回str中最长回文子串的长度。举例:str=“123”,其中的最长回文子串为“1”、“2”或者“3”所以返回1。str=“abc1234321ab”其中的最长回文子串为“1234321”,所以返回7。

进阶问题:给定一个字符串str,想通过添加字符的方式使得str整体都变成回文字符串,但要求只能在str的末尾添加字符,请返回在str后面添加的最短字符串。

举例:str=“12”在末尾添加“1”之后,str变为“121”,是回文串。在末尾添加“21”后,str变为“1221”也是回文串。但“1”是所有添加方案中最短的。所以返回“1”。

要求:如果str的长度为N,解决原问题和进阶问题的时间复杂度都达到O(N).

解决问题的方法叫做Manacher算法。Manacher算法解决的问题是在线性时间内找到一个字符串的最长回文子串。

首先来一个很好理解的方法。从左到右遍历字符串,遍历到每个字符串的时候,都看看以这个字符作为中心能够产生多大的回文字符串。比如str=“abacaba”,以str[0]=='a'为中心的回文字符串最大长度为1,以str[1]=='b'为中心的回文字符串最大长度为3,......其中最大的回文子串是以str[3]=='c'为中心的时候。这种方法非常容易理解,只要解决奇回文和偶回文宣召方式的不同就可以,比如‘121’是奇回文,有确定的轴‘2’。‘1221’是偶回文,没有确定的轴,回文的虚轴在“22”中间。但是这种方法有明显的问题,之前遍历过的字符完全无法知道后面遍历的过程,也就是对每个字符串来说都是从自己的位置出发,往左右两个方向扩出去检查。这样对每个字符来说,往外扩的代价都是一个级别的。举一个极端的例子“aaaaaaaaaaaaaaaaaaaa”对每一个‘a’来讲,都是扩到边界才停止。所以每个字符扩出去检查的代价都是O(N),总的时间复杂度是O(N^2)Manacher算法可以做到O(N)的时间复杂度,精髓是之前字符的“扩过程”。下面是Manacher算法解决原问题的过程:

    1.因为奇回文和偶回文在判断时比较麻烦,所以对str进行处理,把每个字符开头结尾和中间插入一个特殊字符来得到一个新的字符串数组(这里用‘#’处理)比如str=“babba”处理后为“#b#a#b#a#a#”然后从每个字符左右扩出去的方式找到最大回文子串就方便多了。对奇回文来说,不这么处理也能通过扩的方式找到,比如“bcb”,从‘c’开始向左右两侧扩出去能找到最大回文,处理后为“#b#c#b#”从‘c’开始向左右两侧扩出去依然能找到最大回文。对偶回文来说,不处理而直接通过扩的方式是找不到的。比如“aa”因为没有确定的轴,但是处理后为“#a#a#”就可以通过从中间的“#”扩出去的方式找到最大回文,所以这样的处理方式不管是偶回文还是奇回文,都可以通过统一的“扩”过程找到。解决了差异性的问题。

具体处理过程如下:

public char[] manacherString(String str) {
		char[] charArr=str.toCharArray();
		char[] res=new char[str.length()*2+1];
		int index=0;
		for (int i = 0; i < res.length; i++) {
			res[i]=(i&1)==0?'#':charArr[index++];
		}
		return res;
	}

2.假设str处理之后的字符串记为charArr。对每个字符(包括特殊字符)都进行“优化后”的扩过程。在介绍“优化后”的扩过程之前,先解释如下三个辅助变量的意义。

    (1)数组pArr。长度与charArr长度一样,pArr[i]的意义是以i位置上的字符(charArr[i])作为回文中心的情况下,扩出去得到的最大回文半径是多少,举个例子来说明,对‘#c#a#b#a#c#’来说,pArr[0...9]为[1,2,1,2,1,6,1,2,1,2,1].我们整个过程就是在从左到右遍历的过程中,依次计算每个位置的最大回文半径值。

    (2)整数pR。这个变量的意义是之前遍历的所有字符的所有回文半径中,最右即将达到的位置。还是以‘#c#a#b#a#c#’来说,还没遍历之前pR,初始设置为-1.charArr[0]==‘#’的回文半径为1.所以目前回文半径向右只能扩展到位置0.回文半径最右即将到达的位置变为1(pR=1).charArr[1]=='c'的回文半径为2,此时所有的回文半径能够扩到位置2,所以回文半径最右即将到达的位置变为3(pR=3)charArr[2]=='#'的回文半径为1,所以位置2向右只能扩展到位置2,回文半径最右即将到达的位置不变。换句话说,pR就是遍历过的所有字符中向右扩出来的最大右边界。只要右边界更往右,pR就更新。

    (3)整数index。这个变量表示最近一次pR更新时,那个回文中心的位置,以刚刚的例子来说,遍历到charArr[0]时,pR更新,index就更新为0.便利到charArr[1]时,pR更新,index就更新为1......遍历到charArr[5]时,index更新为5.之后的过程中,pR将不再更新,所以index一直都是5.

3.只要能够从左到右依次算出数组pArr每个位置的值,最大的那个值实际上就是处理后的charArr中的回文半径,所以根据最大的回文半径,再对应回源字符串的话,整个问题就解决了,步骤三就是从左到右依次计算出pArr数组每个位置的值得过程。

    (1)假设现在计算到位置i的字符charArr[i],在i之前位置的计算过程,都会不断地更新pR和index的值,即位置i之前的index这个回文中心扩出了一个目前最右的回文边界pR。

    (2)如果pR-1位置没有包住当前的i位置,比如‘#c#a#b#a#c#’计算到charArr[0]==‘#’时,pR为1.也就是说右边界在1位置,1位置为左右回文半径即将到达但还没有到达的位置。所以当前的pR-1位置没有包住当前的i位置,此时和普通做法一样,从i位置开始,向左右两侧扩出去检查,此时的扩过程并没有加速。

    (3)如果pR-1位置包住了当前的i位置,比如“#c#a#b#a#c#”计算到charArr[6...10]时,pR都为11,此时pR-1包住了位置6~10.这种情况下,检查过程是可以获得优化的。这也是manacher算法的核心内容。

4.按照步骤3,的逻辑从左到右计算出pArr数组,计算完成后再遍历依次pArr数组。找出最大的回文半径,假设位置i的回文半径最大,即pArr[i]==max.但max只是charArr的最大回文半径,还得对应回原来的字符串,求出最大回文半径的长度(max-1).如“121”处理成charArr之后为“#1#2#1#”在charArr中,位置3的回文半径最大,最大值为4,对应源字符串的最大回文子串长度为4-1=3。

时间复杂度分析:

Manacher算法的时间复杂度为O(N),算法的关键之处在于估算扩出去检查这一行为发生的数量。原字符串处理过后长度由N变为2N,从步骤3可以看出,要么在计算一个位置的回文半径时完全不需要扩出去检查,要么每一次扩出去检查都会导致pR变量的更新。所以扩出去检查的次数就是O(N)。

代码如下:

    

	public int maxLcpsLength(String str) {
		if(str==null||str.length()==0)
			return 0;
		char [] charArr=manacherString(str);
		int [] pArr=new int[charArr.length];
		int index=-1;
		int pR=-1;
		int max=Integer.MIN_VALUE;
		for (int i = 0; i !=charArr.length; i++) {
			pArr[i]=pR>i?Math.min(pArr[2*index-i], pR-i):1;
			while (i+pArr[i]<charArr.length&&i-pArr[i]>-1) {
				//type type = (type) i+pArr[i]<charArr.length.nextElement();
				if (charArr[i+pArr[i]]==charArr[i-pArr[i]]) {
					pArr[i]++;
				}else {
					break;
				}
				if (i+pArr[i]>pR) {
					pR=i+pArr[i];
					index=i;
				}
				max=Math.max(max, pArr[i]);
			}
		}
		
		return max-1;
	}

进阶问题:在字符串的最后添加最少字符,使整个字符串都成为回文串,其实就是查找在必须包含最后一个字符的情况下,最长的回文子串是什么。那么值钱不是最长回文子串的部分逆序过来,就是应该添加的部分。比如“abcd123321”,在必须包含最后一个字符的情况下,最长的回文子串是“123321”,之前不是最长回文子串的部分是‘abcd’所以末尾应该添加的部分就是“dcba”那么只要把manacher算法稍作修改就可以。具体改成:从左到右计算回文半径时,关注回文半径最右即将到达的位置(pR),一旦发现已经到达最后(pR==charArr.length),说明必须包含最后一个字符的最长回文半径已经找到。直接退出检查过程,返回该添加的字符串即可。

代码如下:

public String shortestEnd(String str) {
		if (str==null||str.length()==0) {
			return null;
		}
		char[] charArr=manacherString(str);
		int [] pArr=new int [charArr.length];
		int index=-1;
		int pR=-1;
		int maxContainsEnd=-1;
		for (int i = 0; i < charArr.length; i++) {
			pArr[i]=pR>i?Math.min(pArr[2*index-i], pR-i):1;
			while (i+pArr[i]<charArr.length&&i-pArr[i]>-1) {
				//type type = (type) i+pArr[i]<charArr.length.nextElement();
				if (charArr[i+pArr[i]]==charArr[i-pArr[i]]) {
					pArr[i]++;
				}else {
					break;
				}
				if (i+pArr[i]>pR) {
					pR=i+pArr[i];
					index=i;
				}
				if (pR==charArr.length) {
					maxContainsEnd=pArr[i];
					break;
				}
			}
		}
		char[]res=new char[str.length()-maxContainsEnd+1];
		for (int j = 0; j < res.length; j++) {
			res[res.length-1-j]=charArr[j*2-1];
		}
		return String.valueOf(res);
		
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值