经典问题与算法:最长回文子串问题与Manacher算法

问题描述:给定一个字符串,求出其最长回文子串的长度
例如:对于字符串s="acaacdbab"而言,其回文子串分别为"caac"和"bab",其中最长回文子串长度为4

解法一:中心扩展法

对于这样的一个问题,一般暴力算法主要是枚举所有子串的中心位置,并在该位置上进行扩展,记录并更新最长回文子串的距离。代码实现如下:

#include "pch.h"
#include <iostream>
using namespace std;
int LongestPalindrome(const char* s, int n) {
    int i, j, c, length=0;
    if (s == 0 || n < 1)
        return 0;
    for (i = 0; i < n; i++) {
        for (j = 0; (j <= i) && (i + j <= n); j++) {
            if (s[i + j] != s[i - j])
                break;
            c = 2 * j + 1;
        }
        if (length <= c)
            length = c;
        for (j = 0; (j <= i) && (i + j <= n); ++j) {
            if (s[i - j] != s[i + j + 1])
                break;
            c = 2 * j + 2;
        }
        if (length <= c)
            length = c;
    }
    return length;
}
int main()
{
    char str[] = "acaacdbab";
    cout << LongestPalindrome(str, sizeof(str) / sizeof(char) - 1) << endl;
}

从代码实现上可以看出暴力算法主要存在两个缺陷:
1、由于回文子串可以分为偶回文和奇回文两种,例如abba是偶回文,其对称中心是bb,而aba则奇回文,其对称中心是b,二者判断逻辑是不同的,所以需要分开进行处理;
2、枚举中心采用了逐个枚举的方式,并没有有效利用到回文子串的对称性,使得代码无法高效地运行

解法二:Manacher算法

Manacher算法是对暴力算法的一个优化。面对暴力算法的第一个缺点,Manacher算法采用了一种方法将所有的偶回文和奇回文都转化成为了奇回文,从而简化了代码的判断逻辑。具体的做法是这样的:在字符串的首尾,以及每两个字符中间插入一个源字符串没有的特殊符号,使得任意长度的回文子串都可以转化为奇回文。举个例子,在源字符串中,有"cabbac"和"bab"两个回文子串,经过处理后会转变为"#c#a#b#b#a#c#"和"#b#a#b#",全部都转化为了奇回文。同时为了更好地处理数组的越界问题,在数组的首部添加一个哨兵位$。
而对于第二个缺点,则是通过定义了一个回文半径数组int P[],其中P[i]表示以字符Si为中心的最长回文子串向左或向右扩张的长度,例如

P数组求解实例.png
若假设原回文串的长度为n,不管是偶回文还是奇回文,扩展后的回文串长度都为2n+1,而P[i]保存回文半径(包括S[i]在内),则显然满足关系:(P[i]-1)*2+1=2n+1,解得P[i]-1=n。因此,要求最长回文子串长度,只需求P[i]-1的最大值即可。

在明白了如何利用P[i]求出原始回文子串的长度后,算法接下来的重点便是如何求解P数组
Manacher算法增加了两个变量id和mx,其中id代表最长回文子串的中心字符位置,mx代表最长回文子串的右边界位置,即mx=P[id]+id。因此,根据回文子串的对称性,我们可以得出一个重要的结论:若mx>i,则有P[i]≥min(P[2id-i],mx-i)*
证明如下:
当mx>i时,不妨记mx的对称点为my,再令j=2*id-i,则回文串可分为两种情况:

  • 1、当mx-i>P[j],以S[j]为中心字符的回文子串被包含在以S[id]为中心字符的回文子串当中,根据回文子串的对称性,必有有P[j]=P[i],如下图:
    情况1.png
  • 2、当mx-i≤P[j]时,以S[j]为中心的回文子串只有部分包含在最长回文子串内部(即图中红框位置),因此可以推断P[i]≥mx-i,至于超出mx后面的部分则需要另行匹配,如下图:
    情况2.png

综合以上两种情况,便可以得出P[i]=min(P[2*id-i],mx-i)这一重要结论
当mx≤i时,我们无法利用最长回文子串的对称性对P[i]的值做出更多假设,因此P[i]=1,然后继续进行中心扩展匹配。
代码实现:

#include "pch.h"
#include <iostream>
#include <algorithm> 
using namespace std;
char str[] = "$#a#c#a#a#c#d#b#a#b#";
int Manacher(const char* str,int* P,int len) {
    int mx = 0;
    int id;
    int max_len = 0;
    for (int i = 1; i < len; i++){
        if (mx > i)
            P[i] = min(P[2 * id - i], mx - i);
        else
            P[i] = 1;
        while (str[i - P[i]] == str[i + P[i]])
            P[i]++;
        if (P[i] + i > mx) {
            mx = P[i] + i;
            id = i;
        }
        max_len = max(max_len, P[i] - 1);
    }
    return max_len;
}
int main()
{
    int length = sizeof(str) / sizeof(char) - 1;
    int* P = new int[length];
    memset(P, 0, length*sizeof(int));
    cout << "最长回文子串长度为:" << Manacher(str, P, length) << endl;
    delete[] P;
    return 0;
}
算法复杂度分析

显然,对于Manacher算法,其空间复杂度为O(n),而对于时间复杂度而言,我们考虑以下两种极端情况:
1、字符串中不含任何回文子串,例如abcdefg,在这种情况中,由于while中的条件始终不满足,因此,循环执行的次数应当为2(n-1),故此时时间复杂度为O(n);

2、设1个仅由一个字符组成的字符串str,长度为n,则经过扩展之后,长度为2n+2。由于这个字符串仅包含一个字符,故整个字符串的任意子串均为回文串,在这种情况下,while的执行次数达到最多的情况。在计算P[i],由于对称性,仅需计算P[1....n+1]的值即可,其计算规律如下表所示:
复杂度分析.png
从表中可以看出:

任取i∈{1,...,n+1},都满足P[i]=i,id=i,mx=2i>i
因此当i>2时,有P[i]=min(P[2id-i],mx-i)=min(P[2(i-1)-i,2(i-1)-i)成立,
令j=2
id-i,则有P[j]=j,进而有

  • P1[i]=min(P[2(i-1)-i,2(i-1)-i)=min(2(i-1)-i,2(i-1)-i)=i-2

注:这是经过if判断语句后得到的P[i]初始值,为了避免和最终P[i]值相混淆,此处记为P1[i],后续均以P1[i]代表初始值,P[i]代表最终值

随后程序进入了while循环,由于P1[i]=i-2,P[i]=i,因此可以很容易看出while循环体最多仅执行2次,故虽然代码有2层循环,但是在最坏的情况下,内存while循环最多对n-1个字符进行了2次循环(当i=1时,循环不执行;当i=2时,循环执行1次),其余字符均不进行循环,因此整体代码的时间复杂度是O(n)。由于最坏与最好的情况下,代码的时间复杂度均为O(n),因此代码的平均时间复杂度为O(n)。

最后我测试了几组极端数据,所测试结果与最差情况下的证明结果相吻合,测试结果如下:
测试结果.png
注:其中count变量为计数器,用于计算对于每个字符,while的执行次数。原始字符串长度为n=7,扩展后的字符串长度为16(包括$符),当i=1时,内层循环执行0次;当i=2时,内层循环执行1次,当i>8(n+1)时,内存循环体均不执行,当i=(3...n+1),内层循环执行次数均为2次。

这也正是Manacher算法的精彩之处,巧妙地利用了之前所有计算出来的P[1...i-1]的值,在每次循环中对P[i]进行快速赋值,避免P[i]的值每次都从1开始计算,大幅削减了比较次数,最终使得求解最长回文子串长度达到了线性级复杂度O(n),同样思想也被运用在KMP算法中。

总结:

1、对于数组,字符串的一些问题,在需要分为奇数、偶数的情况下进行讨论时,往往可以采用填充的方式,将其转化为奇数的情况,从而简化判断逻辑。同样的做法也可以放入到一些树的判断当中,通过填充特殊结点,使一棵普通二叉树转变为完全二叉树,甚至是满二叉树,来使得问题简化;
2、对于出现循环嵌套,且内层循环的判断条件与输入数据相关,在判断时间复杂度时,可以考虑判断最差与最优情况,若最优与最差时间复杂度相同,则一般情况下的时间复杂度应当与最优或最差的时间复杂度相同。(有点类似于数学上的夹逼定理,可以将求时间复杂度的过程近似地看成是求极限的过程)

参考资料:
1、《Manacher 算法》:https://subetter.com/articles/manacher-algorithm.html
2、《leetCode_5_Longest_Palindromic_Substring》:http://windliang.cc/2018/08/05/leetCode-5-Longest-Palindromic-Substring/
3、《编程之法——面试和算法心得》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容