Z函数(扩展kmp) & Manacher
Z函数
定义
(后文中字符串下标默认从0开始)
Z函数也可以认为是Z数组,即定义数组 \(z[i]\) 表示字符串 \(s\) 与其中以 \(s[i]\) 为开头的最长公共前缀
形式化的说,即 \(z[i]\) 表示为最大的 \(z[i]\) 满足 \(s[0 ...z[i] - 1] = s[i...i + z[i] - 1]\)
求解
这里直接讲解 \(o(n)\) 解法
我们注意到 \(z\) 数组是能像 \(kmp\) 中的 \(next\) 数组一致可重复贡献的
注意到 \(s[0...z[i] - 1]\) 与 \(s[i...i + z[i] - 1]\) 是等价的,所以在此范围内,\(z\) 数组也是等价的
那么我们就可以考虑利用左边贡献右边的方式
假设此时 \(z[0 ...i - 1]\) 已经全部求解完成,我们得到这其中影响范围最右的 \(z[j]\) ,并定义 \(r = j + z[j] - 1,l = j\)
根据我们上面的发现,区间 \([l, r]\) 与区间 \([0,l - 1]\) 是等价的
那么对于我们当前要求解的 \(i\) ,如果有 \(i < r\) ,那么我们可以得到 \(z[i] = z[i - l]\)
但注意,上面的等价性质是仅在此范围内成立的,所以如果 \(z[i - l] + i > r\) 则我们无法保证其还是等价的
所以我们在计算 \(z[i]\) 时应当注意上界,即 \(z[i] = min(z[i - 1],r - i + 1)\)
若 \(i >= r\) ,则我么无法快速求出 \(z[i]\) ,这时暴力求出 \(z[i]\) 即可
示例代码
void Z(string s) {
int n = s.length();
int l = 0, r = 0;
for (int i = 1; i < n; ++i) {
if (i <= r) {
z[i] = min(r - i + 1, z[i - l]);
}
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) {
z[i]++;
}
if (i + z[i] - 1 > r) {
l = i;
r = i + z[i] - 1;
}
}
return z;
}
时间复杂度
我们可以分析 \(r\) 来证明时间复杂度
首先 \(r\) 是单调的,且 \(r\) 的上界为 \(n\) ,并且由于 \(i\) 单调递增且固定 $ +1$ ,而 \(r\) 又一定大于等于 \(i\) ,所以 \(r\) 会在 \(O(n)\) 的时间内到达上界
到达上界之后就不会再进行暴力扩展的操作,所以总时间复杂度也是 \(O(n)\)
Manacher(马拉车)
定义
\(Manacher\) 算法,又称为马拉车算法,是一种求解回文字串问题的算法,它可以在 \(O(n)\) 的时间内求出每个位置的最长回文半径
算法过程
在这里,我们只考虑计算奇数长度的回文串,因为偶数长度的回文串我们可以通过添加间隔符来变成奇数长度
定义 \(p[i]\) 表示在字符串 \(s\) 当中,以 \(i\) 为中心点的最长回文半径
我们注意到回文串性质是左串翻转后等于右串,并且又因为我们考虑的是回文串,所以我们并不在意子串顺序
所以我们就有了结论,即左右字串的 \(p[i]\) 在此范围内是等价的
所以我们就可以用左子串贡献右子串的方式
设 \(r = max(j +p[j] - 1), c = j\)
对于当前要求解的 \(i\) ,若 \(r > i\) ,则 \(p[i] = min(2 * c - i,r - i + 1)\) 这里取 \(min\) 是为了防止跳出范围
否则暴力求解 \(p[i]\) ,之后不断更新 \(r, c\) 即可
到这里,阅读完上面Z函数之后可能就已经发现,\(Manacher\) 算法本质与Z函数相同
仅仅是在贡献内容上有区别,所以代码也基本一致,故时间复杂度证明也不再赘述
示例代码
void Manacher(string s) {
int n = s.length();
int c = 0, r = 0;
for (int i = 1; i < n; ++i) {
if (i <= r) {
z[i] = min(p[2 * c - i], r - i + 1);
}
while (s[i + p[i]] == s[i - p[i]]) {
p[i]++;
}
if (i + p[i] - 1 > r) {
c = i;
r = i + p[i] - 1;
}
}
return z;
}

浙公网安备 33010602011771号