Manacher算法详细解析 (马拉车算法)

应用场景:

        如求最长回文子串的题目,或者结合其他算法求解算法题目时可以用到。

        求最长回文子串题目:给你一个字符串s,询问s最长回文子串的长度是多少?(如abaa的最长回文子串是aba,长度为3。)

        结合其他算法的题目:

                        Problem M. Mediocre String Problem(马拉车+拓展KMP)

算法解析:

        在讲解Manacher之前,先考虑如何用暴力算法求解最长回文子串题目

        暴力算法思路:其实暴力算法思路很简单,我们只需要利用for循环将字符串从头到尾每个字符进行遍历。遍历时,将每个字符设为回文子串中心点,并朝左右方向同时扩展,扩展到左右字符不相同时,即得出以该字符为回文子串中心时的最大回文子串长度。最后将以每个字符为中心的最大回文子串长度求个最大值,就是最后的结果了。(P.s.: 扩展时要执行俩种扩展,一种是假设回文子串为奇数的扩展, 一种是假设回文子串为偶数的扩展,如cbabc 和 cbaabc, cbabc 中以下标2的a为中心时,比较第2位和第2位、第1位和第3位、第0位和第4位,并以此类推。cbaabc中以下表2的a为中心时,比较第2位和第3位、第1位和第4位、第0位和第5位,并以此类推。)

        例:给字符串 babcbabcbaccba
        每个字符为回文子串中心时的最大回文子串长度序列为:1 3 1 7 1 9 1 5 1 1 1 1 1 1

        最后求最大值,得出的最长回文子串的长度便是9(abcbabcba)。

        暴力算法缺点:时间复杂度O(n*n)过高。

       

        Manacher算法思路:在上述暴力算法中,我们可以得到每个字符为回文子串中心时的最大回文子串长度,简称为臂长。那Manacher算法的核心思路就是利用之前求得的臂长来减少时间复杂度。

        算法预处理:算法首先需要预处理字符串,如将aba 变成#a#b#a#,  将abba变成#a#b#b#a#, 这是利用2*x+1的结果始终为奇数的特性,将原字符串都变为奇数长度,因为这样我们就不用像暴力算法中的扩展一样需要特别考虑长度为偶数的回文子串了,例如#a#b#a# 当b为回文子串中心点时,长度为配对的个数3(# = #, a = a, # = #),而#a#b#b#a# 当下标4的#为回文子串中心点时,长度为配对的个数4(b = b, # = #, a = a, # = #), 这样的话无论是奇数长度的回文串aba还是偶数长度的abba都可以统一考虑顺利求解了。

        核心思路:设Len[i] 为扩展后的的字符串中的字符i为回文子串中心时的最长回文子串长度的半径,而这个半径也恰恰等于字符串扩展前的最长回文子串长度,例如#a#b#a#, Len[b] = 3,3即是扩展后的字符串的最长回文子串长度的半径(#=#,a=a, #=#),也是扩展前的最长回文子串长度(aba)。那么对于求出最长回文子串的这道题目其实就是求出每个字符的Len[i]并从其中挑出最大值。上面说过,Manacher算法的核心思路就是利用之前求得的臂长( 即之前求出的Len值) 来减少时间复杂度,也就是说通过前面求出的Len值来加速求出当前下标i的 Len[i],快速求出所有的Len 值就是该算法的目的。

我们拿经典样例举个例子:babcbabcbaccba

        假设我们已经求出了前面若干个Len值,即Len[0...i-1],  当要求第11位的 a 的Len[i] 时,我们如何快速求出?

        首先我们需要一直维护一个最右臂长 r, 再维护拥有最右臂长的中心点下标值mid,r = mid + Len[mid], 形象点说就是,维护前面字符中手臂伸的最靠右的,(l, r) 代表着以mid为回文子串中心的回文子串的范围。

      目前已知(l,r)是回文串,也是已知达到最右覆盖的。我们可以快速的求出Len[i]的值,而快速求它需要考虑俩种可能性。

  •        一种是当 i < r 时,当i 处于(l,r)回文串的半径内时,我们就可以利用回文串的特性先寻找 i 的对称点 i' , 对称的i'也等于2*mid-i ,因为我们已经知道对称点i'的Len值(臂长),我们可以得出一个结论Len[i] = min(r - i, Len[2*mid-i]), 这里会有一个疑问,即为什么我们不直接令Len[i] = Len[2*mid-i],而要和r - i 取个最小值呢?,那是因为能够确保的半径值不超过r-i,你不能确保i'镜像的臂长都在(l,r)内。而如果i'镜像的臂长都在(l,r)内,那i点的臂长至少也是镜像i'臂长的长度,超出r的部分,进行暴力比较即while (new_str[i+Len[i]] == new_str[i-Len[i]]) Len[i]++;来更新i点的臂长即可。
  •        另一种就是当 i >= r 时,Len[i]直接设置为默认值1,因为这时的 i 无法利用镜像i' 来获取臂长的至少值,i的右边均是未知值,都需要暴力比较,即while (new_str[i+Len[i]] == new_str[i-Len[i]]) Len[i]++;

      这样就可以快速算出当前i的Len值,每算完一个Len[i]后,我们都需要维护最右臂长 r 和 下标mid 值,即 if((i+Len[i]) > r) {r = i + Len[i]; mid = i} , 因为维护的这个区间是一个回文串,后续可以使用对称点的特性来迅速求出Len[i]值。

时间复杂度解析:

        暴力算法中我们需要对 i 一直拓展到拓展不了为止。而manacher算法可以帮助我们直接跳过拓展 i i + min(r-i, Len[2*mid-i])这部分,从 i + min(r-i, Len[2*mid-i])+1开始拓展。

        所以时间复杂度为O(n), n为拓展后的字符串长度。由于对于每个位置,扩展要么从当前的最右侧臂长 r 开始,要么只会进行一步,而 r 最多向前走 O(n)步,因此算法的复杂度为 O(n)。

int longestPalindrome(string s) {
    string new_str = "";
    new_str += "@";
    for (int i = 0;i < str.length(); i++) {
        new_str += "#";
        new_str += str[i];
    }
    new_str += "#";
    new_str += "$";
    cout << new_str << endl;
    //new_str 存储 扩充后的字符串,如 @#a#b#a#$
    int len = new_str.length();
    int r = 0, mid = 0, ans = 0;//r 表示目前最右回文子串半径,mid则是该字串的中心下标,ans存储最长回文子串长度。
    for (int i = 1;i < len-1; i++) {
        if (i < r) {
           Len[i] = min(r - i, Len[2*mid-i]);
        }
        else Len[i] = 1;

        while (new_str[i+Len[i]] == new_str[i-Len[i]]) {
            Len[i]++;
        }
        if ((i+Len[i]) > r) {
            r = i + Len[i];
            mid = i;
        }
        ans = max(ans, (Len[i] - 1));
    }
    return ans;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值