「转载」Manacher算法-查找最大回文子串的线性方法
前言
转载自剑指 Offer II 020. 回文子字符串的个数的官方题解
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/a7VOhD/solution/hui-wen-zi-zi-fu-chuan-de-ge-shu-by-leet-ejfv/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
剑指 Offer II 020. 回文子字符串的个数
问题描述:
给定一个字符串 s
,请计算这个字符串中有多少个回文子字符串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例:
1 | 输入:s = "aaa" |
方法一:中心拓展
思路与算法
计算有多少个回文子串的最朴素方法就是枚举出所有的回文子串,而枚举出所有的回文字串又有两种思路,分别是:
- 枚举出所有的子串,然后再判断这些子串是否是回文;
- 枚举每一个可能的回文中心,然后用两个指针分别向左右两边拓展,当两个指针指向的元素相同的时候就拓展,否则停止拓展。
假设字符串的长度为 。我们可以看出前者会用 的时间枚举出所有的子串 ,然后再用 的时间检测当前的子串是否是回文,整个算法的时间复杂度是 。
而后者枚举回文中心的是,对于每个回文中心拓展的次数也是 的,所以时间复杂度是 。所以我们选择第二种方法来枚举所有的回文子串。
在实现的时候,我们需要处理一个问题,即:如何有序地枚举所有可能的回文中心,我们需要考虑回文长度是奇数和回文长度是偶数的两种情况。如果回文长度是奇数,那么回文中心是一个字符;如果回文长度是偶数,那么中心是两个字符。当然你可以做两次循环来分别枚举奇数长度和偶数长度的回文,但是我们也可以用一个循环搞定。我们不妨写一组出来观察观察,假设 ,我们可以把可能的回文中心列出来:
编号 | 回文中心左起始位置 | 回文中心右起始位置 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
2 | 1 | 1 |
3 | 1 | 2 |
4 | 2 | 2 |
5 | 2 | 3 |
6 | 3 | 3 |
由此我们可以看出长度为的字符串会生成 组回文中心 ,其中,。这样我们只要从 到 遍历 ,就可以得到所有可能的回文中心,这样就把奇数长度和偶数长度两种情况统一起来了。
代码
1 | class Solution { |
复杂度分析
时间复杂度:。
空间复杂度:。
方法二:Manacher 算法
思路与算法
Manacher 算法是在线性时间内求解最长回文子串的算法。在本题中,我们要求解回文串的个数,为什么也能使用 Manacher 算法呢?这里我们就需要理解一下 Manacher 的基本原理。
Manacher 算法也会面临「方法一」中的奇数长度和偶数长度的问题,它的处理方式是在所有的相邻字符中间插入 ##,比如abaaabaa
会被处理成 #a#b#a#a##a#b#a#a#
,这样可以保证所有找到的回文串都是奇数长度的,以任意一个字符为回文中心,既可以包含原来的奇数长度的情况,也可以包含原来偶数长度的情况。假设原字符串为 ,经过这个处理之后的字符串为 。
我们用 来表示以 的第 位为回文中心,可以拓展出的最大回文半径,那么 就是以 为中心的最大回文串长度 (想一想为什么)。
Manacher 算法依旧需要枚举 的每一个位置并先假设它是回文中心,但是它会利用已经计算出来的状态来更新 ,而不是向「中心拓展」一样盲目地拓展。具体地说,假设我们已经计算好了区间内所有点的 (即我们知道 这些点作为回文中心时候的最大半径), 那么我们也就知道了 拓展出的回文达到最大半径时的回文右端点。例如 的时候 ,说明以第 个元素为回文中心,最大能拓展到的回文半径是 ,此时右端点为 。所以当我们知道一个 对应的 的时候,我们就可以很容易得到它的右端点为 。
Manacher 算法如何通过已经计算出的状态来更新 呢?Manacher 算法要求我们维护「当前最大的回文的右端点 」以及这个回文右端点对应的回文中心 。我们需要顺序遍历 ,假设当前遍历的下标为 。我们知道在求解 之前我们应当已经得到了从所有的 ,并且当前已经有了一个最大回文右端点 以及它对应的回文中心。
初始化
如果 ,说明 被包含在当前最大回文子串内,假设 是 关于这个最大回文的回文中心 的对称位置(即 ),我们可以得到 至少等于,也即。这里将 和 取小,是先要保证这个回文串在当前最大回文串内。(思考:为什么 有可能大于 ?)
如果 ,那就先初始化 。
中心拓展
做完初始化之后,我们可以保证此时的 ,要继续拓展这个区间,我们就要继续判断 和 是否相等,如果相等将 自增;这样循环直到 ,以此类推。
我们可以看出循环每次结束时都能保证 ,而循环继续(即可拓展的条件)一定是 。
这个时候我们需要注意的是不能让下标越界,有一个很简单的办法,就是在开头加一个 $$,并在结尾加一个 !!,这样开头和结尾的两个字符一定不相等,循环就可以在这里终止。
这样我们可以得到 所有点为中心的最大回文半径,也就能够得到 中所有可能的回文中心的的最大回文半径,把它们累加就可以得到答案。
代码
1 | class Solution { |
复杂度分析
时间复杂度:。即 Manacher 算法的时间复杂度,由于最大回文右端点 只会增加而不会减少,故中心拓展进行的次数最多为,此外我们只会遍历字符串一次,故总复杂度为。
空间复杂度:。