【笔记】字符串::Manacher算法

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战.


参考链接和图片来源:oi-wiki.org/string/mana…

简介

Manacher算法俗称马拉车,由Glenn K. Manacher在1975年提出,可以找到每个以字符位置或字符之间位置为中心的回文串长度。

因为回文串可以以两个字符间的位置作为中心,我们把原字符串两端及每两个字符之间插入一个不会被用到的字符,比如abababb变成#a#b#a#b#a#b#b#

此时计算以每个字符位置

ii

为中心的回文串数量

d[i]d[i]

,它同时也是原串中以其为中心的最长回文串长度 +1
【例子】

# a # b # a # b # a # b # b #
1 2 1 4 1 6 1 6 1 4 1 2 3 2 1

  a   b   a   b   a   b   b
  1 0 3 0 5 0 5 0 3 0 1 2 1
复制代码

算法

首先按上述方法扩展字符串,然后只需要求得

d[i]d[i]

数组。
朴素方法是以每个位置为中心开始不断扩展,直到停下为止,复杂度

O(n2)O(n^2)

为了快速计算,我们需要维护已找到的回文子串

[l,r][l,r]

中最靠右的一个,即

rr

最大的一个。


  1. 00

    n1n-1

    ii

    ,按如下方法逐个计算

    d[i]d[i]

  2. 如果
    ii

    位于当前回文串之外,即

    i>ri>r

    11

    开始逐步扩展

    d[i]d[i]

    ,停下后更新

    (l,r),l=id[i]+1,r=i+d[i]1(l,r),l=i-d[i]+1,r=i+d[i]-1

  3. 如果
    i<=ri<=r

    l<=i<=rl<=i<=r

    ii

    (l,r)(l,r)

    内的对称位置

    j=r+lij=r+l-i

    1. 如果
      jd[j]+1>lj-d[j]+1>l

      i+d[j]+1<ri+d[j]+1<r

      ii

      jj

      (l,r)(l,r)

      内的对称性,可以断定

      d[i]=d[j]d[i]=d[j]

在这里插入图片描述
2. 如果

jd[j]+1<=lj-d[j]+1<=l

i+d[j]+1>=ri+d[j]+1>=r

ii

为中心是否有更长的回文串在(l,r)之外:在这里插入图片描述
在这种情况下,令

d[i]=rid[i]=r-i

(l,r)(l,r)

复杂度分析

每一步,要么

ii

增加,要么

rr

增加,这两者永不减少且最大为

nn

,所以复杂度

O(n)O(n)

代码

char str[M], s[M];
int d[M]; //d[i]以i为中心的回文串个数,也表示原串中i/2+1位置为中心的最长回文串长度+1
int manacher(char *str) //处理字符串,得到d数组,返回最长回文子串的长度
{
    s[0] = '#';
    for(int i=0; str[i]; ++i)
    {
        s[i*2+1] = str[i];
        s[i*2+2] = '#';
    }
    for(int i=0, l=0, r=0; s[i]; ++i)
    {
        d[i] = 1;
        if(i<r) d[i] = min(d[l+r-i],r-i+1);
        while(i-d[i]>=0 && s[i+d[i]] && s[i-d[i]]==s[i+d[i]])
            ++d[i];
        if(i+d[i]-1>r)
        {
            r = i+d[i]-1;
            l = i-d[i]+1;
        }
    }
    int res = 1;
    for(int i=0; s[i]; ++i)
        res = max(res, d[i]-1);
    return res;
}
复制代码

习题

P3805 【模板】manacher算法
给定一个长度不超过11000000的字符串,求最长回文子串的长度。

/* LittleFall : Hello! */
#include <bits/stdc++.h>
using namespace std; using ll = long long; inline int read();
const int M = 23000016, MOD = 1000000007;

char str[M], s[M];
int d[M]; //d[i]表示i位置的
//也表示原串中i/2+1位置为中心的最长回文串长度+1
int manacher(char *str) //处理字符串,得到d数组,返回最长回文子串的长度
{
    s[0] = '#';
    for(int i=0; str[i]; ++i)
    {
        s[i*2+1] = str[i];
        s[i*2+2] = '#';
    }
    for(int i=0, l=0, r=0; s[i]; ++i)
    {
        d[i] = 1;
        if(i<r) d[i] = min(d[l+r-i],r-i+1);
        while(i-d[i]>=0 && s[i+d[i]] && s[i-d[i]]==s[i+d[i]])
            ++d[i];
        if(i+d[i]-1>r)
        {
            r = i+d[i]-1;
            l = i-d[i]+1;
        }
    }
    int res = 1;
    for(int i=0; s[i]; ++i)
        res = max(res, d[i]-1);
    return res;
}
int main(void)
{
	#ifdef _LITTLEFALL_
	freopen("in.txt","r",stdin);
    #endif

    scanf("%s",str);
	int res = manacher(str);
    printf("%d\n",res );
    return 0;
}


inline int read(){
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
复制代码

--- 本文也发表于我的 csdn 博客中。