扩展KMP(Z函数)详解
对于一个长度为n的字符串S。z[i] 表示S与其后缀S[i,n]的**最长公共前缀(LCP)**的长度。国外一般将计算该数组的算法称为,而国内则称其为扩展 KMP。下面介绍O(N)时间复杂度内求解Zfunc的方法。Manacher(马拉车)算法详解,原理分析-CSDN博客。
一、Z Func
1.1 定义
对于一个长度为n的字符串S。z[i] 表示S与其后缀S[i,n]的**最长公共前缀(LCP)**的长度。
国外一般将计算该数组的算法称为 Z Algorithm,而国内则称其为 扩展 KMP。
下面介绍O(N)时间复杂度内求解Zfunc的方法。
其计算方法和Manacher算法极为相似,详见:Manacher(马拉车)算法详解,原理分析-CSDN博客
1.2 暴力算法
std::vector<int> zFunction(const std::string s) {
int n = s.size();
std::vector<int> z(n);
for (int i = 0 ; i < n; ++ i)
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++ z[i];
return z;
}
1.3 线性算法
1.3.1 Z-box
类似 Manacher 算法 的加速盒子,Zfunc 的 线性算法也有一个盒子,为当前最靠右的匹配后缀,我们称之为 Z-box。
通常用指针[l, r]代表Z-box的左右边界,个人喜欢维护Z-box的起始指针 j,右边界可以通过j + z[j] 获得。
1.3.2 算法流程
-
当前 Z-box 左端点为 j,需要计算z[i] 的值
-
如果 i 在 [j, j + z[i]) 内,那么我们可以用 i 在 [j, j + z[i]) 的相对位置,计算出前缀的对应位置 k = i - j
那么我们可以利用前缀与Z-box的匹配计算出盒内的z 值
z[i] = std::min(j + z[j] - i, z[i - j]);,二者取min是因为我们只能计算盒内部分
-
对于盒外部分,暴力扩展:
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++ z[i];
-
计算完毕后,如果i + z[i] > j + z[j],则说明要更新Z-box
1.3.3 复杂度分析
对于内层 while 循环,如果成功扩展,一定会在后面更新j(因为如果不扩展,说明i + z[i] 根本没有到达盒子右边界)
而 右边界最多移动到 n ,所以while的整体时间复杂度是O(N)
因而我们算法整体时间复杂度为O(N)
1.3.4 代码实现
std::vector<int> zFunction(const std::string s) {
int n = s.size();
std::vector<int> z(n + 1);
z[0] = n;
for (int i = 1, j = 1; i < n; ++ i) {
z[i] = std::max(0, std::min(j + z[j] - i, z[i - j]));
while (i + z[i] < n && s[z[i]] == s[i + z[i]])
++ z[i];
if (i + z[i] > j + z[j])
j = i;
}
return z;
}
二、OJ练习
2.1 模板
原题链接
P5410 【模板】扩展 KMP/exKMP(Z 函数) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
zFunc计算部分就不说了,我们如何线性求p[]?
类似zFunc 的计算,我们维护一个p-Box,代表 字符串 a 的最右匹配后缀
那么同样的,盒内转移,盒外暴力扩展即可
时间复杂度:O(N)
注意开long long
AC代码
#include <bits/stdc++.h>
#include <ranges>
// #define DEBUG
using i64 = long long;
std::vector<int> zFunction(const std::string& s) {
int n = s.size();
std::vector<int> z(n + 1);
z[0] = n;
for (int i = 1, j = 1; i < n; ++ i) {
z[i] = std::max(0, std::min(j + z[j] - i, z[i - j]));
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++ z[i];
if (i + z[i] > j + z[j]) j = i;
}
return z;
}
void solve()
{
std::string a, b;
std::cin >> a >> b;
std::vector<int> z = zFunction(b);
i64 s = 0;
int n = a.size(), m = b.size();
for (int i = 0; i < m; ++ i) s ^= (z[i] + 1LL) * (i + 1);
std::cout << s << '\n';
std::vector<int> p(n + 1);
s = 0;
for (int i = 0, j = 0; i < n; ++ i) {
p[i] = std::max(0, std::min(j + p[j] - i, z[i - j]));
while (i + p[i] < n && p[i] < m && a[i + p[i]] == b[p[i]]) ++ p[i];
if (i + p[i] > j + p[j]) j = i;
}
for (int i = 0; i < n; ++ i) s ^= (p[i] + 1LL) * (i + 1);
std::cout << s;
}
int main()
{
#ifdef DEBUG
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
std::ios::sync_with_stdio(false), std::cin.tie(nullptr), std::cout.tie(nullptr);
int _ = 1;
// std::cin >> _;
while (_--)
solve();
return 0;
}
2.2 Password
原题链接
思路分析
很简单,如果答案存在那么它是一个后缀,那么z[i] = n - i
然后只需判断中间是否出现过即可
AC代码
#include <bits/stdc++.h>
#include <ranges>
#define sc scanf
// #define DEBUG
using u32 = unsigned;
using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128;
using PII = std::pair<int, int>;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1e18 + 7;
constexpr int P = 1E9 + 7;
constexpr double eps = 1E-6;
std::vector<int> zFunction(const std::string& s) {
int n = s.size();
std::vector<int> z(n + 1);
z[0] = n;
for (int i = 1, j = 1; i < n; ++ i) {
z[i] = std::max(0, std::min(j + z[j] - i, z[i - j]));
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++ z[i];
if (i + z[i] > j + z[j]) j = i;
}
return z;
}
void solve()
{
std::string s;
std::cin >> s;
auto z = zFunction(s);
int n = s.size();
std::vector<int> cnt(n + 2);
int ma = 0, res = 0;
for (int i = 1; i < n; ++ i) {
if (z[i] == n - i && (cnt[z[i]] || z[i] < ma))
res = std::max(res, z[i]);
++ cnt[z[i]];
ma = std::max(ma, z[i]);
}
if (res) std::cout << s.substr(0, res);
else std::cout << "Just a legend";
}
int main()
{
#ifdef DEBUG
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
std::ios::sync_with_stdio(false), std::cin.tie(nullptr), std::cout.tie(nullptr);
int _ = 1;
// std::cin >> _;
while (_--)
solve();
return 0;
}
2.3 F - Concat (2nd)
[补档]2026.01.16
原题链接
思路分析
不难想到字典序最小就是把S[] 按照 S[i] + S[j] < S[j] + S[i] 排序(证明使用微扰法即可)
然后A[2] 肯定是由A[1] 交换了一对下标得到,一般地,我们考虑 S[i] 如果和S[j]交换**(注意这里取i < j)**,那么j 取 i + 1 得到的肯定字典序最小
如果存在 S[i] + S[i + 1] == S[i + 1] + S[i],那么直接输出A[1]即可
否则,A[2] 应由交换 某个(S[i], S[i+1]) 得到,但是检查[0, N - 2] 妥妥超时,我们发现,由于此时 任意S[i] + S[i + 1] != S[i + 1] + S[i],所以我们如果定义 T[i] 为A[1] 交换 S[i], S[i+1] 所得,那么 任意 i < N - 3,有 T[i] < T[N - 2] (考虑字符串前缀字典序即可)
但是T[N - 2] 和 T[N - 3] 的大小关系是无法确定的,比如 “a”, “aab”, “b”,T[N - 3] < T[N - 2]
而 “a", “ad”, “c” 有T[N-2] < T[N-3]
所以排完序比较 T[N - 2] 和 T[N - 3] 即可
但是,如果排序的时候直接利用 s + t < t + s,即创建两个字符串然后比较的话,单次比较是 O(|s| + |t|)的,这样我们可以构造出TLE的样例,如:
N = 1E5,S[0] = string(5E5, ‘a’),然后任意N - 1个互不相同的字符串,由于排序基于快速排序(当然,如python那样的Tim排序也会被下面的情形卡掉),那么在第一次快速划分的时候,如果 S[0] 被选为pivot,那么要进行N次比较,每次O(5E5),这次快速划分就达到了 5E10量级。
但是我们如果将 s + t < t + s 的比较降低到O(min(|s|, |t|)),那么最坏情况下 max(|s[i]|) = sqrt(1E6)时,我们的复杂度也不过是 Σ|si| logN
如何加速?—— ZFunc
对于s,t(|s| > |t|, |s| = n, |t| = m,s 的Z函数为zs[],zt[] 同理),我们先比较前n 个元素,如果还不够,我们看 zs[m]和n-m的关系,如果zs[m] < n - m,我们比较 s[m + zs[m]] 和 s[zs[m]] 即可
否则,我们只需比较 t 和 s[n-m, n)
单次比较时间复杂度为 O(min(|s|, |t|))
另解:利用字符串哈希+二分求lcp来加速s + t 和 t + s 的比较,具体代码见代码二
AC代码(C#)
代码一(ZFunction)
public void Solve() {
int N = br.ReadInt32();
string[] s = new string[N];
int[][] z = new int[N][];
for (int i = 0; i < N; ++i) {
s[i] = br.ReadString();
z[i] = ZFunction(s[i]);
}
var p = Enumerable.Range(0, N).ToArray();
Array.Sort(p, (i, j) => {
int sgn = 1;
if (s[i].Length < s[j].Length) {
(i, j) = (j, i);
sgn = -1;
}
int n = s[i].Length, m = s[j].Length;
for (int k = 0; k < m; ++k) {
if (s[i][k] != s[j][k]) return sgn * s[i][k].CompareTo(s[j][k]);
}
if (n > m) {
if (z[i][m] != n - m) {
return sgn * s[i][z[i][m] + m].CompareTo(s[i][z[i][m]]);
}
}
for (int k = 0; k < m; ++k) {
if (s[j][k] != s[i][n - m + k]) return sgn * s[j][k].CompareTo(s[i][n - m + k]);
}
return 0;
});
string ans = string.Join("", p.Select(i => s[i]));
if (N == 2) {
bw.AppendLine(s[p[1]] + s[p[0]]);
return;
}
for (int i = 0; i + 1 < N; ++i) {
if (s[p[i + 1]] + s[p[i]] == s[p[i]] + s[p[i + 1]]) {
bw.AppendLine(ans);
return;
}
}
StringBuilder a = new(), b = new();
for (int i = 0; i < N - 3; ++i) {
a.Append(s[p[i]]);
b.Append(s[p[i]]);
}
a.Append(s[p[^2]]);a.Append(s[p[^3]]);a.Append(s[p[^1]]);
b.Append(s[p[^3]]);b.Append(s[p[^1]]);b.Append(s[p[^2]]);
string sa = a.ToString(), sb = b.ToString();
bw.AppendLine(sa.CompareTo(sb) < 0 ? sa : sb);
int[] ZFunction(string s) {
int n = s.Length;
int[] z = new int[n + 1];
z[0] = n;
for (int i = 1, j = 1; i < n; ++i) {
z[i] = Math.Max(0, Math.Min(z[i - j], j + z[j] - i));
while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
if (i + z[i] > j + z[j]) j = i;
}
return z;
}
}
代码二(字符串哈希)
public class SolutionA {
public void Solve() {
int N = br.ReadInt32();
string[] s = new string[N];
StringHash[] h = new StringHash[N];
for (int i = 0; i < N; ++i) {
s[i] = br.ReadString();
h[i] = new(s[i]);
}
var p = Enumerable.Range(0, N).ToArray();
Array.Sort(p, (i, j) => {
int sgn = 1;
if (s[i].Length < s[j].Length) {
(i, j) = (j, i);
sgn = -1;
}
int n = s[i].Length, m = s[j].Length;
int lcp = Lcp(i, 0, j, 0, m);
if (lcp < m) {
return sgn * s[i][lcp].CompareTo(s[j][lcp]);
}
if (n > m) {
lcp = Lcp(i, m, i, 0, n - m);
if (lcp < n - m) {
return sgn * s[i][m + lcp].CompareTo(s[i][lcp]);
}
lcp = Lcp(j, 0, i, n - m, m);
if (lcp < m) {
return sgn * s[j][lcp].CompareTo(s[i][n - m + lcp]);
}
}
return 0;
});
int Lcp(int i, int sti, int j, int stj, int n) {
int lo = 0, hi = n;
while (lo < hi) {
int x = (lo + hi + 1) / 2;
if (h[i][sti, sti + x] == h[j][stj, stj + x]) {
lo = x;
} else {
hi = x - 1;
}
}
return lo;
}
string ans = string.Join("", p.Select(i => s[i]));
if (N == 2) {
bw.AppendLine(s[p[1]] + s[p[0]]);
return;
}
for (int i = 0; i + 1 < N; ++i) {
if (s[p[i + 1]] + s[p[i]] == s[p[i]] + s[p[i + 1]]) {
bw.AppendLine(ans);
return;
}
}
StringBuilder a = new(), b = new();
for (int i = 0; i < N - 3; ++i) {
a.Append(s[p[i]]);
b.Append(s[p[i]]);
}
a.Append(s[p[^2]]); a.Append(s[p[^3]]); a.Append(s[p[^1]]);
b.Append(s[p[^3]]); b.Append(s[p[^1]]); b.Append(s[p[^2]]);
string sa = a.ToString(), sb = b.ToString();
bw.AppendLine(sa.CompareTo(sb) < 0 ? sa : sb);
}
更多推荐
所有评论(0)