Manacher 算法 求最长回文子串

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/wangjiangrong/article/details/91449637

刷LeetCode的时候有一题,提到了一个算法Manacher,可以在时间复杂度O(n)的情况下求出最长的回文子串,既然是一个没听过看过的算法,就去查了下并自己实现了下效果,在这纪录一下。

题目链接:https://leetcode-cn.com/problems/longest-palindromic-substring/

回文子串:通俗的讲就是正序倒序都一样的字符串,例如aba,abba,asdfdsa这种的。

题目的要求是比如给定一个字符串asdfdsaba,然后我们要输出其最长的回文子串,即sdfds。

解决这个问题的方法有很多种,比如最简单的就是字符串按位遍历,然后每位进行左右延伸,获取已该位为中心的最长回文子串,最后找出每位的回文子串中最长的即可(中心扩散法)。这里面有个坑,就是例如aba,当你到b时,左右延伸得到aba是回文子串,但是abba时,你到b左右延伸得到的abb或者bba都不是回文子串,导致错误。所以针对这种情况,我们优先在字符串每位中插入一个特殊字符,例如'#',然后再进行上诉操作,即abba为#a#b#b#a#。

接下来我们要讲的Manacher算法,我的理解就是在上面这个方法进行了优化,优化的地方就是不需要遍历的时候不需要每一位都去做左右延伸找回文子串操作,只有在一些特殊的情况下才会去执行,大大提高了效率。那么什么时候需要去找子串,什么时候不需要呢,搞懂这点就等于搞懂了Manacher算法。

Manacher算法

首先我们来了解几个概念

回文中心点:即回文串的中心点,#a#b#a#的中心点为b,#a#b#b#a#的中心点为第三个#。

回文半径:即回文串一半的长度,不算中心点(有些会算上中心点,其实都无所谓,只是影响了后面部分代码的计算),例如aba半径为1,abba半径为2

回文边界:左边界即回文串最左边的字符的位置,右边界即为最右边字符位置。

例如我们现在有字符串qawasdsaz,其中回文子串awa,中心点为w,下标为2,半径为1,左边界下标为1,右边界下标为3。回文子串asdsa,中心点为d,下标为5,半径为2,左边界下标为3,右边界下标为7。

可知:回文右边界 = 中心点 + 半径,回文左边界 = 中心点 - 半径

以字符串中每位字符为中心点得到的回文子串,将其半径组成新的数组,即为回文半径数组。例如刚刚的字符串qawasdsaz,下标0为中心的回文子串为q,半径0,...,下标为2为中心的回文子串为awa,半径为1,...,下标为5为中心的回文子串为asdsa,半径为2...所以得到的回文半径数组为[0, 0, 1, 0, 0, 2, 0, 0, 0]。该数组中的最大值即为最长回文子串的半径,数组下标即为回文子串的中心点所在字符串下标,即可得到最长回文子串。

过程

接下来最重要的就是如何根据字符串得到回文半径数组(其实中心扩散法就是字符串每位都去计算其为中心的回文,而Manacher算法就是在这个基础上进行了优化,从而避免了大量的运算)。

1. 首先在遍历之前我们会对字符串进行每位插入特殊符号(例如 '#')的操作,避免abba这类回文串引发错误。

2. 接着我们开始遍历字符串的每一位,算出以该字符为中心点的最长回文串的半径,纪录在新的数组 radiusArray 中。

3. 然后我们定义几个变量用于存储数据,当前回文子串的中心点center,当前回文子串的右边界right

4. 假设在我们的遍历中的某一次,以下标 i 为中心点 center = i,通过中心扩散的方法,计算出以 i 为中心点的最长子串 s,半径为 r ,即radiusArray[center] = r,右边界 right 即为 i+r,。

5. 在后面的遍历中 i+j,若 i+j < right,说明 i+j 还在子串 s 中。i+j 通过 center 的对称点即为 i-j ,并且由于 i-j 在 i+j 的左边,所以 i-j 的回文半径我们是已知的,即 radiusArray[i-j]。同时我们还知道,子串s的左边界下标为 center-radiusArray[center],对称点 i-j 的回文子串左边界为 i-j-radiusArray[i-j]。而我们要求的是 radiusArray[i+j] 的值

在知道上面这些数据之后,我们便可以通过对比两个左边界,在一些情况下不需要使用中心扩散的方法就判断出 i+j 的回文半径。根据回文的性质,我们可以知道 center-radiusArray[center] 到 center 的倒序 等于 center center+radiusArray[center]

  • 若 i-j-radiusArray[i-j] > center-radiusArray[center],即对称点的左边界在子串s内,由于s串左右两边是一致的,所以 i+j 的回文串等于 i-j 的回文串,radiusArray[i+j] = radiusArray[i-j],避免了使用中心扩散法,center 和 right 的值不变。(一开始可能会有疑问,为什么 i+j 的回文串不会更长或更短,而是正好等于 i-j 的回文串,因为当回文串在 s 的内部的时候,由于左右对称,若 i+j 的回文更长或更短,会同时影响到 i-j 的回文对应变化)
  • 若 i-j-radiusArray[i-j] < center-radiusArray[center],即对称点的左边界在子串s外,此时radiusArray[i+j] = right - (i+j),避免了使用中心扩散法,center 和 right 的值不变。(i-j 到 center-radiusArray[center]的串 等于 i+j 到 right 的串,right ==center+radiusArray[center],但是center-radiusArray[center] - 1 不等于 right + 1,否则s串的长度会再增加 1 的半径。所以 i+j 的回文串止步于right)
  • 若 i-j-radiusArray[i-j] == center-radiusArray[center],即对称点的左边界在子串s左边界上,此时无法确定 i+j 的回文串,可能止步于right,也可能更长,所以需要通过中心扩散法获取新的子串s,同时更新center和 right 的值。(因为两个左边界相同,我们只能确定 i+j 到 right 是 i+j 的回文串的右半部分,但是可能更长)

6.若 i+j > right,就直接利用中心扩散的方法获取新的子串s,即更新center right 的值。

7.遍历结束找出radiusArray中的最大值即为最长回文子串的半径,对应下标即该子串中心点在字符串中的下标。假设radiusArray[i]最大,值为 r,则原字符串(未加'#'的)下标 (i-r)/2 到 下标 (i+r)/2 的子字符串为最长的回文子串

所以当有较多或较长的回文串时,这些回文串的右半部分大部分都可以直接计算出,省去大量的运算。下面就是代码实现:

//求最长的回文子串
public string LongestPalindrome(string s)
{
    if (s.Length <= 1)
    {
        return s;
    }

    //添加特殊符号 #
    System.Text.StringBuilder sb = new System.Text.StringBuilder();
    for (int i = 0, len = s.Length; i < len; i++)
    {
        sb.Append('#');
        sb.Append(s[i]);
    }
    sb.Append('#');
    string newString = sb.ToString();
    sb.Clear();
    sb = null;

    //回文半径数组
    int[] radiusArray = new int[newString.Length];
    //center当前回文串中心点,right当前回文串右边界,left当前回文串左边界,symmetryIndex当前遍历下标的对称下标,symmetryIndexLeft对称下标的回文串左边界
    int center = -1, right = -1, left = -1, symmetryIndex = -1, symmetryIndexLeft = -1;

    //maxRadius 最大半径的值,maxIndex 最大半径的下标
    int maxRadius = -1, maxIndex = -1;
    
    //遍历,如果最长回文串的半径>剩下的长度,则返回。最后两个字符可以不用遍历,因为其回文串肯定是本身
    for (int i = 0, len = newString.Length; i < len - 2 && maxRadius < (len - i); i++)
    {
        if (i > right)
        {
            right = GetRight(newString, i);
            center = i;
            radiusArray[i] = right - i;
        }
        else
        {
            symmetryIndex = center - (i - center);
            symmetryIndexLeft = symmetryIndex - radiusArray[symmetryIndex];
            left = center - (right - center);

            if (symmetryIndexLeft > left)
            {
                radiusArray[i] = radiusArray[symmetryIndex];
            }
            else if (symmetryIndexLeft < left)
            {
                radiusArray[i] = right - i;
            }
            else
            {
                right = GetRight(newString, i);
                center = i;
                radiusArray[i] = right - i;
            }
        }

        //纪录最大值
        if (radiusArray[i]> maxRadius)
        {
            maxRadius = radiusArray[i];
            maxIndex = i;
        }
    }

    return s.Substring((maxIndex - maxRadius) / 2, maxRadius);
}

//中心扩散 获取某个下标的回文串右边界
public int GetRight(string s, int index)
{
    int right = index;
    for(int i=1,len = s.Length; index + i < len && index - i >= 0; i++)
    {
        if (s[index - i] != s[index + i])
        {
            return right;
        }
        else
        {
            right = index + i;
        }
    }
    return right;

提交通过

猜你喜欢

转载自blog.csdn.net/wangjiangrong/article/details/91449637