最长回文子串(Manacher算法)

自闭症网瘾萝莉.ら 提交于 2019-12-04 20:18:31

Manacher算法求最长回文子串

给定一个字符串,求它的最长回文子串,例如"1232231"的最长回文子串为"3223"。用Manacher算法可以在O(N)时间内得到结果。

— 目录 —

1> 题目描述

给定一个长度小于100的字符串,求它的最长回文子串的长度

样例输入:
12212321

样例输出
6

2> 分析解法

普通解法

通常情况下我们会想到的方法是:遍历字符串的各个字符,然后对每一个字符找到以它为中心的最长回文串,遍历结束后再将得到的最大串长输出即可。
不难得出,上述方法的时间复杂度为 O(N2) ,而且对于"中心位置"这个概念还要分两种情况:

  • 例如对字符串 S,此时遍历到的某个字符 a
    a对应的回文子串长度为奇数,形如 BaB;
    a对应的回文子串长度为偶数,形如 BaaB;

因此该算法是不太合理的,但是对于 "中心位置" 需要分奇偶情况的问题,我们可以有很好的办法来解决。

改进

为了解决奇偶问题,首先在每个字符的两边插入一个特殊符号 '#',同时为了方便在代码中处理数组越界问题,在插入后的字符串最左边增加起始符号 '$'

例如,字符串 "abcdefg" 经过插入预处理后就变成了 "$ # a # b # c # d # e # f # g #"

这样做的好处是,将所有可能的回文子串不论是奇数长度还是偶数长度,都转化为了奇数长度统一处理。


继续分析,在遍历每个字符时,第 j 个字符对应的回文串与第 i (0 ≤ i < j) 个字符对应的回文串之间会不会存在某种关系? 从而降低时间复杂度?

如下图 2-1 所示,
i 代表:从左到右遍历字符串时某时刻的字符位置,
id 代表:此时最大回文子串的中心,
mx 代表:最大回文子串的右边界,
j 代表:i 关于 id 的对称点,
图中的大括号分别代表以idij 为中心的最长回文串。

图 2-1

图 2-1

假设 P[i] 代表以 i 为中心的最长回文串的长度,
那么根据图 2-1,一定有P[i] = P[j]


如果 i 的右边界大于 mx 呢?
图 2-2 所示,
此时由于 j 的左边部分延伸到 mx的对称点 左边去了, 所以仅能由虚线部分推测出 P[i]mx - i
至于 P[i] 的值具体是多少,就需要在虚线框的两侧进行两两比较,如果相同,那么 P[i] ++。

这里写图片描述

图 2-2

3>算法思路

Manacher算法思路

Manacher 算法的核心思想就是前面讲到的 插入特殊字符转化为奇数处理P[i] >= MIN(P[j], mx - i)
正是由于Manacher算法采取这种思想后,减少了对于每个字符的左右比较次数,使时间复杂度降低到O(N),空间复杂度仍为O(N)不变。

4> 具体过程

Manacher算法过程

下面以S[i] = “1211”为例,演示Manacher算法的具体过程。


第一步、填充 S[] 为 “$#1#2#1#1#”; 另外再创建数组 P[] ,代表对应字符的最长回文串长度,初始均为0; 定义 id 为最大回文子串的中心; 定义 mx 为最大回文子串的右边界; i 为当前遍历到 S[] 数组的位置 (从 i = 1 开始)。为了方便理解,可以创建如图 4-1所示的表:

这里写图片描述
图 4-1
如图所示,每一轮包括a、b两个步骤,
a 步骤根据 mxi 的关系确定 P[i] 的值
b 步骤继续更新 P[i] 的值,然后根据当前新的最长回文串的位置更新 mxid 的值

第二步、i = 1 :
a -> 先让 P[0] = 1,因为 mx <= i, 所以 P[i] = 1;

b -> 根据如下代码继续更新 P[i]; 因为 P[i] + i > mx , 即当前最大回文串的位置发生了变化, 所以 mx = P[i] + iid = i。第二步结束后,表格如图 4-2所示。

while (S[i + P[i]] == S[i - P[i]])
{
    P[i]++;
}

这里写图片描述
图 4-2

第三步、重复上述步骤,每一次变化结果如下各图所示:

这里写图片描述
图 4-3 当i = 3时


这里写图片描述
图 4-4 当i = 4时


这里写图片描述
图 4-5 当i = 5时


这里写图片描述
图 4-6 当i = 9时
图4-6所示, P[i] 最大值为 4, 所以 S[i] 的最长回文子串长度
4×21=7, 所以原字符串 "1211" 的最长回文子串长度为 (71)÷2=3

5> 实例代码

以下是算法伪代码:

// S[]为已经插入特殊字符后的数组
void Manacher(int S[])
{
    int mx = 0; // 最大回文子串的右边界
    int id;     // 最大回文子串的中心
    for (int i = 1; i < n; i++)
    {
        // mx > i 时,P[i] 先取 P[j](即P[2 * id - i]) 与 mx - i 中的较小值
        if (mx > i)
        {
            P[i] = MIN{P[j], mx - i}
        }
        else // mx <= i时, 先让P[i] = 1
        {
            P[i] = 1;
        }
        // a步结束,开始b步
        // 继续更新P[i]
        while (S[i + P[i]] == S[i - P[i]])
        {
            P[i]++;
        }
        // 如果最大回文子串的位置发生改变,则需要更新 id 和 mx
        if (P[i] + i > mx) 
        {
            mx = P[i] + i; // 右边界
            id = i;        // 中心位置
        }
    }
    return;
}

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!