最长回文子串问题

给定一个字符串 s ,找到 s 中的最长回文子串。

  • 示例 1 :
  1. 输入: "babad"
  2. 输出: "bab"
  • 示例 2:
  1. 输入: "cbbd"
  2. 输出: "bb"

中心扩展算法

根据回文串的特性:关于中心镜像对称,我们可以将回文串从中间进行展开,共有 2n-1 个中心点。从中心向两边扩展,记录最长的回文子串信息。这样就得到了一个 Manacher 算法 - 图1 的算法。

  1. // 中心扩展算法
  2. string longestPalindrome(string s) {
  3. string temp = "#";
  4. for(int i = 0; i < s.length(); ++i){
  5. temp = temp + s[i] + "#";
  6. }
  7. int maxL = 0;
  8. int begin_ = 0;
  9. int end_ = 0;
  10. for(int i = 0; i < temp.length(); ++i){
  11. int begin = i;
  12. int end = i;
  13. int currL = 0;
  14. while(begin >= 0 && end < temp.length() && temp[begin] == temp[end]){
  15. ++currL;
  16. --begin;
  17. ++end;
  18. }
  19. if(currL > maxL){
  20. maxL = currL;
  21. begin_ = begin + 1;
  22. end_ = end - 1;
  23. }
  24. }
  25. string res;
  26. for(int i = begin_; i <= end_; ++i){
  27. if(temp[i] != '#')
  28. res += temp[i];
  29. }
  30. return res;
  31. }

Manacher 算法

中心扩展算法解决了奇偶不对称这一问题,但是没有解决重复访问字符的问题,这样使得时间复杂度仍然达到了 Manacher 算法 - 图2 。Manacher 算法通过跳过部分字符,使得时间复杂度降低到了 Manacher 算法 - 图3
同中心扩展算法相同,首先需要对字符串 s 做预处理:在每个字符之间及首尾插入不在字符串中出现的字符,以解决奇偶不一致问题。

RL 数组

Mancher 算法定义了一个回文半径数组 RL ,用 RL[i] 表示以第 i 个字符为对称轴的回文半径。

字符 c # b # a # b # a # d #
位置 i 0 1 2 3 4 5 6 7 8 9 10
回文半径 RL 1 2 1 4 1 4 1 2 1 2 1
RL - 1 0 1 0 3 0 3 0 1 0 1 0

通过观察我们可以发现, RL - 1 的值即为原字符串中以 i 为中心位置的最长回文子串长度。这样我们的问题就变为了如何高效地求 RL 数组的值。

MaxRight 变量与 pos 变量

MaxRight 变量用来表示当前所有回文子串所能触及到的最右一个字符的位置。另外还要记录下 MaxRight 对应回文串的对称轴所在的位置,记为 pos ,它们的位置关系如下:

pic1.jpg

我们从左往右访问字符串来求 RL ,假设当前访问到的位置为 i ,即要求 RL[i] 。在计算过程中,我们需要关注的是: i 是在 MaxRight 的左边还是右边。我们分情况来讨论:

情况一:i 在 MaxRight 左边

pic2.jpg

  • 图中两个红色块及之间的串是回文的
  • 以 i 为对称轴的回文串与红色块间的回文串有所重叠

我们找到 i 关于 pos 的对称位置 j ,此时 RL[j] 已经计算出来,以 i 为对称轴的回文串和以 j 为对称轴的回文串,是一部分相同的,具体分为两种情况:

以 j 为对称轴的回文串较短

pic3.jpg

  • RL[i] 不会小于 RL[j]
  • 初始化 RL[i] = RL[j] ,并以 i 为对称轴,继续向左右两边扩展,直到左右两边字符不同,或者到达边界。

以 j 为对称轴的回文串较长

pic4.jpg

  • 不超过 MaxRight 部分一定是回文的
  • 初始化 RL[i] = MaxRight - RL[i] ,并以 i 为对称轴,继续向左右两边扩展。

总结起来,具体操作过程为:

  1. RL[i] = min(RL[2 * pos - i], MaxRight - i)
  2. 以 i 为中心进行扩展,直到两边字符不一致或到达边界
  3. 更新 MaxRightpos

情况二:i 在 MaxRight 右边:

pic5.jpg

遇到这种情况,只能从 i 开始扩展。

string longestPalindrome(string s) {
    string temp = "#";
    for(int i = 0; i < s.length(); ++i){
        temp = temp + s[i] + "#";
    }
    int* RL = new int[temp.length()];
    for(int i = 0; i < temp.length(); ++i)
        RL[i] = 1;
    int MaxRight = 0;
    int pos = 0;
    int MaxR = 0;
    int Maxi = 0;
    for(int i = 1; i < temp.length(); ++i){
        RL[i] = i > MaxRight ? 1 : min(RL[2 * pos - i], MaxRight - i);
        int j = i - RL[i];
        int k = i + RL[i];
        while(j >= 0 && k < temp.length() && temp[j] == temp[k]){
            --j;
            ++k;
        }
        if(k-1 > MaxRight){
            MaxRight = k - 1;
            pos = i;
        }
        RL[i] = k - i;
        if(RL[i] > MaxR){
            MaxR = RL[i];
            Maxi = i;
        }
    }
    return s.substr((Maxi + 1 - MaxR) / 2, MaxR - 1);
}

参考文章:

  1. Manacher 算法求最长回文子串