Evanyou Blog 彩带

manacher算法详解+模板 P3805

前言:
记住manacher是一个很简单的算法。
首先我们来了解一下回文字串的定义:若一个字符串中的某一子串满足回文的性质,则称其是回文子串。(注意子串必须是连续的,而子序列是可以不连续的)
那么若给定一长度为n的字符串,要求出最长回文子串的长度,怎么做呢?
首先想到的是暴力搜索,我就不赘述思路了。那如果n特别大呢?10的7次方怎么做?
于是,我们需要了解一个贼有意思的鸡肋算法manacher,俗称“马拉车”,为什么说是贼有意思呢?因为它的思路实在是巧妙,又为什么说是鸡肋呢?因为它貌似只适用于求解最大回文子串的问题。
 
思路:

  首先我们知道回文子串的判定和长度的奇偶性是有关系的,由于回文分为偶回文(比如 bccb)和奇回文(比如 bcacb),而在处理奇偶问题上会比较繁琐,所以这里我们使用一个技巧,在字符间插入一个字符(前提这个字符未出现在串里),常用的是"$""#"。举个例子:s="abbahopxpo",转换为newS="$#a#b#b#a#h#o#p#x#p#o#\0"(这里在串首、尾加的字符"$"和"\0";只是设置边界,为了防止越界,而且显然不影响回文子串,下面会有说明),如此,s 里起初有一个偶回文abba和一个奇回文opxpo,被转换为"#a#b#b#a#"和"#o#p#x#p#o#",长度都转换成了奇数。

  证明经过上述操作回文串的长度必为奇数:

    若原回文串的长度为奇数n(原串 aaa),首尾间共有偶数n-1个空位被加上"#",加上首前和尾后各1个"#"(新串 #a#a#a#),可见新的长度为2n+1,显然是奇数;

    若原回文串的长度为偶数n(原串 aa),首尾间共有奇数n-1个空位被加上"#",加上首前和尾后各1个"#"(新串 #a#a#),可见新的长度也为2n+1,显然是奇数。得证。

  我们定义一个辅助数组int p[]p[i]表示以news[i]为中心的最长回文的半径,例如

  易得P[i]-1即以i为中心的在原串中的回文子串的长度:例如P[5]=5,则4就是以5这个位置为中心在原串中的最长回文子串的长度(abba),为什么这是对的呢?因为我们知道P[i]*2-1为新串中以i为中心的最长回文子串的长度,设该回文子串原长为n,则由上面的证明可知新串的长度=2n+1=P[i]*2-1,移项化简得n=P[i]-1。

  于是重点来了:我们如何快速的求出P[]数组。这时就要用到DP的思想了,我们一般会想到这样求解p[i],先初始化p[i]=1,再以news[i]为中心判断两边是否相等,相等就p[i]++。这就是普通的思维,但是我们想想,能否避免重复操作让p[i]的初始化不是 1,让它更大点,看下图:

  设置两个变量,mx 和 id 。
  mx 代表以news[id]为中心的最长回文最右边界,也就是mx=id+p[id]。

  假设我们现在求p[i],也就是以news[i]为中心的最长回文半径,如果i<mx,如上图,那么

 

  if(i<mx) p[i] = min(p[id*2-i] , mx-i);      

  else p[i] = 1;

 怎么理解呢?我们看图,因为mx是以id为中心的最长回文半径,若当前的i比mx要小,说明以i为中心的最长回文子串的一部分已经出现在以id为中心的回文子串中了,注意图中下面标注的两条短黑线,因为我们是线性dp,j点一定被访问过且P[j]被处理过,而j与i关于id对称(i+j=2*id),所以j=id*2-i,由于回文串的对称性,以i为中心的最长回文子串中的半径最小值一定是p[j]和mx-i中的最小值; 而若i>mx,就只能将p[i]赋为1来更新了。(感觉解释了和没解释一样啊,由于博主表述能力较差,我们不如举例)

  就比如一个回文子串:"#a#a#a#a#a#",我们以中间的a位置为id,所以id=6,mx=12。假设目前访问到了i=8的位置,则与其对称的j=2*6-8=4,而p[4]在访问8之前已经处理,p[4]=4,mx-i=4,所以取最小值4,将p[8]初始值赋为4。因为很容易看出在当前已经可以确定的是以8为中心的最长回文串的半径至少为4(#a#a#a#),当然有可能更大,我们之后判断news[i+P[i]]==news[i-P[i]]是否成立,若成立就p[i]++。

  讲的好心累啊,我学manacher时完全自学,也没有什么解释,完全是靠自己看懂的,还是得自己结合图和代码理解啊,先发一波核心代码。

 

int manacher()
{
    int len=init();
    int ans=-N,id,mx=0;
    for(int i=1;i<len;i++)
    {
        if(i<mx)p[i]=min(p[id*2-i],mx-i);
        else p[i]=1;
        while(news[i-p[i]]==news[i+p[i]])p[i]++;
        if(mx<i+p[i])id=i,mx=i+p[i];
        ans=max(ans,p[i]-1);
    }
    return ans;
}

 

 

 

 

  再发两种情况的图片自行理解一番:

  这是初值p[i]=p[id*2-i]的图:这里写图片描述

   这是初值p[i]=mx-i的图,注意虚线部分:这里写图片描述
 
 
巨说极其重要的性质

这里写图片描述
再来看这张图,我们发现,如果mx不更新,就不会出现本质不同的回文子串,因为前面已经出现过了;而每扩展一次mx,最多新出现一个本质不同的回文子串。
于是得到性质:一个字符串最多只有n个本质不同的回文子串。这个性质很重要,有些题会用到,需要这个性质去分析。

(虽然我也不知道有啥用……)

 

算法复杂度分析:知乎

我自己简略地讲一下,因为i与mx只有两种情况,而每次检索已经保证了单调递增,可以知道每个点最多被访问两次,while()循环本身的时间复杂度在没有前提条件的情况下确实是O(n)但是这里的r(也就是上面答案中的maxlen),是不断往后走而不可能往前退的,它自身的值的变化是递增的。那么你可以明白,要进入while循环,i的值必然是比r大的,也就是说整个程序结束为止,while循环执行的操作数为n次(线性次),而字符串中的每个字符,最多能被访问到2次。时间复杂度必然为O(n)

 

终于讲完了,上模板题洛谷P3805

题目描述

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

字符串长度为n

输入输出格式

输入格式:

一行小写英文字符a,b,c...y,z组成的字符串S

输出格式:

一个整数表示答案

输入输出样例

输入样例#1: 
aaa
输出样例#1: 
3

说明

字符串长度len <= 11000000

 

代码:

#include<bits/stdc++.h>
#define il inline
#define ll long long
#define debug printf("%d %s\n",__LINE__,__FUNCTION__)
using namespace std;
const int N=23000005;
char s[N],news[N];
int p[N];
il int init()
{
    int len=strlen(s);
    news[0]='$',news[1]='#';
    int j=2;
    for(int i=0;i<len;i++)news[j++]=s[i],news[j++]='#';
    news[j]='\0';
    return j;
}
il int manacher()
{
    int len=init();
    int ans=-N,id,mx=0;
    for(int i=1;i<len;i++)
    {
        if(i<mx)p[i]=min(p[id*2-i],mx-i);
        else p[i]=1;
        while(news[i-p[i]]==news[i+p[i]])p[i]++;
        if(mx<i+p[i])id=i,mx=i+p[i];
        ans=max(ans,p[i]-1);
    }
    return ans;
}
int main()
{
    scanf("%s",s);
    printf("%d",manacher());
    return 0;
}

 

 

 

 

posted @ 2018-03-20 17:18  five20  阅读(494)  评论(0编辑  收藏  举报
Live2D