Manacher算法

Manacher算法

描述: Manacher 算法用于求解字符串中最长回文子串的问题。

在了解该算法之前需要明白一些基本概念:

  • 回文串:对于字符串 s s s s = s r e v s = s_{rev} s=srev,即 s s s等于 s s s的逆序,则 s s s是一个回文串
  • 回文串分为奇回文串以及偶回文串

解法:直观的暴力做法时间复杂度为 O ( n 3 ) O(n^3) O(n3),即枚举所有的子串,对这些子串判断是否回文。

稍加思考便会发现,对于一个回文串,他一定有一个对称中心点,这个点的左右两边是相同的,如果按照对称中心来依次枚举,那么复杂度为变为 O ( n 2 ) O(n^2) O(n2)

算法思想

Manacher算法实际上就是对枚举对称中心这一做法的优化。

字符个数的奇偶性不同,对称中心也是不同的,例如对于奇回文串 a b a aba aba,对称中心点为 b b b,对于偶回文串 a b b a abba abba,对称中心点则为 b b bb bb中间,为了统一对称点,一般的做法是,将原字符串 s s s的首尾以及相邻的两个字符中间插入一个不会出现的字符(例如:#)这样会统一成一个奇回文串,新字符串长度 = 原串 * 2 + 1

例如原串: a b b a abba abba

新字符串: ∗ a ∗ b ∗ b ∗ a ∗ *a*b*b*a* abba


考虑维护一个数组: L L L

其中 L [ i ] L[i] L[i]表示以 i i i为对称中心的最长回文串的最右边的字符到 s [ i ] s[i] s[i]的长度(即回文半径)

对于原串: a a b a b aabab aabab,添加一个字符变为 ∗ a ∗ a ∗ b ∗ a ∗ b ∗ *a*a*b*a*b* aabab,他的 L L L数组如下:

*a*a*b*a*b*
12321414121

L L L数组的作用: L [ i ] − 1 L[i]-1 L[i]1可以表示该子串在原串中对应的回文串的长度,例如上面图片中 b b b L L L值为4,那么原串 a a b a b aabab aabab中,以 b b b为对称中心的回文串长度为 4 − 1 = 3 4-1=3 41=3

因此,只需要找到所有的 L L L数组就可以求出最长回文串了。


L L L数组

假定要求位置 i i i L L L数组,( i i i之前的已经求出),我们考虑维护两个值 k k k, r r r。其中 r r r表示, i i i之前所有位置求出来的右端点的最大值(即 L L L数组的最大值), k k k表示 r r r最大时的位置。

那么可以分为以下情况讨论

1、 i < r i < r i<r

如上图所示,其中红色和橙色部分是相同的,找到 i i i关于 k k k的对称位置,即 j j j(其中 j = 2 ∗ k − i j = 2*k-i j=2ki ),由于 k k k是对称中心位置,那么判断一下 L [ j ] L[j] L[j] r − i r-i ri的大小关系

  • l [ j ] < r − i l[j]<r-i l[j]<ri如下图所示:

    绿色部分必定相同,因此 l [ i ] = l [ j ] l[i] = l[j] l[i]=l[j]

  • l [ j ] > r − i l[j] > r-i l[j]>ri

    此时, l [ i ] > = r − i l[i] >= r-i l[i]>=ri的,超出的部分,需要暴力更新。

2、 i > r i>r i>r

没有可以使用的信息,直接暴力更新即可。

注意:记得更新 k , r k,r k,r的值。

一些技巧:可以在字符串首尾放置两个"哨兵",它们和其他字符串均不同,这样暴力更新时不用考虑边界问题

//其中s[0]='@',s[n]='%',中间插入的字符为'#'

int ans = 0, k = 0, r = 0;
    l[1] = 1;
    for (int i = 2; i <= n; i ++) {
        if (i < r) l[i] = min (r - i, l[(k << 1) - i]);
        else l[i] = 1;

        while (s[i - l[i]] == s[i + l[i]]) l[i] ++; // 暴力更新超过r的部分
        if (i + l[i] > r) k = i, r = i + l[i]; // 更新k,r
        ans = max (ans, l[i] - 1); // 找到最长回文子串
    }
    cout << ans << endl;

例题

题目描述

https://www.luogu.com.cn/problem/P3805

给出一个只由小写英文字符 a , b , c , … y , z \texttt a,\texttt b,\texttt c,\ldots\texttt y,\texttt z a,b,c,y,z 组成的字符串 S ,求 S 中最长回文串的长度 。

字符串长度为 n。

输入格式

一行小写英文字符 a , b , c , ⋯   , y , z \texttt a,\texttt b,\texttt c,\cdots,\texttt y,\texttt z a,b,c,,y,z 组成的字符串 S。

输出格式

一个整数表示答案。

输入样例

aaa

输出样例

3

说明/提示

1 ≤ n ≤ 1.1 × 1 0 7 1\le n\le 1.1\times 10^7 1n1.1×107

参考程序

#include <iostream>
#include <cstring>

using namespace std;

const int N = 2e7 + 2e6;

string t, s;
int n, m;
int l[N];


int main () {
    cin >> t;
    m = t.length();
    n = (m << 1) + 1;
    s.resize(n + 1);
    s[0] = '@', s[n] = '%', s[1] = '#';
    for (int i = 1; i <= m; i ++) {
        s[i << 1] = t[i - 1];
        s[i << 1 | 1] = '#';
    }

    int ans = 0, k = 0, r = 0;
    l[1] = 1;
    for (int i = 2; i <= n; i ++) {
        if (i < r) l[i] = min (r - i, l[(k << 1) - i]);
        else l[i] = 1;

        while (s[i - l[i]] == s[i + l[i]]) l[i] ++;
        if (i + l[i] > r) k = i, r = i + l[i];
        ans = max (ans, l[i] - 1);
    }
    cout << ans << endl;
    return 0;
}
  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值