Java-Manacher算法

        回文子串:指的是一个字符串正着读和反着读是一样的,就叫回文子串,比如abccba。

        最长回文子串:即一个字符串中最长的回文子串,比如abcwe12344321ewcbdrt。每个字符自身是回文的。

        那么如何得到一个字符串中最长的回文子串长度呢(知道长度和中心位置很容易得到具体的最长子串)?

一、暴力中心拓展方法

        以字符串每个位置的字符为中心向两边扩,能扩到的最大范围就是该字符的最长回文子串,遍历为整个字符串,算出每个位置的最长回文子串,就可以得到整个字符串最长的回文子串。

        比如奇数长度的字符串:abcecba,0位置1,1位置1,2位置1,3位置7,4位置1,5位置1,6位置1。所以整个字符串的最长回文子串长度是7。

                比如偶数长度字符串:abccba,0位置1,1位置1,2位置1,3位置1,4位置1,5位置1,6位置1。所以整个字符串的最长回文子串长度是1。但实际该字符串的最长回文子串长度是6。

我们发现偶数长度的字符串,用中心扩展法的时候,会匹配不到回文串。为了解决此问题,在处理最长回文子串的问题中,我们可以先预处理一下字符串,做一个填充,规避到偶数字符串匹配的问题。填充后的字符串长度一定是奇数;并且填充什么字符都可以,不会影响最后的结果(因为每次比较一定是真实字符两两比较、填充字符两两比较,不会存在真实字符和填充字符比较的时候)

public String fill(String s){
    int N=s.length();
    StringBuffer sb=new StringBuffer();
    for (int i = 0; i < s.length(); i++) {
        sb.append("#");
        sb.append(s.charAt(i));
    }
    sb.append("#");
    return sb.toString();
}

        偶数长度字符串:abccba,经过填充变为:#a#b#c#c#b#a#。此时用中心扩展发,0位置1,1位置3,2位置1,3位置3,4位置1,5位置3,6位置137位置3,8位置1,9位置3,10位置1,11位置3,12位置1。所以整个填充后的字符串的最长回文子串长度是16。实际该字符串的最长回文子串长度是13/2=6。完美解决问题。那么我们来看下暴力中心扩展法的代码。

public String findLongestPalindrome(String s){
    String res=fill(s);
    char[] ch=res.toCharArray();
    int N=res.length();
    int i=0;
    //最长回文子串的中心位置
    int maxC=0;
    //最长回文子串长度
    int maxLen=1;
    while(i<N){
        //i为中心开始拓展的位置
        int L=i-1;
        int R=i+1;
        //拓展不能越界
        while(L>=0&&R<N){
            if(ch[L]==ch[R]){
                L--;
                R++;
            }else{
                //只要不相等就跳出循环
                break;
            }
        }
        //以i为中心的最长回文子串的长度
        int curLen=R-L-1;
        if(curLen>maxLen){
            //更新最长长度
            maxLen=curLen;
            //更新中心位置
            maxC=i;
        }
        //循环求下一个位置的最长回文子串长度
        i++;
    }
    return res.substring(maxC-maxLen/2+1,maxC+maxLen/2).replaceAll("#","");
}  

5. 最长回文子串 - 力扣(LeetCode)测试可过。

二、Manacher算法

        Manacher算法的整体过程和暴力中心扩展算法是一样的,只不过在中间求解每个位置的最长回文子串长度的时候有优化加速。

        我们再看一下上面的例子。 偶数长度字符串:abccba,经过填充变为:#a#b#c#c#b#a#。此时用中心扩展发,0位置1,1位置3,2位置1,3位置3,4位置1,5位置3,6位置137位置3,8位置1,9位置3,10位置1,11位置3,12位置1。可以发现关于6位置对称的位置,其回文子串长度相等。说明肯定有一些规律在里面(看完Manacher算法就知道确实存在规律,存在何种规律)。下面我们就看下Manacher算法的步骤。

        1、了解定义

        回文右边界R:当前最长回文字符串拓展到的最远右边界。

        回文中心C:一定是某个位置的最长回文子串将回文右边界拓展到了当前最远,那这个位置就是回文中心(回文中心和回文右边界同步更新)。

        回文半径:以i为中心的最长回文字符串长度的一半+1,比如aba,b的回文半径是3/2+1=b到左侧a包含的字符个数=b到左侧a包含的字符个数=2。

        回文半径数组:p[i],i位置的回文半径

        2、如何求某个位置的(最长)回文半径

        a、i位置在回文右边界外,暴力中心扩展

        b、i位置在回文右边界里(那么一定有C<i<R这样的位置关系)

                b1、情况一,i关于C对称的左侧位置i`的回文半径在C的回文半径内部(如下图)。此时i的回文半径i`的回文半径

        怎么证明呢?如图所示,根据i`的回文半径可知,x!=y,根据C的回文半径可知,x=p且y=z。所以z!=p。所以i的回文半径不能在扩展,等于p[i`]。

        b2、情况二,i关于C对称的左侧位置i`的回文半径在C的回文半径外部(如下图)此时i的回文半径p[i]=R-i

       证明:根据i`的回文半径可知,x=y,根据C的回文半径可知,y=z且x!=p,所以可得z!=p。所以i的回文半径不能在扩展,等于p[i`]。

        b3、情况三,i关于C对称的左侧位置i`的回文半径落在C的回文半径边界(如下图)。此时i的回文半径至少是R-i。能不能再扩展,未知。

​​​​

        证明:根据i`的回文半径可知,x!=y,根据C的回文半径可知,y=z且x!=p,所以可得z!=x,但是z和p的关系无法知道,需要进一步去验证,所以i的回文半径至少是R-i

        以上我们就讨论了全部的位置关系,这样就可以依次算出每个位置的回文半径,即可得出最长回文半径和其中心点,然后得到最长回文子串。下面看代码。

public String manacher(String s){
    String res=fill(s);
    int N=res.length();
    char[] ch=res.toCharArray();
    //回文右边界
    int R=-1;
    //回文中心
    int C=-1;
    //回文半径数组
    int[] p=new int[N];
    int i=0;
    //最长回文半径
    int maxLen=0;
    //最长回文半径中心
    int maxC=0;
    while(i<N){
        if(i>=R){
            //i不在回文右边界里,回文半径至少为1,暴力扩
            p[i]=1;
            int iL=i-p[i];
            int iR=i+p[i];
            while(iL>=0&&iR<N){
                if(ch[iL]==ch[iR]){
                    p[i]++;
                    iL--;
                    iR++;
                }else{
                    break;
                }
            }
        }else{
            //i在回文右边界里
            int i_=C-(C-i);//i关于C的对称位置
            if(p[i_]<R-i){
                //对称点的回文半径在C的回文半径内部
                p[i]=p[i_];
            }else if(p[i_]>R-i){
                p[i]=R-i;
            }else{
                p[i]=R-i;
                int iL=i-p[i];
                int iR=i+p[i];
                while(iL>=0&&iR<N){
                    if(ch[iL]==ch[iR]){
                        p[i]++;
                        iL--;
                        iR++;
                    }else{
                        break;
                    }
                }
            }
        }
        //看此时的回文右边界是否被扩展
        if(i+p[i]>R){
            R=i+p[i];
            C=i;
        }
        //更新最长回文半径记录
        if(p[i]>maxLen){
            maxLen=p[i];
            maxC=i;
        }
        i++;
    }
    return res.substring(maxC- maxLen+1 , maxC+ maxLen).replaceAll("#", "");
}     

        上面的代码严格按照情况分支编写的,会发现有一些重复代码(两种需要暴力扩的情况代码重复)。下面看一个精简版的代码。

public String manacher(String s){
    String res=fill(s);
    int N=res.length();
    char[] ch=res.toCharArray();
    //回文右边界
    int R=-1;
    //回文中心
    int C=-1;
    //回文半径数组
    int[] p=new int[N];
    int i=0;
    //最长回文半径
    int maxLen=0;
    //最长回文半径中心
    int maxC=0;
    while(i<N){
        int i_=C-(C-i);
        //i在R外部,i的回文半径至少是1;i在R的内部时,回文半径至少是Math.min(R-i,p[i_])。
        p[i]=i>=R?1:Math.min(R-i,p[i_]);
        //然后我们不去讨论直接暴力扩当前的回文半径
        //根据我们的分析,其中两种情况一定会扩失败
        int iL=i-p[i];
        int iR=i+p[i];
        while(iL>=0&&iR<N){
            if(ch[iL]==ch[iR]){
                p[i]++;
                iL--;
                iR++;
            }else{
                break;
            }
        }
        //看此时的回文右边界是否被扩展
        if(i+p[i]>R){
            R=i+p[i];
            C=i;
        }
        //更新最长回文半径记录
        if(p[i]>maxLen){
            maxLen=p[i];
            maxC=i;
        }
        i++;
    }
    return res.substring(maxC- maxLen+1 , maxC+ maxLen).replaceAll("#", "");
}

        至此,Manacher算法的全部内容就讲完了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值