文章目录
算法基础
问题引入
如果要用哈希统计从 s 中每一位字符开始最多可以匹配多少位 p 中的字符,需要用到二分查找,此时时间复杂度为 O ( n l o g m + m ) O(n log m + m) O(nlogm+m) ,其中 n 表示 s 的长度,m 表示 p 的长度。
扩展KMP(也称为Z algorithm)
-
能够以线性的时间复杂度求出一个字符串 s 和它的任意后缀
s[i] ... s[n]
的最长公共前缀的长度。 -
注意其与KMP算法求出的 next 数组的区别,一个是以字符
s[i]
结束,另一个是从字符s[i]
开始。 -
KMP
以
s[i]
结束的后缀与s的前缀匹配的最大长度 -
Z algorithm
以
s[i]
开始的后缀与s的前缀匹配的最大长度
举几个例子:
因为i = 1
时, 以i
结尾的后缀就是s串本身, 所以不再考虑
-
s1 = “aaaaaa”
z函数: z = [-, 5, 4, 3, 2, 1]
对应的后缀: “aaaaa”, “aaaa”, “aaa”, “aa”, “a”
-
s2 = “aabaacd”
z函数: z = [-, 1, 0, 2, 1, 0, 0]
对应的后缀: “a”, “”, “aa”, “a”, “”, “”
-
s3 = “abababab”
z函数: z = [-, 0, 6, 0, 4, 0, 2, 0]
对应的后缀: “”, “ababab”, “”, “abab”, “”, “ab”, “”
-
s4 = “ababaaba”
z函数: z = [-, 0, 3, 0, 1, 3, 0, 1]
对应的后缀: “”, “aba”, “”, “a”, “aba”, “”, “a”
Z函数计算
对于满足i > 1
的位置 i
,z[i]
表示字符串 s 和后缀s[i] ... s[n]
的最长公共前缀的长度。
定义z[1] = 0
,然后从2
到n
枚举i
,依次计算z[i]
的值。
-
假设我们要计算第
i
个位置的值z[i]
,此时z[1]....z[i -1]
都已经计算好了。 -
对于
j
,有s[j] ...s[j + z[j] - 1]
和s[1] ... s[z[j]]
完全相等。 -
为了计算
z[i]
,在枚举i
的过程中,我们要维护R最大的区间[L,R]
,其中L = j (j < i), R = j + z[j] - 1
。即区间[L, R]
是目前R最大的s的前缀。 -
初始时区间为空: L= 1,R = 0。
-
如果 i ≤ R i \le R i≤R,则情况如下:
根据定义,有
s[L] ... s[R] = s[1] ...s[R - L + 1]
令k = i - L + 1
,i
在[L,R]
中的位置对应了 k 在[1, R-L+1]
中的位置,此时s[i]...s[R]
和s[k].. s[R-L+1]
是相等的。此时
z[k]
已知, 即有:s[1]~s[z[k]] = s[k]~s[k + z[k]]
, 又因为s[k]~s[z[k]]
对应了s[i]~s[i+z[k]]
, 所以以i
为后缀的子串基础长度取决于
z[k]
和[k, R-L+1]
的长度可以理解为
z[k]
为可以提供给i
的关于与前缀匹配的信息, 位置i
至少能匹配的前缀长度取决于z[k]
本身和[k, R-L+1]
的长度- 如果
z
[
k
]
<
R
−
i
+
1
z[k] < R - i + 1
z[k]<R−i+1,说明从k开始匹配不到
R - L + 1
那么远,也就是说从i
开始匹配不到R那么远,, 此时z[i] = z[k]
。 - 如果
z
[
k
]
≥
R
−
i
+
1
z[k] \ge R - i + 1
z[k]≥R−i+1,说明从 i 开始至少可以匹配到那么远, 即至少有
z[i] = z[k]
,因为s[R + 1] != s[R - L + 2]
, 所以不能根据z[k]
本身推出z[i]
, 要考虑到边界影响, 但此时z[i]
的初始值可确定为[k, R-L+1]
的长度, 这时我们从 R+1 开始继续暴力向后匹配即可。
- 如果
z
[
k
]
<
R
−
i
+
1
z[k] < R - i + 1
z[k]<R−i+1,说明从k开始匹配不到
-
如果
i > R
:暴力枚举匹配即可。
求出
z[i]
后用i
和z[i]
的值更新 L 和 R, 所以我们可以知道要求R最大是为了包含更多的未确定z函数值的位置。 -
时间复杂度
暴力向后匹配的过程中R的值也在同步增加,如果R加了 O ( n ) O(n) O(n)次到达了字符串终点, 则此后任意位置
i
的z[i]
可立刻得知, 不需要暴力与前缀匹配, 所以R最多只会被加 O ( n ) O(n) O(n)次,因此算法的时间复杂度为 O ( n ) O(n) O(n)。
代码模板
char s[N];
int n, z[N];
void exkmp() {
int l = 1, r = 0; // 空区间
z[1] = 0; // 定义z[1] = 0, 后面直接while就行
// 递推求z[i]
for(int i = 2; i <= n; i++) {
if(i > r) z[i] = 0; // z[i] = 0, 等会while循环暴力
else { // 利用区间[l, r]更新z[i]
int k = i - l + 1; // i 在 l~r 的对应位置
// 只能确定 l ~ r == 1 ~ r-l+1
// 则 i ~ r == k ~ r-l+1
// 如果 z[k] < r - i + 1, 则 z[i] = z[k]
// 如果 z[k] >= r - i + 1, 则 z[i] = r - i + 1
z[i] = min(z[k], r - i + 1);
}
// 暴力匹配, 两种情况都会在这暴力匹配
while(i + z[i] <= n && s[z[i] + 1] == s[z[i] + i]) z[i]++;
// 更新R最大区间
if(i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}
}
习题
字符串匹配例题
给你两个字符串 a, b,字符串均由小写字母组成,现在问你 b 在 a 中出现了几次。
输入有多组数据,第一行为数据组数 𝑇,每组数据包含两行输入,第一行为字符串 a,第二行为字符串 b。
对于每组输入需要输出两行,其中第一行为出现次数,第二行为每次出现时第一个字符在 a 中的下标(字符串首位的下标为 1)。如果找不到,输出两行 −1。
利用Z数组的含义: 以i
为起点的后缀能匹配的最长前缀的长度
将模式串p放在前面, 源串s放在后面, 并用’‘#’'分割, 则在源串s位置i
的z函数值z[i]
等于模式串长度m, 表明模式串p在源串s中出现过, 且以位置i
为起点
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+5;
const int INF = 0x3f3f3f3f;
int z[2 * N];
string a, b;
void exkmp() {
string s = "@" + b + "#" + a;
int n = a.size(), m = b.size();
int L = 0, R = 1;
z[1] = 0;
for (int i = 2; i <= n + m + 1; i++) {
// 获取初始z函数值
if (i > R) z[i] = 0;
else z[i] = min(z[i - L + 1], R - i + 1);
// 从i+z[i]-1位置开始暴力匹配
// i + z[i]是下一个要匹配位置
// 当前位置合法, 并且要匹配的下一对位置字符相等
while (i + z[i] <= n + m + 1
&& s[i + z[i]] == s[1 + z[i]]) {
z[i]++;
}
// 更新区间[L, R]
if (i + z[i] - 1 > R) {
R = i + z[i] - 1, L = i;
}
}
std::vector<int> ans;
// 1 ~ m m+1 m+2 ~ n+m+1
// b # a
for (int i = m + 2; i <= n + m + 1; i++) {
if (z[i] == m) {
ans.push_back(i - (m + 2) + 1);
}
}
if (ans.empty()) cout << -1 << endl << -1;
else {
cout << ans.size() << endl;
for (auto v : ans) cout << v << " ";
}
cout << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int T; cin >> T;
while (T--) {
cin >> a >> b;
exkmp();
}
return 0;
}
Password
给你一个字符串 s,由小写字母组成,你需要求出其中最长的一个子串 p,满足 p 既是 s 的前缀,又是 s 的后缀,且在 s 中以非前缀后缀的形式出现过。如果找不到输出
Just a legend
。输入一行一个字符串 s,输出一行一个字符串 p。
可以用KMP和扩展KMP解决
判断子串既是前缀又是后缀
- 对于位置
i
,z[i]
表示以i
开始的子串能与前缀匹配的最大长度, 如果该长度可以到达整个字符串的终点, 则说明该子串是一个既是前缀又是后缀
判断是否以非前缀后缀形式出现过
-
如果
s[i] ~ s[n]
既是前缀也是后缀, 则有s[i] ~ s[n] = s[1] ~ s[z[i]]
如果此时位置
i
前面有位置j (j < i)
使得z[j] > z[i]
, 将两个位置起始的子串对于到其前缀有j
起始子串匹配的前缀包含了i
起始的子串匹配的前缀, 即s[1] ~ s[z[i]]
包含在s[1] ~ s[z[j]]
中因为
s[1] ~ s[z[j]] = s[j]~s[j + z[j] - 1]
,所以有s[1] ~ s[z[i]] = s[j] ~ s[j + z[i] - 1]
即该前后缀
s[i]~s[n] = s[1]~s[z[i]] = s[j] ~ s[j + z[i]]
存在于中间位置
综上, 只需要遍历每个位置, 判断该子串是否是前后缀, 再判断该位置前的最大z函数是多少, 如果大于该位置的z函数, 则存在非前后缀的中间串与前后缀相等, 最后取最长的z函数值即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6+5;
const int INF = 0x3f3f3f3f;
int z[2 * N];
string s;
void exkmp() {
s = "@" + s;
int n = s.size() - 1;
int L = 0, R = 1;
z[1] = 0;
for (int i = 2; i <= n; i++) {
// 获取初始z函数值
if (i > R) z[i] = 0;
else z[i] = min(z[i - L + 1], R - i + 1);
while (i + z[i] <= n
&& s[i + z[i]] == s[1 + z[i]]) {
z[i]++;
}
// 更新区间[L, R]
if (i + z[i] - 1 > R) {
R = i + z[i] - 1, L = i;
}
}
int preMaxZ = 0, maxZ = 0;
for (int i = 1; i <= n; i++) {
// 要求此等于前缀后缀右端点为字符串右端点
if (z[i] == n - i + 1) {
// 如果前面存在大于z[i]的, 说明中间存在一个等于前缀的后缀
if (preMaxZ >= z[i]) {
maxZ = max(maxZ, z[i]);
}
}
preMaxZ = max(preMaxZ, z[i]);
}
if (!maxZ) cout << "Just a legend" << endl;
else cout << s.substr(1, maxZ) << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
cin >> s;
exkmp();
return 0;
}
Prefixes and Suffixes
您的任务是,对于与字符串s的后缀匹配的字符串s的任何前缀,打印它的长度和作为子字符串在字符串s中出现的次数。
与上一题类似, 我们判断出后缀等于前缀的位置后, 如果前面位置存在大于该位置的z函数值的位置, 则存在于该后缀相等的子串, 本体中要求求这样的后缀子串个数, 则问题变为求某个位置前面有多少个位置的z函数值大于等于该位置的z函数值
可以用树状数组维护到位置i
之前的位置的z函数值域的树状数组, 但是z函数值可以取0, 所以0不能直接用树状数组存
树状数组可以查询位置i
之前的前缀权值和, 即查询有多少位置的z函数值小于等于z[i]
, 因为位置i
前面有i-1
个位置, 前面的权值要么比z[i]
大, 要么比z[i]
小, 要么等于0, 所以位置i
前面有(i - 1) - query(z[i] - 1) - zero
个位置的z函数值大于等于z[i],
最后, 位置i
开始的后缀也不要忘记
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+5;
const int INF = 0x3f3f3f3f;
ll c[N];
int n, z[2 * N];
string s;
ll query(int x) {
ll s = 0;
for (; x; x -= x & (-x)) s += c[x];
return s;
}
void add(int x, ll s) {
for (; x <= N; x += x & (-x)) {
c[x] += s;
}
}
void exkmp() {
s = "$" + s;
n = s.size() - 1;
int L = 0, R = 1;
z[1] = 0;
for (int i = 2; i <= n; i++) {
if (i > R) z[i] = 0;
else z[i] = min(z[i - L + 1], R - i + 1);
while (i + z[i] <= n
&& s[i + z[i]] == s[1 + z[i]]) {
z[i]++;
}
if (i + z[i] - 1 > R) {
R = i + z[i] - 1, L = i;
}
}
z[1] = n;
std::vector<pair<int, int>> ans;
int zero = 0;
for (int i = 1; i <= n; i++) {
if (z[i] == n - i + 1) {
int cnt = (i - 1) - zero - (query(z[i] - 1)) + 1;
ans.push_back({z[i], cnt});
}
if (!z[i]) zero++;
else add(z[i], 1);
}
sort(ans.begin(), ans.end());
cout << ans.size() << endl;
for (auto v : ans) cout << v.first << " " << v.second << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
cin >> s;
exkmp();
return 0;
}
Periodic Strings
如果字符串可以通过连接另一个长度为k的字符串的一次或多次重复来形成,则该字符串具有周期k。例如,字符串“abcabcabc”具有周期3,因为它是由字符串“abc”的4次重复组成的。它也有周期6(“abcabc”重复两次)和12(“abcabcabc”重复一次)。
扩展kmp求最小循环节
当z[i] = n - i + 1
时, 表示从i
开始的后缀等于等长的前缀: s[i] ~ s[n] = s[1] ~ s[z[i]]
所以s[1]~s[i-1] = s[i]~s[i+i-2]
, 以此类推后缀s[i]~s[n]
可被前缀s[1]~s[i-1]
, 如果后缀s[i]~s[n]
的长度z[i]
能被该前缀的长度i-1
整除, 则i-1
就是该字符串的最小循环节, 否则出现只使用部分s[1]~s[i-1]
覆盖调字符串, 则s[1]~s[i-1]
不是最小循环节
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100+5;
const int INF = 0x3f3f3f3f;
int z[2 * N];
string a;
void exkmp() {
string s = "@" + a;
int n = s.size() - 1;
int L = 0, R = 1;
z[1] = 0;
for (int i = 2; i <= n; i++) {
if (i > R) z[i] = 0;
else z[i] = min(z[i - L + 1], R - i + 1);
while (i + z[i] <= n && s[i + z[i]] == s[1 + z[i]]) {
z[i]++;
}
if (i + z[i] - 1 > R) {
R = i + z[i] - 1, L = i;
}
}
// 1 ~ m m+1 m+2 ~ n+m+1
// b # a
for (int i = 2; i <= n; i++) {
if (z[i] % (i - 1) == 0
&& z[i] == n - i + 1) {
cout << (i - 1) << endl;
return;
}
}
cout << n << endl;
}
//
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int T; cin >> T;
while (T--) {
cin >> a;
exkmp();
if (T) cout << endl;
}
return 0;
}