算法学习(20):Manacher算法

Manacher算法

找到一个字符串的最长回文子串

  如果什么处理也不做,暴力解,从0开始往后走,每到一个位置前后找回文,记录回文长度,最后返回最大的,这样会出现一个问题,如果回文的长度是偶数,没法求abba这种格式的回文,所以要改进。
  改进方法就是把所有两个字符中间、字符串的最左边、最右边都加一个相同的字符,比如“#”(加什么都无所谓),这样就可以解决回文长度是偶数的问题,把找到的最大回文长度除以2就是正确的答案。
  但是这种暴力解的时间复杂度太高,是O(N2),有没有办法找到一种算法的时间复杂度是O(N),这就引出了manacher算法。

Manacher算法

首先明确Manacher算法中的几个概念

  • 回文半径:回文长度的一半
  • 回文半径数组:在从左往右遍历的过程中,所有位置回文半径都记录下来,生成一个数组
  • 所到达的最右回文右边界:举个例子:#1#2#1# ,每到达一个字符,计算它的回文半径,然后看它产生的回文的最右边是哪个下标,看看有没有越过之前的最右回文右边界,没有则不更新,越过了则更新最右回文右边界为当前位置字符产生的回文的最右边界。在还没有开始遍历数组前,最右回文右边界为-1
所到达的位置 0 1 2 3 4 5 6
回文半径
0 1 0 3 0 1 0
最右回文右边界 0 2 2 6 6 6 6
  • 取得更远右边界时的中心点:与最右回文右边界一起更新,每次最右回文右边界更新的时候,找到这时回文的中心位置,就是所要求的中心点。

Manacher算法的流程

最右回文右边界R初始值为-1,取得更远右边界时的中心点C初始值为-1。当来到第0号位置时,回文半径一定是0,R变为0,C变为0。以后当来到i位置时,
分情况讨论:

  • 当前位置在R之外,左右两边字符对比,暴力扩,然后更新R和C
  • 当前位置在R内,假设c的左边界是L,右边界是R:
    (1)i关于c的对称位置i'的回文区域在L和R内(没有压L的线),即i'的回文左边界下标比L大,这时候i的回文半径一定与i'相等
    (2)i关于c的对称位置i'的回文区域在L和R之外,即i'的回文左边界下标比L小,这时候i的回文半径一定是i到R的距离。假设R右边的字符为Y,L左边的字符为X,i的回文左边界左边的字符为N,i'的回文右边界的字符为M,根据对称关系得到X = M,M = N,如果i的回文半径比i到R的距离大,则会得到N = Y,从而得到X = Y,这时c会由于X = Y,回文半径会变大,这是和事实不符的,所以i的回文半径不可能比i到R的距离大。而由对称关系得到i的回文半径至少有i到R的距离,所以i的回文半径一定是i到R的距离。
    (3)i关于c的对称位置i'的回文区域刚好压在L上,由对称关系得到i的回文半径至少有i到R的距离,而会不会更大,需要将R右边的字符与i的回文左边界左边的字符作比较,如果相等则半径更大,继续比较更外侧的字符,直到不相等,记录此时的半径。

至此,所有情况讨论完毕。

Manacher算法C++代码实现

string manacherString(string str);

int maxLcpsLength(string s)
{
    string str = manacherString(s);
    vector<int> pArr;
    pArr.resize(str.size());
    int C = -1;
    int R = -1;     //所到达的最右回文右边界再往右一个位置,相当于上面讨论中的R+1
    int Max = INT_MIN;
    for (int i = 0; i < str.size(); i++)
    {
        pArr[i] = R > i ? min(pArr[C * 2 - i], R - i) : 1;   //得到上面讨论的三种情况至少不需要扩大的部分,i在R外是1,其余则是i'回文半径和R-i较小的那个
        while ((i + pArr[i]) < str.size() && (i - pArr[i]) > -1)       //不越界
        {
            if (str[i + pArr[i]] == str[i - pArr[i]])          //看看能不能往外扩
            {
                pArr[i]++;
            }
            else
            {
                break;      //扩失败一次就break
            }
        }
        if (i + pArr[i] > R)        //看看R和C要不要更新
        {
            R = i + pArr[i];
            C = i;
        }
        Max = max(pArr[i], Max);        //储存当前最大的回文半径
    }
    return Max - 1;         //加过特殊字符后的字符串的回文半径-1就等于原字符串的回文长度
} 

string manacherString(string str)
{
    string ms = "#";
    for (int i = 0; i < str.size(); i++)
    {
        ms += str[i];
        ms += "#";
    }
    return ms;
}
posted @ 2022-08-01 16:49  小肉包i  阅读(26)  评论(0)    收藏  举报