📜  Manacher算法-线性时间最长回文子串-第2部分(1)

📅  最后修改于: 2023-12-03 15:32:48.469000             🧑  作者: Mango

Manacher算法-线性时间最长回文子串-第2部分

上一篇文章 中,我们介绍了 Manacher 算法的基本原理,并展示了如何使用 Manacher 算法在线性时间内计算最长回文子串。

本文将继续讲解 Manacher 算法的细节,并给出一份详细的代码实现。

Manacher算法的细节
1. 字符串的总长度

在 Manacher 算法中,我们使用了两个指针,一个在回文中心,一个在回文末端。为了避免越界,我们需要保证指针从中心扩散时不会超出字符串的范围。

解决方法是使用一个新字符串 $s'$,在每个原始字符间插入一个特殊字符,例如插入“#”符号。这样,新字符串的长度变成了 $2n + 1$(假设原字符串的长度为 $n$)。

我们同时需要记录从左到右扩展的四个指针,它们的角色分别是:

  1. id:当前最长回文子串的中心节点位置
  2. mx:回文串延伸到的最右端位置,即最右边的那个原始字符串的下标
  3. p[i]:以 $i$ 为中心的回文串向左/右扩展的长度(包含半径 $r[i]$)
  4. r[i]:以 $i$ 为中心的回文串的半径长度
2. 判断回文串的延伸

回文串的延伸分为两种情况:

  1. 关于中心节点对称的情况,例如 aba
  2. 以中心节点为分界点的对称情况,例如 abba

我们在计算每个回文半径的时候,需要同时计算回文半径向左/右延伸的长度。根据回文串的特性,对于以 $i$ 为中心的回文串,如果存在一个 $j \in [1, i-1]$,满足 $j + p[j] = i$,那么 $i$ 的回文半径就可以直接赋值为 $p[j]$。

这是因为 $j$ 就是 $i$ 的左侧对称点(关于 $id$ 点对称),$p[j]$ 就是 $j$ 点向外扩散的半径长度,所以 $p[j]$ 向右扩展的部分一定会延伸到 $i$ 点。而且,对于可能的 $j$ 的值,我们只需要从右往左扫描一次即可,所以这部分时间复杂度可以达到线性。

如果不存在该 $j$ 值,我们需要暴力向左/右扩展搜索回文串。注意,我们可以从之前的扩展中获得部分匹配的结果,所以时间复杂度是线性的。

具体实现请看下面的代码。

3. 回文半径的计算

回文半径的计算方式比较简单,假设当前的中心位置为 $mid$,向左/右扩展的最远距离为 $r[mid]$。我们从左往右遍历字符串,计算每个位置 $r[i]$ 的值。

这里存在一个小 trick:我们使用 $mx$ 记录当前最右的一个回文串的位置,即 $\max(j + p[j])$,然后通过比较 $mx$ 和 $i$ 的大小判断当前是否需要扩展。为什么这个方法是对的呢?首先在框架中我们可以看出,$i$ 位置成为新的回文中心时,它能够回文扩散的范围一定大于等于 $mx$,因为 $mx$ 已经是记录过的最大回文半径,所以 $i$ 点扩展的范围必然比 $mx$ 大。

image.png

当然,如果 $i$ 点扩展的范围小于 $mx$,那么我们需要将 $i$ 点左侧的回文半径直接赋值为 $i$ 在 $mx$ 下的对称点左侧的回文半径,即 $rx = 2 \times id - i$。

具体实现请看下面的代码。

代码实现

下面是整个 Manacher 算法的具体实现代码。请注意,该代码中省略了字符串预处理,并使用了 C++ STL 的 vector 实现,您需要自行根据具体情况调整。

vector<int> p(n, 0);
int id = -1, mx = -1;
for (int i = 0; i < n; i++) {
    p[i] = mx > i ? min(p[2*id-i], mx-i) : 1;
    while (i - p[i] >= 0 && i + p[i] < n && s[i - p[i]] == s[i + p[i]]) p[i]++;
    if (i + p[i] > mx) {
        mx = i + p[i];
        id = i;
    }
}

至此,我们就完整地展示了 Manacher 算法的整个实现。

参考资料
  • LeetCode 5. Longest Palindromic Substring:https://leetcode.com/problems/longest-palindromic-substring/
  • 标清的线性做法Manacher:https://www.acwing.com/solution/content/26672/