「笔记」Z 函数(扩展 KMP)
写在前面
这 b 东西感觉和 KMP 没啥关系啊我草,比起 KMP 感觉和 Manacher 关系更大一点呃呃
虽然这仨玩意本质上都是在利用已匹配的部分来省略不必要的匹配过程来进行加速。
又发现好久没写知识笔记了、、、好像再学就只能学省选牛逼科技了,或许需要啃一下各类计数科技之类的呃呃我是没脑子选手
至少串串基本都学完了,还剩下什么 BM 算法啊后缀平衡树这种科技就算了吧。
简介
对于某长度为 的字符串 ,其 函数是一个长度为 的数组,位置 对应的 Z 函数 定义为 与后缀 的最长公共前缀的长度。如对于字符串 :
7 | 0 | 1 | 0 | 3 | 0 | 1 |
枚举后缀大力匹配的朴素算法显然是 级别的。但是通过之前重复利用状态中已匹配的信息来加速新的状态,字符串的 Z 函数可以在 的时空复杂度内求得。
另外,求得字符串 的 Z 函数后,可以通过类似的算法同样在 的时空复杂度内求得另一字符串 的所有后缀与 的最长公共前缀的长度。
算法流程
对于字符串 的每个位置 ,称区间 是 的匹配段(Z-box),也即后缀 与 的最长公共前缀对应的区间。
由定义有 。考虑顺序枚举位置 并依次计算它们的 Z 函数,在计算 时会利用到已计算好的 。在算法过程中记当前已求出 对应的匹配段中右端点最靠右的匹配段为 ,初始化 。在枚举到 时:
- 若 ,根据匹配段的定义,则子串 是前缀 的一个后缀,有 。则 。
- 此时若 ,则 。
- 否则 ,则令 再暴力扩展 直至不能扩展。
- 若 ,则直接暴力扩展求 。
- 求得 后若其匹配段更靠右则更新 。
代码
实现上把 与 这两种需要暴力扩展的情况合并到一块写了。
复制复制void z_function() {
z[1] = n;
for (int i = 2, l = 0, r = 0; i <= m; ++ i) {
if (i <= r && z[i - l + 1] < r - i + 1) {
z[i] = z[i - l + 1];
} else {
z[i] = std::max(0, r - i + 1);
while (i + z[i] <= m && s[z[i] + 1] == s[i + z[i]]) ++ z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
}
复杂度证明
外层 for
至多执行 次,内层 while
每执行一次 至少向后移一位,有 则至多执行 次。
综上,总时间复杂度为 级别。
求串 所有后缀与串 的最长公共前缀。
设串 长度为 ,后缀 与 的最长公共前缀为 ,有两种算法:
第一种比较好理解,在两个串直接加个分隔符连接构造串 并求得该串的 Z 函数,该串后半段 部分的 Z 函数即为所求。听着简单但是还需要把求出来的 Z 函数再折腾到另一个数组里才好用,一般不这么写。
第二种则是先对 求 Z 函数,然后利用 Z 函数在串 上运行类似的算法。
对于字符串 的每个位置 ,称区间 是 的匹配段(Z-box),也即后缀 与 的最长公共前缀对应的区间。考虑顺序枚举位置 并依次计算 ,同样记当前已求出 对应的匹配段中右端点最靠右的匹配段为 ,初始化 。在枚举到 时:
- 若 ,根据匹配段的定义,则子串 是前缀 的一个后缀,有 。则 。
- 此时若 ,则 。
- 否则 ,则令 再暴力扩展 直至不能扩展。
- 若 ,则直接暴力扩展求 。
- 求得 后若其匹配段更靠右则更新 。
由上述分析可知复杂度为 级别。
void extend() {
for (int i = 1, l = 0, r = 0; i <= m; ++ i) {
if (i <= r && z[i - l + 1] < r - i + 1) {
p[i] = z[i - l + 1];
} else {
p[i] = std::max(0, r - i + 1);
while (i + p[i] <= m && t[i + p[i]] == s[p[i] + 1]) ++ p[i];
}
if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
}
}
下位替代
枚举每个后缀哈希+二分求得与原串的最长公共前缀即可。
复杂度 ,常数不小。
与 KMP 的关系
为什么叫扩展 KMP?感觉也没扩展啊呃呃
- 都是利用了过重复利用已匹配的信息,将朴素的算法优化成了线性。
- 与 KMP 的应用范围有一些重合:均可以匹配子串、求字符串周期……
例题
感觉纯 Z 函数的题不多见的样子毕竟有只多一个 的下位替代。
好像大多数串串题都有多种解决思路的样子、、、
匹配子串
在长度为 的字符串 上匹配长度为 的字符串 ,首先求得 的 Z 函数,然后按照上述算法求得 的每个后缀与 的最长公共前缀 ,若 说明子串 匹配 。
求字符串周期
给定一长度为 的字符串 ,找到其最短的整周期,即寻找一个最短的字符串 ,使得 可以被若干个 拼接而成的字符串表示。
计算 的 函数后,其周期的长度为最小的 ,满足 。正确性显然,上式成立说明字符串 向右平移 后可以与后缀 重合。
若求最小整周期则钦定 为 的因数即可。
KMP 求字符串最小周期为 大概也是这个思想呃呃。
P5410 【模板】扩展 KMP/exKMP(Z 函数)
给定两个字符串 ,你要求出两个数组:
- 的 函数数组 ,即 与 的每一个后缀的 LCP 长度。
- 与 的每一个后缀的 LCP 长度数组 。
对于一个长度为 的数组 ,设其权值为 。
,所有字符均为小写字母。
1S,500MB。
板题。
//知识点:Z 函数
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e7 + 10;
//=============================================================
int n, m, z[kN], p[kN];
char a[kN], b[kN];
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void z_function() {
z[1] = m;
for (int i = 2, l = 0, r = 0; i <= m; ++ i) {
if (i <= r && z[i - l + 1] < r - i + 1) {
z[i] = z[i - l + 1];
} else {
z[i] = std::max(0, r - i + 1);
while (i + z[i] <= m && b[z[i] + 1] == b[i + z[i]]) ++ z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
}
void extend() {
for (int i = 1, l = 0, r = 0; i <= n; ++ i) {
if (i <= r && z[i - l + 1] < r - i + 1) {
p[i] = z[i - l + 1];
} else {
p[i] = std::max(0, r - i + 1);
while (i + p[i] <= n && a[i + p[i]] == b[p[i] + 1]) ++ p[i];
}
if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
}
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
scanf("%s", a + 1); n = strlen(a + 1);
scanf("%s", b + 1); m = strlen(b + 1);
z_function();
extend();
LL ans = 0;
for (int i = 1; i <= m; ++ i) ans ^= 1ll * i * (z[i] + 1);
printf("%lld\n", ans);
ans = 0;
for (int i = 1; i <= n; ++ i) ans ^= 1ll * i * (p[i] + 1);
printf("%lld\n", ans);
return 0;
}
P7114 [NOIP2020] 字符串匹配
组数据,每组数据给定仅由小写字母组成的字符串 ,求 的方案数,其中 ,其中 表示字符串 中出现奇数次的字符的数量。两种方案不同当且仅当拆分出的 、、 中有至少一个字符串不同。
对于所有测试点,保证 ,。
1S,512MB。
知识点:枚举,结论,Z 函数,哈希
首先预处理所有前缀和后缀中出现次数为奇数次的字符的数量,然后考虑枚举 的长度。但是接下来不暴力枚举 而是进一步深挖性质。
首先考虑以 为循环节的最长前缀可以到什么位置,即求合法的 的 的最大值。手玩了下发现可以通过求 与 的最长公共前缀,也即 Z 函数求得,满足:
注意 都要求非空,所以上式中有一个取 防止出现 非空的情况。
然后考虑为什么要强调 代表出现次数为奇数的字符数量?手玩下发现 中所有字符出现次数均为偶数,也即当 的变化量为偶数时 是不变的,仅需讨论 为奇数与偶数的情况即可确定 ,再求有多少 合法即可,不需要再枚举 了。
为奇数的个数为 ,此时有 ; 为偶数的个数为 ,此时有 。同样可通过树状数组维护 求合法 的数量。
总时间复杂度为 级别。但是已经有了不依赖于字符集大小纯线性的做法?牛逼,但是懒了。
//知识点:枚举,结论,Z 函数,哈希
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 5e5 + 10;
const int kM = 30;
//=============================================================
int n, z[kN];
int cnt_pre[30], cnt_suf[30], sum_pre[kN], sum_suf[kN];
char s[kN];
LL ans;
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
namespace Bit {
#define lowbit(x) ((x)&-(x))
int time, t[kM], tim[kM];
void Init() {
++ time;
}
void Insert(int pos_) {
++ pos_;
for (int i = pos_; i <= 27; i += lowbit(i)) {
if (tim[i] != time) t[i] = 0, tim[i] = time;
t[i] ++;
}
}
int Sum(int pos_) {
++ pos_;
int ret = 0;
for (int i = pos_; i; i -= lowbit(i)) {
if (tim[i] != time) t[i] = 0, tim[i] = time;
ret += t[i];
}
return ret;
}
#undef lowbit
}
void z_function() {
z[1] = n;
for (int i = 2, l = 1, r = 1; i <= n; ++ i) {
if (i <= r && z[i - l + 1] < r - i + 1) {
z[i] = z[i - l + 1];
} else {
z[i] = std::max(0, r - i + 1);
while (i + z[i] <= n && s[z[i] + 1] == s[i + z[i]]) ++ z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
}
void Init() {
ans = 0;
scanf("%s", s + 1); n = strlen(s + 1);
for (int i = 0; i <= 27; ++ i) cnt_pre[i] = cnt_suf[i] = 0;
for (int i = 1; i <= n + 1; ++ i) sum_pre[i] = sum_suf[i] = 0;
for (int i = 1; i <= n; ++ i) {
++ cnt_pre[s[i] - 'a'];
if (cnt_pre[s[i] - 'a'] % 2 == 1) sum_pre[i] = sum_pre[i - 1] + 1;
else sum_pre[i] = sum_pre[i - 1] - 1;
}
for (int i = n; i >= 1; -- i) {
++ cnt_suf[s[i] - 'a'];
if (cnt_suf[s[i] - 'a'] % 2 == 1) sum_suf[i] = sum_suf[i + 1] + 1;
else sum_suf[i] = sum_suf[i + 1] - 1;
}
z_function();
Bit::Init();
}
void Solve() {
for (int ab = 2; ab <= n; ++ ab) {
int cnt = std::min(z[ab + 1] / ab + 1, (n - 1) / ab);
Bit::Insert(sum_pre[ab - 1]);
ans += 1ll * (cnt + 1) / 2ll * Bit::Sum(sum_suf[ab + 1]);
ans += 1ll * cnt / 2ll * Bit::Sum(sum_suf[1]);
}
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
int T = read();
while (T --) {
Init();
Solve();
printf("%lld\n", ans);
}
return 0;
}
CF1968G2
组数据,每组数据给定一仅包含小写字母的长度为 的字符串 ,参数 。
定义 表示将字符串 划分为互不相交的、覆盖整个字符串的 段连续子串 后,最长公共前缀 的最大值。
求:。
,,。
3S,256MB
Z 函数,调和级数,贪心
我去 div3 居然有 Z 函数的题呃呃,不过倒是没卡哈希+二分所以暂且认为 div3 有这题不算超纲。
由 的性质可知答案显然有单调性,即一定有 。
考虑对于某个答案 对于询问 是否合法,发现问题实际上即选出一些位置 ,钦定 ,,且 。
发现 即 Z 函数的定义,于是先求 Z 函数然后考虑按 Z 函数倒序枚举所有位置(即倒序枚举答案 的长度),并检查选择上述所有位置,在满足 的限制下至多能选多少。设可以选择 个,则一定有 。
发现选位置一定是贪心地尽可能选最小的,且又发现枚举到 时至多仅会选择 个位置,是个调和级数的形式,于是考虑用 set 维护所有可选的位置并直接大力贪心即可。
对求得的 取后缀最大值即为答案。
总时间复杂度 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int n, l, r, z[kN], ans[kN];
std::string s;
std::vector<int> pos[kN];
std::set<int> have;
//=============================================================
void z_function() {
z[1] = n;
for (int i = 2, l = 0, r = 0; i <= n; ++ i) {
if (i <= r && z[i - l + 1] < r - i + 1) {
z[i] = z[i - l + 1];
} else {
z[i] = std::max(0, r - i + 1);
while (i + z[i] <= n && s[z[i] + 1] == s[i + z[i]]) ++ z[i];
}
if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
}
void check(int len_) {
int cnt = 0;
for (int i = 1; i <= n; ) {
auto p = have.lower_bound(i);
if (p == have.end()) break;
if (*p + len_ - 1 > n) break;
i = *p + len_;
++ cnt;
}
int p = std::min(r, cnt);
ans[p] = std::max(ans[p], len_);
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
int T; std::cin >> T;
while (T --) {
std::cin >> n >> l >> r;
std::cin >> s; s = "$" + s;
z_function();
for (int i = 1; i <= n; ++ i) pos[i].clear();
for (int i = 1; i <= n; ++ i) pos[z[i]].push_back(i);
have.clear();
for (int i = l; i <= r; ++ i) ans[i] = 0;
for (int i = n; i; -- i) {
for (auto p: pos[i]) have.insert(p);
check(i);
}
for (int i = r - 1; i >= l; -- i) ans[i] = std::max(ans[i + 1], ans[i]);
for (int i = l; i <= r; ++ i) std::cout << ans[i] << " ";
std::cout << "\n";
}
return 0;
}
P9576 「TAOI-2」Ciallo~(∠・ω< )⌒★
昨天晚上看到室友在玩一个叫千恋万花的游戏,中间忘了,现在我才认清我室友自私自利的嘴脸,根本不值得我给他分享好东西。
给定两个仅由小写字母构成的字符串 ,求有多少组 ,满足下列条件:
- 。
- 设 删去子串 后变为 ,有:。
- 。
,。
2S,512MB。
知识点:Z 函数,哈希,枚举,数数,序列数据结构
先解决一种简单的情况。若最终得到的 即为 中原来就是与 匹配的连续的子串 ,则仅需确定删去的 ,考虑 只可能同时出现在 的一侧且可以相等,显然方案数为 种。
然后考虑 最终得到的 在 中不连续的情况,即 可以被拆成 的一段前缀 和一段后缀 ,两者在 中保持前后关系且不相邻,考虑枚举其中的一方并求可以拼成 的另一方的数量。
记 表示后缀 与 的最长真公共前缀长度, 表示前缀 与 的最长真公共后缀长度。 和 可以通过对 正串和反串分别求 Z 函数预处理得到。为什么强调是真前缀和真后缀呢?因为要保证 和 都不能是空的,这样限制方便下一步求贡献。
则以 为开头可以组成长度为 的 。对于以 开头长度为 的 ,设可以与之拼成 的 以 为结尾,则 需要满足:
对于一个满足 的 ,考虑它可以对某个 产生多少贡献。为了保证 和 都不能是空的,则有贡献的以 为结尾的 的长度限制为:
由上式可知,对于所有以以 开头的 ,所有满足上述条件的 数量之和,也即所有 对 的贡献之和即为:
上面括号里的两部分可以以相似的方式分别求出来。发现满足条件的 是一个二维偏序的形式,这东西显然可以通过序列数据结构维护,即考虑在倒序枚举 的同时维护两个初始为空的树状数组,枚举到 时将位置 加上 ,查询区间 中的权值和即得括号内的值。
总时间复杂度 级别。
所以这东西本质上就是个枚举嘛呃呃
//知识点:Z 函数,哈希,枚举,数数,序列数据结构
/*
By:Luckyblock
Ciallo~(∠・ω< )⌒★
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 4e5 + 10;
//=============================================================
int n, m, z1[kN], z2[kN], p1[kN], p2[kN];
char s[kN], t[kN];
LL ans;
//=============================================================
inline int read() {
int f = 1, w = 0; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
struct Bit {
#define lowbit(x) ((x)&-(x))
int lim;
LL t[kN];
void Init(int n_) {
lim = n_;
}
void Insert(int pos_, LL val_) {
for (int i = pos_; i <= lim; i += lowbit(i)) {
t[i] += val_;
}
}
LL Sum(int pos_) {
LL ret = 0;
for (int i = pos_; i; i -= lowbit(i)) {
ret += t[i];
}
return ret;
}
LL Query(int l_, int r_) {
return Sum(r_) - Sum(l_ - 1);
}
#undef lowbit
} bit1, bit2;
void z_function() {
z1[1] = m;
for (int i = 2, l = 0, r = 0; i <= m; ++ i) {
if (i <= r && z1[i - l + 1] < r - i + 1) {
z1[i] = z1[i - l + 1];
} else {
z1[i] = std::max(0, r - i + 1);
while (i + z1[i] <= m && t[z1[i] + 1] == t[i + z1[i]]) ++ z1[i];
}
if (i + z1[i] - 1 > r) l = i, r = i + z1[i] - 1;
}
for (int i = 1, l = 0, r = 0; i <= n; ++ i) {
if (i <= r && z1[i - l + 1] < r - i + 1) {
p1[i] = z1[i - l + 1];
} else {
p1[i] = std::max(0, r - i + 1);
while (i + p1[i] <= n && s[i + p1[i]] == t[p1[i] + 1]) ++ p1[i];
}
if (i + p1[i] - 1 > r) l = i, r = i + p1[i] - 1;
}
std::reverse(t + 1, t + m + 1);
std::reverse(s + 1, s + n + 1);
z2[1] = m;
for (int i = 2, l = 0, r = 0; i <= m; ++ i) {
if (i <= r && z2[i - l + 1] < r - i + 1) {
z2[i] = z2[i - l + 1];
} else {
z2[i] = std::max(0, r - i + 1);
while (i + z2[i] <= m && t[z2[i] + 1] == t[i + z2[i]]) ++ z2[i];
}
if (i + z2[i] - 1 > r) l = i, r = i + z2[i] - 1;
}
for (int i = 1, l = 0, r = 0; i <= n; ++ i) {
if (i <= r && z2[i - l + 1] < r - i + 1) {
p2[i] = z2[i - l + 1];
} else {
p2[i] = std::max(0, r - i + 1);
while (i + p2[i] <= n && s[i + p2[i]] == t[p2[i] + 1]) ++ p2[i];
}
if (i + p2[i] - 1 > r) l = i, r = i + p2[i] - 1;
}
std::reverse(p2 + 1, p2 + n + 1);
}
void Solve1() {
for (int i = 1; i <= n; ++ i) {
if (p1[i] == m) {
int l1 = i - 1, l2 = n - (i + m) + 1;
ans += 1ll * l1 * (l1 + 1) / 2ll;
ans += 1ll * l2 * (l2 + 1) / 2ll;
}
}
}
void Solve2() {
for (int i = 1; i <= n; ++ i) {
if (p1[i] == m) -- p1[i];
if (p2[i] == m) -- p2[i];
}
bit1.Init(m), bit2.Init(m);
LL sum = 0;
for (int i = n - m; i; -- i) {
if (p2[i + m]) bit1.Insert(p2[i + m], 1);
if (p2[i + m]) bit2.Insert(p2[i + m], p2[i + m]);
LL ret1 = bit1.Query(m - p1[i], m - 1);
LL ret2 = bit2.Query(m - p1[i], m - 1);
sum = ret2 - ret1 * (m - p1[i] - 1);
ans += sum;
}
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
scanf("%s", s + 1); n = strlen(s + 1);
scanf("%s", t + 1); m = strlen(t + 1);
z_function();
Solve1();
Solve2();
printf("%lld\n", ans);
return 0;
}
写在最后
参考:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!