一、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;
}