manacher算法(马拉车)

manacher算法

首先我们先来看一个问题:求出字符串的最长回文子串或者它的长度,比如:"cbabfd"的最长回文子串就是"bab"它的长度为3.

来看一个暴力的解决方法:中心拓展法

回文子串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断字符串是否相等就可以了。

但由于存在奇数和偶数的字符串,所以我们需要从一个字符串开始扩展,或者从两个字符串中间开始扩展,总共有 n + n - 1个中心。

来看下代码的实现:

String longestPalindrome(String s) {
    if (s == null || s.length() < 1) return "";
    int start = 0, end = 0;
    for (int i = 0; i < s.length(); i++) {
        int len1 = expandAroundCenter(s, i, i); //从一个字符扩展
        int len2 = expandAroundCenter(s, i, i + 1); //从两个字符之间扩展
        int len = Math.max(len1, len2);
        //根据 i 和 len 求得字符串的相应下标
        if (len > end - start) {
            start = i - (len - 1) / 2;
            end = i + len / 2;
        }
    }
    return s.substring(start, end + 1);
}

int expandAroundCenter(String s, int left, int right) {
    int L = left, R = right;
    while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
        L--;
        R++;
    }
    return R - L - 1;
}

时间复杂度:O(n ^ 2),两层循环,每层循环都是遍历每个字符。

空间复杂度:O(1)。

如果要降低时间复杂度,该怎么办呢? Manacher's Algorithm马拉车算法登场。

马拉车算法 Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,由一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性。

接下来我们开始学习这个巧妙的算法。

处理字符串

由于字符串可能是奇数可能是偶数,而为了最终的代码统一,我们可以将其全部转换为奇数。方法就是在两个数字之间插入"#"号,插入"#"的个数用于是原字符串的长度-1。

如果原来是偶数,那么加入的"#"号是奇数;偶 + 奇 = 奇;

如果原来是奇数,那么加入的"#"号是偶数;奇 + 偶 = 奇;

就此我们就已经统一了字符串的奇偶性。

然后,为了后续中心扩展的时候不越界,我们可以在头部插入"$",尾部插入"^",(现在字符串里面没有的符号就都可以)。这样当我们扩展到头部和尾部的时候,不可能和字符串中的其他字符相同,就可以结束循环。

(找来的图是头部插入"^",尾部插入"$",只不过如同上文所说,这两个符号并没有实际意义,只要符合条件都可以)

求p[i]

p[ i ]:以i为中心的最长回文子串的长度

接下来就是算法的关键了,充分利用了回文串的对称性,也是马拉车能在线性时间内得到答案的原因。

我们用center表示当前最右边界的的对称中心,用max_right表示当前达到的最右边界,而 i 就是当前遍历到的点。

我们现在要求p[i],如果是用中心扩展法(暴力法),那就向两边扩展对比就可以了,但马拉车算法利用了回文字符串的对称性。

来看看下面几种情况,大概就明白为什么说利用了回文字符串的对称性了。

先预告一下,关键公式 : p[i] = min (max_right - i,p[2 * center - i])

情况 1.0: i >= max_right

当出现这种情况的时候,由于当前 i 的回文子串和之前的已经判断的最长回文子串没有关系,所以无法利用回文子串的对称性,需要继续暴力中心扩展。

(因为回文子串是判断 i 的两边的字符,所以即使 i == max_right 时,也对判断无帮助)

情况 2.0: i < max_right

当前,我们已经得到了p[0...i-1]。那么如果 i 在max_right的左边就可以利用我们已经计算出来的p[ j ]来计算 p[ i ];

补充一下,j 是 i 关于当前center的对称点。

这时又要分为两种情况

情况 2.1: i + p[j] <= max_right

此时 p[ i ] = p[ j ];

为什么呢?

因为以 center为中心的最长回文子串,包括了以 j 为对称中心的最长回文子串 和 i,而且 i 和 j 以center为中心对称,那么一定有p[ i ] = p [ j ]

情况2.2: i + p[ j ] > max_right

在这种情况下,只能让p[ i ] = max_right - i 了,因为我们只能保证半径到max_right这个位置是可以回文的,但是超过右边的部分我们不能判断,只能继续用中心扩展方法(暴力法)来得到最长的回文子串。

总结一下:

  • i >= max_right; 直接继续暴力
  • i < max_right 且 i + p[ j ] <= max_right; p[ i ] = p[ j ]
  • i < max_right 且 i + p[ j ] > max_right; p [ i ] = max_right - i,然后继续暴力

考虑了,i 在center的最长子串里面,和被center的最长子串全包围和 i + p[ j] 超过了max_right, 你是否会想如果 i - p[j] 还超过了左边max_right的对称点怎么办?

其实并不会有这种情况,因为 i 的对称点 j 的最长子串不会超过 max_right,如果超过了的话,此时的最长子串应该是 j 为中心,半径为 j + p[ j ],所以根本不可能鸭。

情况看起来很多,只不过所有的所有,转换为代码,只有短短的几行:

int j = 2 * center - i;//得到i关于中心点的对称点
if(max_right > i)//在最右边界的覆盖范围内,就利用回文字符串的特性
    p[i] = min(max_right - i,p[j]);//min的第一个对应i + p[j] > max_right,第二个对应 i + p[j] <= max_right
else
    p[i] = 0;

顺便看下待会要进行的暴力代码

//暴力向两边扩展
while(t[i - 1 - p[i]] == t[i + 1 + p[i]])
     p[i]++;

马拉车算法的优化就在于,直接省略了一些点的部分回文子串的判断,这样暴力就不用太暴力了。

比如,当 i + p[ j ] < max_right 的时候p[ i ] = p[ j ],上面这个循环直接就退出了。

更新center 和 max_right

当我们一步一步求p[ i ]的时候,如果p[ i ]的右边界大于当前的max_right的时候,就需要更新center 和 max_right了。因为我们希望 i 尽可能的在max_right里面,所以就要更新啊,直接看代码吧。

if(i + p[i] > max_right){
  //更新最右边界,以及对称中心
  center = i;
  max_right = i + p[i];
}

得到结果

好了,工作结束了,来看下利用我们求的p数组,如何得到正确的解吧。

从图中可以看出,p数组里存储的数字(也就是从中心扩展的最大个数),刚好就是它去掉"#"的原字符串的总长度,比如上图中的p[6] == 5,所以他是从左边扩展5个字符,相应的右边也是扩展5个字符,也就是"#c#b#c#b#c#"。而去掉"#"恢复后就是 cbcbc。(这个叫马拉松的人的观察力真好)。

所以说,我们遍历找到p[i]的最大值,就是最长子序列了。

代码如下:

int max_len = 0;
for(int i = 0; i < len;i++){
  if(max_len < p[i])
    max_len = p[i];
}

如果求长度的话,到此就可以了,那如果是求那个最长的回文子串呢?

也不难,我们只要求对应在原字符串下标,再substr出来就可以了。

来看看如何求原字符串下标吧。

用p 的下标 i 减 p[i] 再除以2,就是原字符串的开头下标了。

例如我们找到p[ i ]的最大值为5,也就是回文串的最大长度为5,在T中对应下标是6,所以原字符串的开头下标是(6 - 5) / 2 = 0,返回 0 到 第 (5 - 0)位就可以了。

int max_len = 0;
int centerIndex = 0;
for(int i = 0; i < len;i++){
  if(max_len < p[i]){
    max_len = p[i];
    centerIndex = i;
  }
}

int start = (centerIndex - max_len) / 2;//最长回文子串的起点
for(int i = start; i < max_len;i++)
  printf("%c",s[i]);

来分析下它O(n)的时间复杂度吧。不严谨的想一下,for里面套了一层while循环,但是很多点可以直接利用对称得到自己的解,不会进入while循环。

如果有错误,麻烦指出,以上是我今天学manaer的总结。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值