算法学习笔记——字符串匹配算法
KMP
线性时间匹配字符串的算法。
引入:前缀函数
先明确「前缀」和「后缀」的定义:
- 前缀:从字符串开头开始的子串(如
abcde的前缀有a、ab、abc、abcd); - 后缀:以字符串结尾结束的子串(如
abcde的后缀有e、de、cde、bcde); - 「相等前缀和后缀」:指内容完全相同的前缀和后缀(如
abab的前缀ab和后缀ab相等)。
则 π[i] 表示:
- 子串 s[0…i](即从第 0 个字符到第 i 个字符的子串)中,最长的、既等于该子串前缀,又等于该子串后缀的非平凡子串的长度。
注意最长这一条件。
计算前缀函数
因为有“最长”这一条件限制,因此采用 DP 即可实现其最优结构。
1 | s = "..." |

与 next 的关系
next 不是前缀函数本身,而是基于前缀函数构造的“跳转表”,用于在匹配失败时决定模式串该“回退到哪里”。
1 | pat = ""; // 模式 |
Manacher
寻找最长回文子串的算法。
寻找回文子串的一般做法
枚举一个中心点,并向两侧扩展,复杂度为$O(n^2)$。
不仅复杂度高,而且需要奇偶分类讨论。因此需要一个更高效的算法。
Manacher 算法
有一个字符串s="abcba",为了使其有一个对称中心,我们可以考虑插入特殊字符使得其具有一个对称中心,也就是说 s.length() 从 $n$ 到 $2n+1$ 。
t = "^#a#b#c#b#a#$" 注意 ^$ 是防止越界的。
考虑有一个数组 P[i] 表示 s[i] 的半径(包括自身,例如ab的半径为1,aba的半径为2)。利用动态规划的思想,假设一切 P[i] 都是已知的,接下来寻找递推关系。显然有:
1 | if(s[i+P[i]] == s[i-P[i]]){ |
同时有恒等式 s[i+P[i]-1] == s[i-P[i]+1]。
但是,如果仅仅这样,复杂度仍是 $O(n^2)$ ,其关键在于 P[i] 从 1 开始遍历。为了降低时间复杂度,我们需要通过一些观察使得 每次计算P前,都为P初始化一个值。
重要观察 : 在中点 C 的半径内, P[i] 的最小值的分布是对称的,也就是说 P[C-k] == P[C+k] 当 k <= P[i] 且P均代表着可能的最小值。这由字符串对称性是显然的。因此,我们就可以根据这个最小值缩写枚举的范围。
需要注意的是,因为半径以外的内容是“不可见”的,因此先初始化的部分 (也就是对称点前的)有可能更大(因为半径外的字符串可能有影响),这时就要求:对P的初始化不能使得p[i] > R-i也就是要使得该点为中心,半径不会超过右边界。
可以进行一步贪心:记录当前最大右边界 R 及最长回文中点 C 并维护之,有:
1 | if(i + P[i] > R){ |
在半径内初始化P[i]:
1 | if(i < R){ |
例题: LeetCode 647 回文子串:给你一个字符串
s,请你统计并返回这个字符串中 回文子串 的数目。
1 | class Solution { |