Manacher 算法——Leetcode 5.最长回文子串

在了解之前,我们先要了解什么是回文串,什么是回文子串。

回文串和回文子串:

        回文串是指一个字符串正序遍历和反向遍历结果相同的字符串。如 ABBA,正着读反着读结果是一样的。

有了回文串的概念,回文子串的概念也就显而易见了。也就是一个字符串的子字符串是回文串,则该子字符串被称为回文子串。

有了这些概念,我们就能介绍Manacher算法。在我们看实际例题前,我们先来看看什么是Manacher算法。

Manacher算法介绍:

Manacher算法是一个用来查找一个字符串中的最长回文子串(不是最长回文序列)的线性算法。它的优点就是把时间复杂度为O(n^2)的暴力算法优化到了O(n)。

既然是一个寻找字符串的最长回文子串,我们就能想到一种暴力的解法:通过从一个字符串中每个字符的位置开始,向两边扩展,一次一位。当左右指针相对应的字符相同时,那么我们就初步得到了一个回文子串。当我们对整个字符串都进行这样的操作时,我们就能得到最长的回文子串。

这样做的时间复杂度是O(n^2),也是我们想要优化的对象。

下面放出例题和我们通过暴力的解法的代码:

例题:

给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

输入:s = "cbbd"
输出:"bb"

提示:

  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成
代码:
class Solution {
    public String longestPalindrome(String s) {
        int len = s.length();
        String max = s.substring(0,1);
        for(int i=0;i<len-1;i++){
            String odd = new String();
            String even = new String();
            if(s.charAt(i)==s.charAt(i+1)) even = extend(s,i,i+1);
            odd = extend(s,i,i);
            if(odd.length() > max.length()) max = odd;
            if(even.length() > max.length()) max = even;
        }
        return max;
    }
    public String extend(String s,int l,int r){
        int len = s.length();
        String ans = new String();
        while(l>=0 && r<s.length()){
            if(s.charAt(l) == s.charAt(r)){
                if(l==r) ans+=s.charAt(l);
                else{
                    ans = s.charAt(l)+ans;
                    ans = ans + s.charAt(r);
                }
                l--;
                r++;
            }
            else{
                break;
            }
        }
        return ans;
    }
}

当我们遇到ABA这样的字符串是,我们直接扩展得到的结果是正确的131。但是当遇到ABBA这样的偶数长度是,我们得到的结果却是1111,显然这样是错误的。因此,我们对此专门做了分类讨论:

当此处的字符与下一个相同时,我们扩展是从这两个同时扩展的。也就是解决了偶数的问题。‘

当此处的字符与下一个不相同的时候,我们将会从当前的字符直接开始扩展,也就是奇数的问题。

这样做虽然解决了找寻最长回文子串的问题,但是我们会发现每次向外扩展就是O(N)的时间复杂度。每次从一个字符开始向外扩展,也就是一共花费O(N^2)的时间复杂度。

那么有没有更好的方法呢?下面来介绍Manacher算法

Manacher算法:

首先我们要解决的是将麻烦的奇偶情况分类讨论的问题。我们显然可知,偶数的情况相较于奇数的情况更加复杂,因为奇数只需要从此处开始向两边扩展即可。因此我们要想办法将偶数串变成奇数串,同时不影响判断。所以我们需要预处理。

预处理:

假设一个字符串长度为n,无论n是奇是偶,2*n+1的结果一定是奇数,这是毋庸置疑的。所以我们只需将原字符串长度变成2n+1即可。在不影响原串的前提下,我们需要插入n+1个无关字符。插在哪里不影响呢?当然是间隙之间。比如插入'*' ,'$' ,'#' 这种无关字符。这样一来,我们就得到了一个奇数长度的字符串。

Manacher算法核心:

为了介绍Manacher算法,我们在此需要引入几个概念:

Manacher字符串:经过Manacher预处理的字符串
回文半径:回文字符串的中心字符到两端距离。

最右回文边界R:在遍历字符串时,每个字符遍历出的最长回文子串都会有个右边界,而R则是所有已知右边界中最靠右的位置,也就是说R的值是只增不减的。

回文中心C:取得当前R的第一次更新时的回文中心。由此可见R和C时伴生的。

半径数组:这个数组记录了原字符串中每一个字符对应的最长回文半径。

## 此处因为Manacher字符串长度是 2n+1 ,因此它的半径数组记录的其实是原字符串回文子串的回文直径,也就是回文子串的长度。

初始化 R C均为-1,然后从0处遍历字符串。同时创建半径数组。这里有点与概念相差的小偏差,就是R实际是最右边界位置的右一位。因为在计算时R包含了C的位置,导致pArr[i]+i重复计算了一次i的位置。

这时会遇到三种情况:

此处参考了 https://www.cnblogs.com/cloudplankroader/p/10988844.html 的图片和思想。

1️⃣ i > R ,也就是i在R外,此时没有什么花里胡哨的方法,直接暴力匹配,此时记得看看C和R要不要更新。

2️⃣ i <= R,也就是i在R内,此时分三种情况,在讨论这三个情况前,我们先构建一个模型

L是当前R关于C的对称点,i'是i关于C的对称点,可知 i' = 2*C - i,并且我们会发现,i'的回文区域是我们已经求过的,从这里我们就可以开始判断是不是可以进行加速处理了

情况1:i'的回文区域在L-R的内部,此时i的回文直径与 i' 相同,我们可以直接得到i的回文半径,下面给出证明

红线部分是 i' 的回文区域,因为整个L-R就是一个回文串,回文中心是C,所以i形成的回文区域和i'形成的回文区域是关于C对称的。

情况2:i'的回文区域左边界超过了L,此时i的回文半径则是i到R,下面给出证明

 

首先我们设L点关于i'对称的点为L',R点关于i点对称的点为R',L的前一个字符为x,L’的后一个字符为y,k和z同理,此时我们知道L - L'是i'回文区域内的一段回文串,故可知R’ - R也是回文串,因为L - R是一个大回文串。所以我们得到了一系列关系,x = y,y = k,x != z,所以 k != z。这样就可以验证出i点的回文半径是i - R。

情况3:i' 的回文区域左边界恰好和L重合,此时i的回文半径最少是i到R,回文区域从R继续向外部匹配,下面给出证明

因为 i' 的回文左边界和L重合,所以已知的i的回文半径就和i'的一样了,我们设i的回文区域右边界的下一个字符是y,i的回文区域左边界的上一个字符是x,现在我们只需要从x和y的位置开始暴力匹配,看是否能把i的回文区域扩大即可。

总结一下,Manacher算法的具体流程就是先匹配 -> 通过判断i与R的关系进行不同的分支操作 -> 继续遍历直到遍历完整个字符串

 简单来说,我们需要在暴力算法的基础上利用到以前的结果。如果i<R,那么说明还在范围内,由之前的结果作参考,遍历的范围小。否则只能从1开始逐步遍历。

class Solution {
    public String longestPalindrome(String s) {
        int len = s.length();
        char[] tchs = s.toCharArray();
        char[] chs = new char[tchs.length*2+1];
        int idx = 0;
        for(int i=0;i<chs.length;i++){
            chs[i] = (i & 1) == 1 ? tchs[idx++] : '#';
        }
        int[] pArr = new int[chs.length];
        int R = -1;
        int C = -1;
        int max = Integer.MIN_VALUE;
        int index = -1;
        for(int i=0;i<chs.length;i++){
            pArr[i] = i < R ? Math.min(pArr[2*C-i],R-i) : 1;
            while(i-pArr[i] >= 0 && i+pArr[i]<chs.length){
                if(chs[i+pArr[i]] == chs[i-pArr[i]]){
                    pArr[i]++;
                }else{
                    break;
                }
            }
            if(i+pArr[i] > R){
                R = i + pArr[i];
                C = i;
            }
            if(pArr[i] >= max){
                max = pArr[i];
                index = i;
            }
        }
        String ans = new String();
        for(int i=index-max+1;i<=index+max-1;i++){
            if(chs[i] != '#') ans += chs[i];
        }
        return ans;
    }
}

当我们的 i 还在最右侧的内部时,我们只需要从

Math.min(pArr[2*C-i],R-i)

中的最小值开始遍历就行了,很好理解,此处不展开。就是上面的3种情况。否则只能从1重新开始。遍历结束后,如果现有的范围已经超过原R,扩展R同时改变C为当前的C。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值