• 关于 manacher


    CSDN同步

    这一节我们试图解决这样一个问题:

    给定一个长度为 (n) 的串 (s),求其最长回文子串。(n leq 10^7).

    大体分析

    貌似有 (n^2) 个供选择的子串,于是似乎很难有线性的做法。

    算法一 暴力 (mathcal{O}(n^3))

    比较屑。把 (n^2) 个子串枚举出来,一个个验证。

    算法二 动态规划 (mathcal{O}(n^2))

    很显然不能考虑去枚举子串了。

    考虑快速判断回文。假设我们已经知道 (s_{i cdots j})回文性(即是否回文),如何知道 (s_{i-1 cdots j+1}) 的回文性?

    很显然。如果用 ( ext{hw}(s) = 0 / 1) 表示其回文性,(1) 为回文的话:

    [ ext{hw}(s_{i-1 cdots j+1}) = ext{hw}(s_{i cdots j}) | (s_{i-1} == s_{j+1}) ]

    (|) 就是位运算中的 (|) 符号。

    很显然,只有 (s_{i cdots j}) 回文且多出的一位可以匹配成功的情况下,(s_{i-1 cdots j+1}) 才是回文。

    这样我们可以得到一个有些玄学的 “( ext{dp})” 做法。

    考虑求出所有的 ( ext{hw}) 值,利用刚才的动态规划转移方程,把这玩意儿搞成 区间动规,于是可以 (mathcal{O}(n^2)) 完事了。

    算法三 朴素做法 (mathcal{O}(n^2))

    基于算法二,考虑一个不基于动态规划,而基于暴力的做法。

    比如 abbcbbc 中,b , bcb , bbcbb 均为回文子串,其特点在于 中心一样

    于是我们可以枚举中心字符,然后不断向两边扩展,这个思路也很好理解。

    但是对于偶数个字符的问题,貌似解决不了。

    一个方法是,枚举完奇数之后,枚举 (s_i = s_{i+1}) 的所有 (i),再向两边扩展。

    这个方法比较易懂一些。但是下面我们要介绍一个,更贴合 ( ext{manacher}) 特点的解决方案。

    考虑本来的串是 abbcbbc,对于偶数回文子串 bb,考虑只枚举一个中心就做完的方案。

    也就是这样操作:将原串每两个字符的间隔内加上一个新的,原串中没有的字符,一般用 # 表示。即

    $ exttt{abbcbbc} ightarrow $ #( exttt{a})#( exttt{b})#( exttt{b})#( exttt{c})#( exttt{b})#b#( exttt{c})#

    这样你会发现一件事。对于原来就有的字符,枚举其中心的做法没有问题,只不过要对长度进行处理罢了,这样解决了奇数回文的情况;而对 # 的中心扩展方案,则是解决了偶数回文的情况。

    这样子串长度翻了一倍,但循环一遍即可解决,较为简单。

    于是,我们管这个算法叫 “朴素算法”。虽然 (mathcal{O}(n^2)) 的复杂度不优于上述动态规划的复杂度,但在思维上已经跨出了一大步。

    算法四 ( ext{manacher} space mathcal{O}(n))

    质的飞跃即将到来了。

    先着手解决奇数串。因为上述添加 # 的操作,偶数串用同样的代码即可求出。

    (d) 表示以 (i) 为中心的奇数串的个数,方便记忆。

    下面考虑 (d_{i-1} ightarrow d_i) 怎么做。为方便,我们令 (l,r) ,其中 (r) 是当前回文子串最右侧的位置,(l) 为其对应的子串左端点。

    如果 (i>r),我们只能调用朴素算法。因为与前面的元素无法形成联系。

    另外 (i leq r) 的情况比较难解决。

    由于 (i) 包含在 (s_{l cdots r}) 的回文串中,则必然存在 (s_i = s_{l+r-i}),并且 在该范围内,以 (l+r-i) 为中心的回文子串必然也有与其对应的以 (i) 为中心的回文子串。注意 “在该范围内”。此时令 (j = l+r-i).

    于是是否意味着 (d_i = d_j) 呢?不是的。

    因为很有可能,以 (j) 为中心的最长回文子串的左端点超出了 (l),此时就不一定会产生对应关系。也就是 (j - d_j < l) 的时候,需要分类讨论。

    考虑这个时候,我们用朴素算法求解。也就是暴力拓展。

    无论什么情况,不要忘记维护 (l,r) 的值。

    算法讲完了。可你觉得这是对的么?

    时间复杂度证明

    简单地证明,( ext{manacher}) 算法的时间复杂度是线性 (mathcal{O}(n)) 的。

    因为很显然,(r) 只会不断变大(或不变)。每一次朴素做法都是将 (r) 不断扩大,再至多从 (r) 开始继续暴力搜。

    这样 (r uparrow) 的复杂度是 (mathcal{O}(n)) 的,往左扫的次数和往右扫一样,也是 (mathcal{O}(n)) 的,其余部分也都是线性的。

    这样最终的复杂度就是 (mathcal{O}(n)) 了。

    于是我们成功的在 (mathcal{O}(n)) 的时间内解决了最长回文子串问题。

    简易的代码胜过复杂的说教。
  • 相关阅读:
    Android Activity与Service的交互方式
    Android Service和Thread的区别
    Android Binder机制简单了解
    Android内的生命周期整理
    Android App的生命周期是什么
    ListView item 中TextView 如何获取长按事件
    Go之并发处理(售票问题)
    Go之简单并发
    Go之函数直接实现接口
    Go之类型判断
  • 原文地址:https://www.cnblogs.com/bifanwen/p/14327157.html
Copyright © 2020-2023  润新知