HDU - 3068 最长回文(马拉车算法模板题)

HDU原题链接:传送门

最长回文

Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 33823 Accepted Submission(s): 12368

Problem Description

给出一个只由小写英文字符a,b,c...y,z组成的字符串S,求S中最长回文串的长度.
回文就是正反读都是一样的字符串,如aba, abba等

Input

输入有多组case,不超过120组,每组输入为一行小写英文字符a,b,c...y,z组成的字符串S
两组case之间由空行隔开(该空行不用处理)
字符串长度len <= 110000

Output

每一行一个整数x,对应一组case,表示该组case的字符串中所包含的最长回文长度.

Sample Input

aaaa

abab

Sample Output

4
3

题目分析——马拉车算法

本题是Manacher(马拉车)算法的一题模板题,解决一个字符串的最长回文子串的问题,解题之前需要讲述一下马拉车算法的原理,我找了一篇讲解这个算法比较好的博客:传送门 参考前辈的文章后,在此也做出一些自己的总结,以免时间久了自己产生遗忘,当然马拉车算法代码量极少,但是其思想却需要仔细揣摩,做好准备我们就开始吧!

首先对于一个给出的字符串,例如aba,abba,abababadfsaa等,最直观的想到的去求它的最长回文子串的方法是从左到右遍历每一个字符,以这个字符作为回文串的中心,然后不断向两边扩充,直到不满足后遍历下一个字符,求出最长的回文子串的长度。但是这么做在字符串长度很长时效率很低,花费大量的时间。

在介绍马拉车算法之前,有必要先提及一个很巧妙且重要的方法,不知你是否发现,通过上述的方法求回文子串,我们总是以一个字符为出发点向两边匹配,那么每次所匹配出来的回文串都是奇数,而无法判断abba这样的偶数回文串,所以就产生了一种拼接字符串的神奇方法,将一个初始的字符串用一个不会出现的字符隔开,例如#a#b#a#,#a#b#b#a#,你惊奇的发现,无论是奇数还是偶数的字符串经过这么处理之后不但不会改变它原有的对称特性(只是长度好像变长了一倍?因为#也是一个字符,且它的存在不会打断原有的回文特性,只是增长了而已,放心下面会讲明它变长的长度是有着一个固定的数学关系的),而且所有的串都变成了奇数串(奇+偶=奇,偶+奇=奇),秒啊!

那么我们得到了一个类似#a#b#a#的串之后该怎么做呢,别急,我们需要将这个字符再做一点处理,在它最前面插入一个不会出现且不同于#的字符,就像$#a#b#a#这样,这么做有一个好处,就是我们将实际会用于算法求解的字符从下标0开始转化成了从下标1开始,后面我们遍历时也是从下标1开始的,这样第一个有效字符的位置就是s[1],第二个就是s[2],方便后面我们使用马拉车算法进行推导。

对于一个字符串如:$#a#a#b#a#a#,我们设P[i]为以第i个字符为回文串中心时最长的回文半径(包含S[i]本身),那么遍历P[i]就可以找出最长的回文子串的长度

下标 0 1 2 3 4 5 6 7 8 9 10 11
S $ # a # a # b # a # a #
P 1 1 2 3 2 1 6 1 2 3 2 1
下标 0 1 2 3 4
s a a b a a
p 1 1 3 1 1

此时就会有人提出疑问,这个是我拼接之后的字符串呀,求出它的最长回文子串的长度与求拼接前的最长回文子串长度有什么关系吗?有关系,你发现上述拼接后的字符串最长回文子串是以i==6为中心的,就是以b这个字符为中心,那么你惊奇的发现原字符串最长回文子串一定也是以b为中心的,因为我们上面就提到了,拼接的#不会影响原字符串的回文特性,只是让它们在原先的基础上都变长了而已,而它们之间的关系很显然是2倍关系P[i]==2*p[i],由于题目所求的是原字符串的最长回文子串的长度,那么答案就是P[i]-1,因为我们自始自终用拼接后的字符串参与推导,所以只要关注P[i]数组即可。

那么所有问题的矛头都指向一处!p[i]怎么求(下面都用小写的p代表拼接后字符串的以i为回文中心的最长回文半径,频繁切换大写让我觉得有些异样w(゚Д゚)w),马拉车算法,启动!因为我们是从左到右遍历i的,在计算p[i]时我们要确保p[1]~p[i-1]已经求出,这样才能借助回文串的对称特性,也是就前面求出来的p[j]减少后面p[i]的匹配次数(此时你可能不明白这句话的含义,且仔细往下看),我们要用到两个辅助的变量id和mx,id为i之前的一个能向右伸展的最远的回文串的回文串中心,mx为以id为回文中心的这个回文串往右(往左)能伸展到的最远的距离

还是借助一下图来更加清晰的展示

m1.png

其中j是i关于id的对称点,j=2*id-i,这个自行推一下便可,那么此时上图展示的只是其中一种情况,当以i为回文中心的时i点在左侧一个向右伸展足够长至把i点包括其中的回文串内,即i<mx,当这种情况时,又分为两种小的情况

  1. 当以p[j]即p[2*id-i]为中心的回文串长度没有超过以id为对称中心的回文串的左边界
m2.png

由于以i和j为中心的回文串是关于id对称的两个子串,所以p[i]自然而然和p[j]相等,即p[i]=p[2*id-i]

  1. 当以p[j]为中心的回文串左边界超过以id为回文中心的回文串的左边界
m3.png

左边的绿色线代表超过id左边界的回文串,此时,由于对称关系,只能得知以i为中心的回文串在id右边界之内的部分和以j为中心的回文串在id左边界之内的部分是对称的,而以i为中心的超过右边界的部分则只能老老实实向两侧匹配,所以在这两种情况下,如果p[2 * id-i]>=mx-i+1,则说明超过左边界,则只取用边界内的对称部分p[i]=mx-i+1,如果p[2 * id-i]<mx-i+1,说明不超过左边界,则由于p[2 * id-i]和p[i]对称,直接继承,p[i]=p[2 * id-i]。

//在借助回文串i前已经计算出的数据减少匹配次数后,依旧要老老实实调用下面的函数去匹配
while(i-p[i]>=1&&i+p[i]<=len*2+1){    //下标向左不越界,下标向右不越界
    if(s[i-p[i]]==s[i+p[i]]) p[i]++; //初始化p[i]都是1,至少回文半径是1
    else break;
} 

不要忘记还有一种大情况,就是i点在mx的边界或者外面时

m4.png
m5.png

在这两种情况下,p[i]是否对称都要从超过id右边界(mx)的地方开始一个一个首尾比较,所以没有办法借助p[j]来减少预匹配的次数,直接调用上面的函数老老实实匹配吧

要注意的是随着每一次i的遍历之后,如果mx能更加向右延伸,即p[i]+i-1>p[id]+id-1,-1是因为重复加点了,仔细推理一下便可,例如id=5,p[id]=3,则mx=p[id]+id-1=7,那么要更新id的位置为i,更新mx为p[i]+i-1,确保id永远是右边界最接近下一个要遍历的i点那个对称中心

示例代码

#include<iostream>
#include<string>
#include<cstring>
#include<string.h>
#include<cstdio>
#include<queue>
#include<algorithm>
using namespace std;

const int N=110000+10;
char s[N*2];
int p[N*2];

int main(){
    while(scanf("%s",s)!=EOF){
        int len=strlen(s);
        for(int i=len;i>=1;i--){
            s[i*2+1]='#';
            s[i*2]=s[i-1];
//          s[i*2-1]='#';   具体的拼接字符串的方法每个人写的都有些细微的差别 
        }
        s[1]='#';
        s[0]='$';
//      printf("%s\n",s);
//      memset(p,0,sizeof(p));
        int maxx=1;         //用于统计最后的输出结果 
        fill(p,p+2*N,1);
        int id=1,mx=1;      //mx=id+p[id]-1==1 代表i前的以id为中心的回文串可以达到的最右边的边界 
        for(int i=2;i<=len*2+1;i++){
            if(i<mx){
                p[i]=min(p[2*id-i],mx-i+1); //i关于id的对称点2*id-i,回文串半径可能超过mx-i+1,有可能更短 
            }
            //上述if语句借助了回文串的对称特性减少匹配次数
            //此时如果p[2*id-i]>=mx-i+1,则相对的i的回文串半径也可能更长
            //或者i本身就在mx的外面,则无法借助上述if语句减少以i为中心的回文串的匹配次数
            while(i-p[i]>=1&&i+p[i]<=len*2+1){
                if(s[i-p[i]]==s[i+p[i]]) p[i]++;
                else break;
            } 
            //需要i每往后一次就要实时更新id和mx的大小
            if(p[i]+i-1>mx){
                mx=p[i]+i-1;
                id=i; 
            } 
            if(p[i]>maxx) maxx=p[i];
        }
        printf("%d\n",maxx-1);
    }
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容