一篇搞定求回文问题的算法天花板

Manacher算法

manacher 算法主要解决的就是 最长回文子串长度的问题。但凡涉及到求回文串方面的,manacher算法就是这方面的天花板算法。

下面,就以求最长回文子串长度作为问题,来解释何为manacher

什么是回文

回文简单来说,就是正着念和反着念是一样的。

例如字符串 121, 相当于就是2作为分割线,往俩边扩,扩的时候俩边字符相等

例如字符串1221,相当于在俩个2之间有一条虚拟的分割线,往俩边扩,扩的时候俩边字符相等

暴力方式

知道了什么是回文之后,如何通过暴力方式来求最长回文子串的长度呢?

假设给定的字符串是 a b 1 2 1 k f,它的最长回文子串长度是3。

对于每个位置来说,都作为分割线的情况下,都尝试同时向俩边扩,看看最远能扩多远。

例如a字符,它左边没有东西,右边是b,扩不动,所以a作为分割线的情况下,能够形成的最长回文长度是1。

再看看b字符,它能形成的也是1。

依次类推,最后,每个位置都会有一个答案,那么全局最大就是最长回文子串长度。

但是,注意,如果字符串是 a b 1 2 2 1 k f,最长回文子串长度是偶数的话,那上面的方法就行不通了。

所以,在这种情况下,就对字符串进行加工,变成:

# a # b # 1 # 2 # 2 # 1 # k # f #

加工成了这种字符串的话,那就可以采用上述的方式,去暴力求每个位置作为分割线的情况下,得到的答案了。

因为加工了,所以最后得到的答案再除以2,就是原先未加工之前字符串的最长回文子串的长度了。

前置概念

上述的暴力方式时间复杂度是0(N^2),而manacher算法的时间复杂度是0(N)

不管是暴力方法还是manacher算法,都要对字符串进行加工,在加工的基础上进行操作。

在具体解析manacher算法之前,需要知道以下几个前置概念

回文半径和回文直径

例如字符串 a b 1 2 1 k f 1 2 2 1

其中,有回文子串 121,它的回文直径就是3,回文半径就是2

还有回文子串1221,它的回文直径就是4,回文半径就是2

回文半径数组

对于加工后的字符串来说,每个位置作为分割线的情况下,最远能够扩多远,形成的回文串的回文半径,记在数组里。这就是回文半径数组。

最右回文边界

一个int类型,命名为R,初始是-1。

表示的意思就是,每来到一个位置,如果它能扩出去的长度大于了R的话,那R就变成当前位置扩出去的最右边界

例如: # 1 # 1 # 2 # 1 # 1 # k #

刚开始,R = -1

来到0位置,扩不出去,它形成的回文串的右边界就是0,发现大于R,那R = 0

来到1位置,形成回文串右边界是2,发现大于R,那R = 2

依次类推…

当来到字符为2的位置的时候,它扩的最远,R直接到了11位置

对于从字符2的位置的后续来说,它们所形成的回文串,最远也不可能超过R了,所以R不会变化

最右回文边界的触发下标

一个int类型,记作C。

会随着R的改变而改变。

简单来说,它就是哪个下标尝试扩的时候,把R改变了,那就用C记录下这个下标

例如上述的例子当中,发现0位置把R改变了,那么C = 0

1位置把R改变了,那么C = 1

Manacher算法的实现

有了前置的几个概念,下面就是阐述manacher算法了。

当前来到了i位置,根据i位置是否被R包括来分为俩大情况。

没被R包括

没被R包括,就是 i > R

这种情况,没有任何技巧。就是i位置分割线,然后往左右俩边扩,看看能扩多远

被R包括

被R包括,也就是 i <= R

这种情况,说明i位置的字符必然根据C有个对应的ii下标,这个ii下标的字符等于i位置的字符,并且也是在R以内的。

另外,R位置的字符根据C,也有个对应的L,L位置字符等于R位置的字符,以C作为中间点。

所以,就会形成如下大概的样子

L …ii … C … i … R

在这种大情况下,就可以根据不同小情况,来加速获取i位置形成的回文子串长度了。

ii形成的回文串的左边界是大于L的

例如字符串:

a b c d c k s t s k c d c b a

L ii C i R

上面例子可以看到,当前求i位置能够形成回文长度有多长的时候,发现对应映射的ii位置的长度是3,也就是cdc,这个可以直接从回文半径数组当中直接拿到了。并且回文串的左边界是大于L的。

那么,在这种情况下,其实i位置的回文长度就等于ii位置的回文长度。

ii形成的回文串的左边界小于L的

例如字符串:
在这里插入图片描述

可以发现,i对应的ii位置所形成的回文串左边界已经越过了L。

在这种情况下,其实i位置能够形成的回文长度的回文半径就是i到R的距离

ii形成的回文串的左边界等于L

例如:
在这里插入图片描述

等于L,其实就是ii位置形成的回文串的左边界刚好压到的是L位置

在这种情况下,对于i位置能够形成的回文串,对于这个回文串来说,至少的回文半径是i到R位置的距离。

然后,在这基础之上,再尝试往左右俩边去扩,看看能不能扩的更远,同时注意更新R和C。

总结

总体分为了俩大情况,第二大情况里面,又根据i位置对应的映射下标ii位置所形成的回文串的左边界和L位置的关系,又分为了三个小情况。

在求出了回文半径数组之后,那么最长回文子串的长度就可以直接根据这个数组拿到了。

代码实现

public static int manacher(String s) {
    String manacherStr = getManacherString(s);
    char[] str = manacherStr.toCharArray();
    int[] preArr = new int[str.length];
    int C = -1;

    // 这里是R指的是最右的回文边界的下一个位置,方便coding
    int R = -1;

    int max = Integer.MAX_VALUE;

    for (int i = 0; i < preArr.length; i++) {
        // R-1其实才是此时最右的回文边界
        // R大于i,必然i是被扩住的,根据扩住的情况,知道当前i位置的回文半径至少长度
        // 要么是对称的的那个下标对应的回文长度,要么就是i到R位置的距离

        // 如果R <= i ,说明没有扩住,那起码回文半径长度是1
        preArr[i] = R > i ? Math.min(preArr[2 * C - i], R - i) : 1;

        // 尝试向俩边扩
        while (i + preArr[i] < str.length && i - preArr[i] >= 0) {
            if (str[i + preArr[i]] == str[i - preArr[i]]) {
                preArr[i]++;
            }
            else {
                break;
            }
        }

        if (i + preArr[i] > R) {
            R = i + preArr[i];
            C = i;
        }

        max = Math.max(max, preArr[i]);
    }

    // 最大的回文半径长度-1就是原本字符串的最长回文子串的长度了
    return max - 1;
}
ak;
            }
        }

        if (i + preArr[i] > R) {
            R = i + preArr[i];
            C = i;
        }

        max = Math.max(max, preArr[i]);
    }

    // 最大的回文半径长度-1就是原本字符串的最长回文子串的长度了
    return max - 1;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值