浅谈Manacher算法与扩展KMP之间的联系

首先,在谈到Manacher算法之前,我们先来看一个小问题:给定一个字符串S,求该字符串的最长回文子串的长度.对于该问题的求解,网上解法颇多,时间复杂度也不尽相同,这里列述几种常见的解法.

解法一

通过枚举S的子串,然后判断该子串是否为回文,由于S的子串个数大约为,加上每次判断需要的时间,所以总的时间复杂度为,空间复杂度为.

bool check(string &S, int left, int right)
{
    while (left < right && S[left] == S[right])
        ++left, --right;
    return left >= right;
}
int solution(string &S)
{
    int ans = 0;
    for (int i = 0; i < S.size(); ++i)
        for (int j = i; j < S.size(); ++j)
            if (check(S, i, j))
                ans = max(ans, j - i + 1);
    return ans;
}

解法二

我们也可以利用动态规划求解该问题。现假设得知S[i....j]是S的一个回文子串,那么,我们同样可以得到S[i+1.....j-1]也是S的一个回文字串,换句话说,我们可以通过已知的状态求解出未知状态。现定义dp[i][j]表示S以i为起点,j为终点的子串是否为回文,状态转移方程也很容易想到:

 && 

由于状态存在种,所以时间复杂度为,利用滚动数组,我们可以将空间复杂度降为.

int solution(string &S)
{
    vector<vector<bool> > dp(2, vector<bool>(S.size(), false));
    int ans = 0;
    for (int i = S.size() - 1; i >= 0; --i)
    {
        for (int j = i; j < S.size(); ++j)
        {
            dp[i & 1][j] = i <= j - 2 ? (S[i] == S[j] && dp[(i + 1) & 1][j - 1]) : S[i] == S[j];
            if (dp[i & 1][j])
                ans = max(ans, j - i + 1);
        }
    }
    return ans;
}

解法三

该解法是基于解法一的一种优化,在解法一中,check函数对于以i为起点,j为终点的回文子串,需要判断(j - i + 1) / 2次,但这里面也存在着某个子串不是回文,但也需要判断(j - i + 1) / 2次的情况,比如:aaabaa,aaaabcaaa....为了避免出现这种情况,我们可以去枚举回文子串的中点,然后以中点为中心,向两边扩展,这样就能避免上述的最坏情况。枚举子串中点时需要分长度为奇数和偶数的情况,具体的可以参考下这两组样例:aabaa,aabb。中点的个数存在个,每次以中点为中心向两边扩展最坏需要,所以总时间复杂度为,空间复杂度.

int solution(string &S)
{
    const int n = S.size();
    int ans = 0;
    for (int i = 0; i < n; ++i)
    {
        //for the odd case
        for (int j = 0; (i - j >= 0) && (i + j < n) && S[i - j] == S[i + j]; ++j)
            ans = max(ans, j << 1 | 1);
        //for the even case
        for (int j = 0; (i - j >= 0) && (i + 1 + j < n) && S[i - j] == S[i + 1 + j]; ++j)
            ans = max(ans, 2 * j + 2);
    }
    return ans;
}

解法四

在解法三中,当枚举以i中点的最长回文子串,需要以i为中点,向两边进行扩展,无疑,最坏情况下会退化到。这里,我们可以通过利用字符串的hash来降低时间复杂度(注:不熟悉字符串hash的朋友,可以参考下这篇博客点击打开链接,整理的很详细)。假设当前判断的是以i为中点偶数长度的最长回文,对于任意一个长度k,如果S[i
- k + 1....i]的hash值与S[i + 1.....i + k]的hash值不同,那么以i为中点的最长回文子串的长度必定小于2 * k,因此,可以通过该条件进行二分,这样就能在的时间范围内找到最优解。由于每次判断的时间复杂度只需要,所以该解法的时间复杂度为,空间复杂度为

const int BASE = 131, N = 1e+6 + 7;
typedef unsigned long long ULL;
//rec: record forward direction hash value
//rRec:record backward direction hash value
//P: record power of BASE
ULL rec[N], rRec[N], P[N];
int Bin(int len, int end, int rEnd, int __len)
{
    int l = 1, r = len;
    while (l <= r)
    {
        int mid = l + (r - l) / 2;
        ULL lHash = rec[end] - (end - mid >= 0 ? rec[end - mid] : 0) * P[mid];
        ULL rHash = rRec[rEnd] - (rEnd + mid < __len ? rRec[rEnd + mid] : 0) * P[mid];
        if (lHash ^ rHash)
            r = mid - 1;
        else
            l = mid + 1;
    }
    return r;
}
int solution(char *S)
{
    const int len = strlen(S);
    P[0] = 1ULL;
    //calculate power of BASE
    for (int i = 1; i < =len; ++i)
        P[i] = P[i - 1] * 131;
    rec[0] = S[0], rRec[len - 1] = S[len - 1];
    //calculate the string <span style="font-family:Microsoft YaHei;">hash </span>value
    for (int i = 1, j = len - 2; i < len; ++i, --j)
        rec[i] = rec[i - 1] * BASE + S[i], rRec[j] = rRec[j + 1] * BASE + S[j];
    int ans = 0;
    for (int i = 0; i < len; ++i)
    {
        int tmp;
        //for the even case
        tmp = Bin(min(i + 1, len - i - 1), i, i + 1, len);
        ans = max(ans, tmp << 1);
        //for the odd case
        tmp = Bin(min(i, len - i - 1), i - 1, i + 1, len);
        ans = max(ans, tmp << 1 | 1);
    }
    return ans;
}

上述代码有两个地方需要说明一下:1.无符号长整型溢出时,编译器会自动取模 2.关于计算P数组,如果是单case,P数组的求解可以放到solution函数中,如果是多case,P数组的求解必须放到外面,因为P数组只用计算一次就可以了.此种解法,能跑过POJ
3974和hdu 3068,感兴趣的朋友可以试试这种解法.

解法五

该问题也可以用后缀数组求解,在源字符串末尾添加一个源字符串中未出现过的字符,然后将源字符串的反转串连接在后面,那该问题就转换为在新得到的字符串中求解某两个后缀的LCP,而求解LCP是后缀数组典型的应用。由于后缀数组构造和实现,相比前面简述的几种方法,实现和理解相比之下要困难的多,这里就不做过多解释.

Manacher算法

前面简述了五种解法,而各种解法的时间复杂度、空间复杂度也不尽相同,这里在介绍一种时间复杂度、空间复杂度均为的算法:Manacher算法。该算法首先对源字符串进行处理,在源字符串的每一个字符前后加一个源字符串中未出现过的字符,例如源字符串为:aba,通过预处理后,源串变为:#a#b#a#。对于新得到的字符串,容易得知,该串没有长度为偶数的回文子串,因为串中没有相邻字符是相同的,这样就避免了讨论奇数、偶数的讨论。

现定义数组P[i] = x,表示以i为中心[i - x...i + x]是最长且回文的,那么就是源字符串的最长回文子串的长度,以字符串abaaba为例:

S :  #    a    #    b    #    a    #    a     #    b    #     a    #

P :  0    1    0    3    0     1    6    1     0    3     0     1    0

通过观察P数组,发现其最大值是6,而源串中的最长回文子串abaaba的长度也正好是6。现在,面临的问题是怎么求解P数组?

假设计算P[i]时,P[0..i - 1]已经计算好了,对于前面的P[x](0 <= x < i),定义一个框[x - P[x]...x + P[x]],right等于max{x + P[x]},center值为取到right时的x值。现在要计算P[i],对于i值,这里要分两种情况:

  1. i <= right:  先计算i关于center的对称点i‘ = 2 * center - i,根据回文串的对称性,从框左边left...i‘和i...right是一致的,如果P[i‘]的值能把i + P[i‘]的值限定在框里,那么P[i] = P[i‘],因为框里的东西已经比较过了。例如源串为babcbabcbaccba,现在要计算P[13]值,如下图所示:

    i = 13关于center的对称点是i‘ = 9,将[i‘ - P[i‘]......i + P[i]]子串取出(这里为了便于叙述,先假设i‘ - P[i‘] >L),得到如下的图:  
              通过对比上图可以发现,以i‘为中点的最长回文子串S[8..10]对应着S[12...14],也就是说,S[i‘ - P[i‘]...i‘ + P[i‘]]与S[i - P[i‘]...i + P[i‘]]一定是相等的(注:此处的前提条件是i‘ - P[i‘] >L),而且P[i]一定等于P[i‘],因为S[i + P[i‘] + 1] 一定不等于S[i - P[i‘] - 1],这在求P[i‘]时,就已经比较过了。当i‘ - P[i‘] <= L时,可以得到S[L...2 * i‘ -
    L]一定是回文子串,而S[L...2 * i‘ - L]恒等于S[2 * i - R....R],此时,P[i]的值至少是R - i,而大于right部分的,都是没有比较过的,所以只能以i为中点,以R - i + 1为半径向两边扩展。结合i‘ - P[i‘] > L和i‘ - P[i‘] <= L的情况,可以发现P[i]的值至少等于min(P[i‘], R - i),所以,在i < right的情况下,使P[i] = min(P[i‘], R - i),然后以i为中心,P[i]为半径,向两边扩展,并更新相应的center和right值即可.

  2. i > right: 这种情况下,只能以i为中心,向两边扩展,并更新相应的center和right值。

复杂度分析

计算过程中,需要用到额外的P数组,而right的值只能增加n次,所以该算法的时间、空间复杂度均为

const int N = 1e+6 + 7;
char orign[N << 1];
int P[N << 1];
int Manacher(char *S)
{
    int len = strlen(S);
    S[len << 1] = '#', S[len << 1 | 1] = '\0';
    for (int i = len - 1; i >= 0; --i)
        S[i << 1 | 1] = S[i], S[i << 1] = '#';
    int center = 0, right = 0, ans = 0;
    len <<= 1;
    for (int i = 0; i <= len; ++i)
    {
        P[i] = i <= right ? min(P[2 * center - i], right - i) : 0;
        while (i - P[i] - 1 >= 0 && i + P[i] + 1 <= len && S[i - P[i] - 1] == S[i + P[i] + 1])
            ++P[i];
        if (i + P[i] > right)
            right = i + P[i], center = i;
        ans = max(ans, P[i]);
    }
    return ans;
}

前面已经讲述了Manacher算法的工作原理,这里谈一下Manacher算法的应用.

应用一:回文子串个数

对于一个给定串S,能否在线性时间复杂度内求出该字符串有多少个回文子串?

答案是肯定的,统计存在多少个回文子串,只用统计以i(0 <= i < len(S))为中心、长度为偶数的回文子串数量和以i为中心长度为奇数的回文子串数量,然后累加即可。假设现在需要求解以i为中心、长度为奇数的回文子串数量,只需要找到以i为中心、长度为奇数的最长回文子串的长度值,然后将长度值加一除2,即为所求的解,偶数的处理方式一样。而在求解最长回文子串的长度时,计算出来的P[i]值,就已经计算出了以源串所有点为中心、长度分别是偶数和奇数的最长回文子串的长度,只需要线性遍历一遍P[i]数组,将(P[i]
+ 1) / 2的值累加,就是S的回文子串的个数。

应用二:扩展KMP

给定一个串S[0...n],能否在线性时间复杂度内求出P[i] = LCP(S[i...n],S[0...n])(1 <= i <= n)?

对于该问题,我们可以套用Manacher算法。现计算P[i],假设P[1...i - 1]都已经计算好了,设定right为max(P[x] - 1 + x)(1 <= x < i),left为取到right值时的x值。(1)当right >= i时,通过已经计算出来的P[1..i-1]值,我们可知S[left....right] = S[0...right
- left],找到i的位置相当于S串的开头的位置:i‘ = i - left,如果i + P[i‘] <= right,那么很容易得出P[i] = P[i‘],如果i + P[i‘] > right,那么P[i]的值至少为right - i + 1,综上两个情况,易知P[i]值至少为min(P[i‘],right - i + 1),然后暴力比较,并更新相应的left和right。由于right的值只能增加n次,所以该算法是

当然了,上述的算法并不局限于与串自身匹配LCP,比如给定两个串S、T,要在S串中查找是否出现T串,用一个在S、T中都没有出现的字符连接T和S,这里假设为‘#‘,得到新串T#S,最后在判断P数组中是否存在值为len(T)的元素就能判断T串是否在S串中出现。而在扩展KMP中,对于模式串T,需要计算LCP(S[i...n],T)(0 <= i <=
n),同样可以利用上述的方法在的时间范围内求解,这也是Manacher算法与扩展KMP算法的相似之处。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-08-05 13:55:35

浅谈Manacher算法与扩展KMP之间的联系的相关文章

hdu1875浅谈prim算法的朴素实现

阅读原题 题目大意 给你几个(<=100)小岛的坐标,然后你把所有的岛都修上桥连接起来,求最小花费,还有个附加的限制:只有岛之间的距离大于等于10,或小于等于1000时才能修桥. 大概是因为十米以内不用建桥,千米以上无法建桥.哈哈,说着玩的. 很明显这是一道MST(最小生成树)的题目,貌似也有人用并查集AC过. 最小生成树算法 概述 最小生成树的常用算法有两个kruskal和prim算法.两者都是不停地执行归并操作,然而一言以蔽之,两者的不同之处在于:kruskal----归并边:prim---

浅谈分词算法(1)分词中的基本问题

[TOC] 前言 分词或说切词是自然语言处理中一个经典且基础的问题,在平时的工作中也反复的接触到分词问题,用到了不同的模型,不同的方法应用在各个领域中,所以想对分词问题做一个系统的梳理.大多数分词问题主要是针对类似汉语.韩语.日语等,词语之间并没有天然的分割,而像英语等,句子中是带有天然的分割的.但是英语也会涉及到分词问题,比如实体识别.词性标注等内容.而本系列文章更多的是讨论汉语中的分词问题,首先我们从分词问题的基本出发,之后从传统的词典分词到将分词转为序列标注问题的分词,以及最新的结合深度学

浅谈欧洲算法——模拟退火

初听说退火这个名词感觉就很(zhuang)帅(A__CDEFG...) 直到学了退火之后,我才发现: 退火不只是帅,而且非常万能 甚至比 D (大) F (法) S (师)还要万能 简直就是骗(de)分神器啊 简介 作为一个计算机算法,它竟然在百度上有物理词条! 当时我看了就懵了,你说计算机一个算法,跟冶炼金属有什么关系啊? 后来我看了算法的词条... 是不是更懵了... 方便大家理解(变得更懵),我搬了百度上的定义: Simulate Anneal Arithmetic (SAA,模拟退火算法

浅谈分词算法(2)基于词典的分词方法

[TOC] 前言 在浅谈分词算法(1)分词中的基本问题中我们探讨了分词中的基本问题,也提到了基于词典的分词方法.基于词典的分词方法是一种比较传统的方式,这类分词方法有很多,如:正向最大匹配(forward maximum matching method, FMM).逆向最大匹配(backward maximum matching method,BMM).双向扫描法.逐词遍历法.N-最短路径方法以及基于词的n-gram语法模型的分词方法等等.对于这类方法,词典的整理选择在其中占到了很重要的作用,本

浅谈字符串算法(KMP算法和Manacher算法)

[字符串算法1] 字符串Hash(优雅的暴力) [字符串算法2]Manacher算法 [字符串算法3]KMP算法 这里将讲述  字符串算法2:Manacher算法 问题:给出字符串S(限制见后)求出最大回文子串长度 Subtask1  对于10%的数据 |S|∈(0,100] Subtask2  对于30%的数据|S|∈(0,5000] Subtask3 对于100%的数据|S|∈(0,11000000] Subtask1(10pts):最朴素的暴力 枚举字符串的所有子串,判断其是否回文,时间复

浅谈聚类算法(K-means)

聚类算法(K-means)目的是将n个对象根据它们各自属性分成k个不同的簇,使得簇内各个对象的相似度尽可能高,而各簇之间的相似度尽量小. 而如何评测相似度呢,采用的准则函数是误差平方和(因此也叫K-均值算法): 其中,E是数据集中所有对象的平方误差和,P是空间中的点,表示给定对象,mi为簇Ci的均值.其实E所代表的就是所有对象到其所在聚类中心的距离之和.对于不同的聚类,E的大小肯定是不一样的,因此,使E最小的聚类是误差平方和准则下的最优结果. 选取代表点用如下几个办法: (1)凭经验.根据问题性

从数组循环左移问题中浅谈考研算法设计的规范代码

问题:设将n(n>1)个整数存放到一维数组R中.设计一个算法,将R中的序列循环左移p(0<p<n)个位置,即将R中的数据由{X0,X1,...,Xn-1}变换为{Xp,Xp+1,...,Xn-1,X0,X1,...,Xp-1}.要求:写出本题的算法描述. 分析: 本题不难,要实现R中序列循环左移p个位置,只需先将R中前p个元素逆置,再将剩下的元素逆置,最后整体逆置操作即可.本题算法描述如下: 1 #include <iostream> 2 using namespace st

浅谈分支限界算法

1. 定义: 分支限界算法是按照广度优先的方式对解空间树(状态空间树)进行搜索,从而求得最优解的算法.在搜索的过程中,采用限界函数(bound function)估算所有子节点的目标函数的可能取值,从而选择使目标函数取极值(极大值或者极小值)的节点作为扩展结点(如果限界值没有超过目前的最优解,则剪枝)进行下一步搜索(重复 BFS -> 计算所有子节点限界 -> 选择最优子节点作为扩展结点的过程),从而不断调整搜索的方向,尽快找到问题的最优解. (ps:回溯算法求出满足约束的所有可行解,分支限界

浅谈排序算法

桶排序(BucketSort) 排序过程: 假如我们现在要排序的一组数为:5,3,5,2,8. 这组数都在0-10的范围之内.这个时候,我们可以拿11个桶,标号为0,1,2,3......10.也就是定义长度为11的数组.现在我们来遍历这些数字,第一个数字为5,那么给第五号桶中插一个小红旗,第二个数字为3,给第三号桶插一个小红旗,以此类推.其中,插入一个小红旗代表的是数组元素+1(开始初始化数组元素都为0),遍历完成之后,可以查看所有桶中小红旗的数量,也就是数组中存储元素的个数.发现a[5] =