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

281 阅读3分钟

这是我参与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. 00n1n-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,此时因为iijj(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 博客中。