刷题基础----Manacher算法求解最大回文子串

来源:互联网 发布:成都怎么样知乎 编辑:程序博客网 时间:2024/06/01 10:26

      刷题基础----Manacher算法求解最大回文子串

    和字符串相关的算法题是面试中常考的题型,包括但不限于:1.判断给定的字符串是否是回文字符串  2.求解给定的字符串的最长回文子串。

    本篇文章重点介绍如何求解一个给定字符串的最长回文子串,并介绍了两种解法的原理以及java实现代码。

    问题描述:任意给定一个字符串S,返回该字符串中的最长回文子串,例如输入字符串”fcbbca”,输出的最长回文子串为cbbc。回文子串是一种特殊的字符串,其从左往右读的结果等于其从右往左读的结果。

解题思路

    1.穷举出该字符串的所有子串,一一比较子串是否是回文子串。这种方式时间复杂度是O(n3),在字符串很长的时候,时间肯定会溢出,不能采用穷举的方式解决该问题

    2.方法一是从寻找子串的角度来考虑问题,找到所有的子串,然后判断是否回文,在判断的过程中记录最大的回文。这种方法做了很多无用的工作,因为字符串的子串不都是回文串,我们并不需要列举出所有的子串,我们只需要找到回文子串,并且比较长度即可。该方法从回文子串的角度来考虑问题,提出了一种时间复杂度在O(n2)的解法。其算法思想如下:

    回文子串字符串依据长度可分为:

    (1)奇数长度的回文字符串“bafab”,该回文字符具有一个对称轴

    (2)偶数长度的回文字符串“baab”,该回文字符串将两个数当成对称轴

    基于上述回文字符串的特点,我们可将定字符串中的每个字符或每两个字符看成对称轴,往两变扩展,找到该对称轴下的最长回文子串。

    对于字符串“bafab”而言:

    (1)对称轴为b时,最长回文子串是“b”

    (2)对称轴为“ba”时,此时无回文子串

    (3)对称轴为“a”时,此时回文子串是”a”

    (4)对称轴为“af“时,此时无回文子串

    (5)对称轴为“f”时,此时回文子串为“bafab”

    (6)依次类推,分别将对称轴设为“fa”,“a”,“ab”,”b”,求解这种情况下的回文情况,最后输出最长回文子串即可。该算法的java实现代码如下:

    //获取字符串的最长回文字串,时间复杂度为O(n2)

publicstatic StringgetSubString(String s){       intmaxLength=0;       String result="";       for(int i=0;i<s.length();i++){           //以奇数为中心扩展的回文字符串           int left =i-1;           int right =i+1;           int count=0;           while(left>=0&&right<s.length()&&               s.charAt(left)==s.charAt(right)){              count++;              left--;              right++;           }           maxLength=count;           if(count!=0)              result =s.substring(left+1, right);           //以偶数为中心的扩展回文字符串           left=i;           right =i+1;           count=0;           while(left>=0&&right<s.length()           &&s.charAt(left)==s.charAt(right)){              count++;              left--;              right++;           }           if(count>maxLength){              maxLength=count;              result = s.substring(left+1,right);           }       }       returnresult;}

3.利用Manacher算法求解输入字符串中的最长回文子串,方法2可以在时间复杂度为O(n2)的情况下,求解输入字符串的最长回文子串,但是在面对长度较大的字符串时O(n2)的时间复杂度还是不能够接受的,Manacher算法是对上述算法的改进,可在O(n)的时间复杂度下解决问题。

    观察字符串“abkba cbfbc abkba”(忽略字符串中的空格),假如利用方法2来解决该问题:

    (1)在遍历到前面的abkab中的k时,以f为对称轴时,此时双向扩展,得到的回文子串“abkba”,算法会记录下该回文子串,并继续执行下去.

    (2)在遍历到abkba cbfcb中的k时,以f为对称轴时,此时双向扩展,得到的回文子串“abba  cbfbc abkba“,以对称轴f为界其左右两侧的序列是完全一样的,并且对称轴左侧任意一点的回文半径和其对称点的回文半径是一样的  (            abkba序列以k为对称轴时回文半径是3,”a“序列的回文半径是1        )

    (3)此时方法2的执行并没有终止,其会继续遍历,当遍历到”abkba“序列中的k时,此时其依然会双向扩展,但是我们发现这是不必要的消耗,因为k处在一个更大的回文字符串中,k的回文半径和其对称点的回文半径是一样的。

    Manacher算法正是在方法二基础上,利用了回文序列的对称性,减少了重复计算回文半径的过程,将时间复杂度压缩到了O(n)。下面详细的介绍,如何利用Manacher算法求解字符串的最长回文子串。

    (1)Manache通过对称性,减少对回文半径的重复计算,所以Manacher算法处理的字符串应该是奇数长度的字符串,所以算法的一步是对字符进行填充处理。例如“cbbc“,填充后为”#c#b#b#c#“,这样可以保证,在不改变原有回文性的基础上,将对称轴都转换成了一个。

      (2)定义三个辅助变量,mx,id 和p数组:

      id:用来记录当前回文子串的对称轴位置,id初始化为0。

      mx:用来记录当前回文子串的右边界 ,mx初始化为0。

      p数组:用来记录以当前字符串为对称轴时的回文半径,p[i] =3,代表以第i个字符为对称轴时的回文半径是3.

      (3)依次顺序遍历字符串,计算当前字符串的p[i]值,以下图所示的状态为例

      

if(mx>i) {// i是当前遍历字符的下标,如果i在回文边界中利用对称性  p[i]= Math.min(p[2*id-i],mx-i);//i*id-i是坐标i关于id对称的坐标点}else{      //如果不在回文边界中,怎利用两边的扩展的方式求解  p[i]=1;}

关于上述代码可能会有疑问了,为什么要取最小值:p[i] = Math.min(p[2*id-i],mx-i)

如图所示,黑色箭头代表对称位置,红色代表遍历位置,id代表对称轴,max代表边界


假如红色箭头i处的对称位置:p[2*id-i]<mx-i,代表红色箭头所指的回文子串在mx范围的内部,可以利用对称性,红丝箭头的回文和黑色箭头的回文是镜像的关系。

但是假如红色箭头所指的回文超过了mx的范围,那么只能保证mx范围之内的部分具有对称性,其最长可利用的对称长度为mx-i。

(4)在遍历的过程中,mx和id的值需要不断的改变,保证指向最右侧的回文范围:

if(mx<i+p[i]){    mx =p[i]+i;    id=i;}

(5)遍历完成后,求解出所有p[i],找到最大回文半径,然后输出。

其完整的java代码实现如下。

   //利用Manacher算法求解最大回文子串    publicstatic StringgetSubString1(String s){       String result ="";       //最大回文字串的覆盖范围       intmax =0;       //最大回文子串的中心坐标       intid=0;       //1.manacher 算法的预处理       StringBuilder processStr =new StringBuilder();       processStr.append("$#");       for(int i=0;i<s.length();i++){           processStr.append(s.charAt(i)+"#");       }       //processStr.append("&");       //2.manacher 算法的遍历过程       result = processStr.toString();       int[] p =new int[result.length()-1];       for(int i =1;i<result.length();i++){           //如果max覆盖了当前的i           if(max>i){              p[i-1] = Math.min(p[2*id-i-1],max-i);           }else{              p[i-1] = 1;           }           while((i+p[i-1])<result.length()&&(i-p[i-1]>=0)&&                  result.charAt(i+p[i-1])==result.charAt(i-p[i-1])){              p[i-1]++;           }           if(max<i+p[i-1]){              max = i+p[i-1];              id = i;}       }       max =0;       id=0;       for(int i=0;i<p.length;i++){           if(p[i]>max){              max =p[i];              id= i;           }       }       intleft = 1+id-max+1;       intright =1+id+max-1;       StringBuilder finalStr = new StringBuilder();       for(int i =left;i<=right;i++){           if(result.charAt(i)!='#'){              finalStr.append(result.charAt(i));           }       }       returnfinalStr.toString();   }