注意:对于字符串 S S S, S [ i , j ] S[i,j] S[i,j]表示从下标为 i i i的位置到下标为 j j j的位置所构成的子串。
Z函数
定义:对于字符串 s s s, z i z_i zi 定义为 ∣ LCP ( s , s [ i : ] ) ∣ |\operatorname{LCP}(s, s[i:])| ∣LCP(s,s[i:])∣,即从 i i i 开始的后缀与 s s s 的最长公共子串的长度
。即 s [ 0 , z [ i ] − 1 ] = s [ i , i + z [ i ] − 1 ] s[0,z[i]-1] = s[i, i + z[i]-1] s[0,z[i]−1]=s[i,i+z[i]−1]最长。
暴力构造法: O ( n 2 ) O(n^2) O(n2)
cin >> p;
int n = strlen (p);
for (int i = 0; i < n; i ++)
while (i + z[i] < n and p[z[i]] == p[z[i] + i]) z[i] ++;
for (int i = 0; i < n; i ++)
cout << z[i] << " ";
优化方法:
需要明白,优化方法一定是利用已有的信息来求未知的信息。
Z − b o x Z-box Z−box:相当于一段区间 [ l , r ] [l,r] [l,r],并且这段区间是字符串的前缀,其中要求 r r r要尽可能的大, Z − b o x Z-box Z−box会随着 i i i进行移动,因此,在位置 i i i时, [ l , r ] [l,r] [l,r]必须包含 i i i。
对于字符串 b a c b c b a c b a bacbcbacba bacbcbacba, Z Z Z函数的值如下
b | a | c | b | c | b | a | c | b | a | |
---|---|---|---|---|---|---|---|---|---|---|
i n d e x index index | 0 0 0 | 1 1 1 | 2 2 2 | 3 3 3 | 4 4 4 | 5 5 5 | 6 6 6 | 7 7 7 | 8 8 8 | 9 9 9 |
Z − f u n c t i o n Z-function Z−function | 10 10 10 | 0 0 0 | 0 0 0 | 1 1 1 | 0 0 0 | 4 4 4 | 0 0 0 | 0 0 0 | 2 2 2 | 0 0 0 |
z
[
5
]
=
4
z[5] = 4
z[5]=4, 下标为
5
5
5的
Z
−
b
o
x
Z-box
Z−box范围为
s
[
5
,
8
]
s[5,8]
s[5,8]
z
[
6
]
=
0
z[6] = 0
z[6]=0, 下标为
6
6
6的
Z
−
b
o
x
Z-box
Z−box范围为
s
[
5
,
8
]
s[5,8]
s[5,8]
z
[
7
]
=
0
z[7] = 0
z[7]=0, 下标为
7
7
7的
Z
−
b
o
x
Z-box
Z−box范围为
s
[
5
,
8
]
s[5,8]
s[5,8]
z
[
8
]
=
2
z[8] = 2
z[8]=2, 下标为
8
8
8的
Z
−
b
o
x
Z-box
Z−box范围为
s
[
8
,
9
]
s[8,9]
s[8,9]
如何用 Z − b o x Z-box Z−box来求 Z Z Z函数呢?(假定字符串用 S S S表示)
假定已经求得了 Z [ i − 1 ] Z[i-1] Z[i−1]的值,以及到 i − 1 i-1 i−1位置的 Z − b o x Z-box Z−box(即 [ l , r ] [l,r] [l,r])现在要求出 Z [ i ] Z[i] Z[i]的值。
我们可以比较 i i i与 r r r的大小,分成两种情况
1、 i > r i > r i>r
![](https://i-blog.csdnimg.cn/blog_migrate/cdb61837ac5f296f72fbfc1753c814c4.png)
此时,虽然我们知道 i − 1 i-1 i−1的 Z Z Z值和 [ l , r ] [l,r] [l,r]但是这些信息并不能推导出 Z [ i ] Z[i] Z[i]的值,因此我们暴力的进行匹配求 Z Z Z的值,由于 r r r的值增加了,同时要更新 Z − b o x Z-box Z−box。
2、 i < r i < r i<r
![](https://i-blog.csdnimg.cn/blog_migrate/c00d9f15da1551cc2c96d5888ec96c39.png)
我们知道 [ l , r ] [l,r] [l,r]和字符串的前缀是相等的, 将红色的区间移动到开头,匹配出对应位置如下图
![](https://i-blog.csdnimg.cn/blog_migrate/f283cd74ca328b5601ef6899ad25b9a7.png)
由于上面的两段红色区间是完全相等的(由 Z − b o x Z-box Z−box的定义),那么 s [ i − 1 + 1 , r − l + 1 ] = s [ i , r ] s[i-1+1, r-l+1] = s[i,r] s[i−1+1,r−l+1]=s[i,r],如下图绿色区域
![](https://i-blog.csdnimg.cn/blog_migrate/1f3be345ec9671097be334931edf285f.png)
在计算 Z [ i ] Z[i] Z[i]之前,所有的 Z Z Z都被计算过了,因此我们可以利用绿色的这段相等部分,来优化。
此时需要分为两种情况讨论
- Z [ i − l + 1 ] < r − l + 1 Z[i-l+1] < r-l+1 Z[i−l+1]<r−l+1
![](https://i-blog.csdnimg.cn/blog_migrate/411d1d1e2e0c04d15d3c4c8536e6b616.png)
此类情况,可以直接利用前面的匹配信息, Z [ i ] = Z [ i − l + 1 ] Z[i]=Z[i-l+1] Z[i]=Z[i−l+1] ,同时 Z − b o x Z-box Z−box不变。
- Z [ i − l + 1 ] > = r − l + 1 Z[i-l+1] >= r-l+1 Z[i−l+1]>=r−l+1
![](https://i-blog.csdnimg.cn/blog_migrate/87642e73be528740a82330d11496e5c1.png)
此时, s [ i , r ] s[i,r] s[i,r]的部分一定是与前缀相同的,只需要在 > r >r >r的部分进行暴力匹配即可。
同时由于 r r r的范围更大了,需要更新 Z − b o x Z-box Z−box为 [ i , i + z [ i ] − 1 ] [i,i+z[i]-1] [i,i+z[i]−1]。
思考:
Z − b o x Z-box Z−box中的 r r r取值为什么要尽可能靠右?
原因在于越靠右,上图中绿色区域的范围会越大,这样 z [ i − l + 1 ] < r − l + 1 z[i-l+1]<r-l+1 z[i−l+1]<r−l+1的情况会更多,所以会更好的节省时间。
总体时间复杂度为: O ( n ) O(n) O(n),因为 Z − b o x Z-box Z−box右端点最多右移 n n n次。
参考程序如下:
void get_z (char *s, int n) {
int l = 0, r = 0;
z[1] = n;
for (int i = 2; i <= n; i ++) {
if (i > r) {
while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
l = i, r = i + z[i] - 1;
}
else if (z[i - l + 1] < r - i + 1) z[i] = z[i - l + 1];
else {
z[i] = r - i + 1;
while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
l = i, r = i + z[i] - 1;
}
}
}
扩展KMP
扩展KMP算法主要解决以下字符串问题:
给定两个字符串 s t r 1 str1 str1以及 s t r 2 str2 str2,求 s t r 1 str1 str1的每一个后缀与 s t r 2 str2 str2的 L C P LCP LCP(最长公共前缀)。
类比:
相比于 Z Z Z函数,实际上如果 s t r 1 str1 str1 等于 s t r 2 str2 str2,扩展 K M P KMP KMP求解的就是 Z Z Z函数,因此可以使用类似于 Z Z Z函数的方法求解
假定 e x t e n d [ i ] extend[i] extend[i]表示 s t r 1 str1 str1第 i i i位开始的后缀与 s t r 2 str2 str2的最长公共前缀
同理:类似于 Z − b o x Z-box Z−box,我们维护一个区间 [ l , r ] [l,r] [l,r]保证
- [ l , r ] [l,r] [l,r]随着 s t r 1 str1 str1中的 i i i移动,在位置 i i i时,必须包含 i i i
- r r r尽可能的大
同理可以分好几种情况讨论
1、 z [ i − l + 1 ] < r − i + 1 z[i-l+1] < r-i+1 z[i−l+1]<r−i+1
![](https://i-blog.csdnimg.cn/blog_migrate/a4655f39a50275353c325c9348326a2e.png)
此时, e x t e n d [ i ] = z [ i − l + 1 ] extend[i] = z[i-l+1] extend[i]=z[i−l+1],区间 [ l , r ] [l,r] [l,r]保持不变。
2、 z [ i − l + 1 ] > = r − i + 1 z[i-l+1] >= r-i+1 z[i−l+1]>=r−i+1
![](https://i-blog.csdnimg.cn/blog_migrate/4189f6d158becdc50deab4ef391fb962.png)
我们只能保证绿色区域相同,超出的部分需要收到更新,即 e x t e n d [ i ] extend[i] extend[i]从 r r r开始暴力扫描求解,区间 [ l , r ] [l,r] [l,r]变成 [ i , i + e x t e n d [ i ] − 1 ] [i, i+extend[i]-1] [i,i+extend[i]−1]
3、 i > r i > r i>r时,无法使用已有信息进行匹配更新,直接从 i i i位置开始扫描求解,同时区间 [ l , r ] [l,r] [l,r]变成 [ i , i + e x t e n d [ i ] − 1 ] [i, i+extend[i]-1] [i,i+extend[i]−1]
参考程序:
void get_ext (char *s1, int n, char *s2, int m) { // s1的每一个后缀与s2的lcp
int l = 0, r = 0;
for (int i = 1; i <= n; i ++) {
if (i > r) {
while (i + ext[i] <= n and ext[i] + 1 <= m and s1[i + ext[i]] == s2[ext[i] + 1]) ext[i] ++;
l = i, r = i + ext[i] - 1;
}
else if (z[i - l + 1] < r - i + 1) ext[i] = z[i - l + 1];
else {
ext[i] = r - i + 1;
while (i + ext[i] <= n and ext[i] + 1 <= m and s1[i + ext[i]] == s2[ext[i] + 1]) ext[i] ++;
l = i, r = i + ext[i] - 1;
}
}
}
// 优化写法
inline void exkmp(char *s, int n, char *t, int m) {
Z(t, m);
for (int i = 1; i <= n; i++) p[i] = 0;
for (int i = 1, l = 0, r = 0; i <= n; i++) {
if (i <= r) p[i] = min(z[i-l+1], r - i + 1);
while (i + p[i] <= n && s[i+p[i]] == t[p[i]+1]) ++p[i];
if (i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
}
}
还有一种解决问题的思路:
求 s t r 1 str1 str1的每一个后缀与 s t r 2 str2 str2的 l c p lcp lcp,我们可以将 s t r 1 str1 str1拼接在 s t r 2 str2 str2后面,对新的字符串 s t r str str,求它的 Z Z Z函数即可!
那么对于原先 s t r 1 str1 str1的 e x t [ i ] ext[i] ext[i]等价于 z [ i + s i z e ( s t r 2 ) ] z[i+size(str2)] z[i+size(str2)]。
例题
题目描述
给定两个字符串 a , b a,b a,b,你要求出两个数组:
- b b b 的 z z z 函数数组 z z z,即 b b b 与 b b b 的每一个后缀的 L C P LCP LCP 长度。
- b b b 与 a a a 的每一个后缀的 L C P LCP LCP 长度数组 p p p。
对于一个长度为 n n n 的数组 a a a,设其权值为 xor i = 1 n i × ( a i + 1 ) \operatorname{xor}_{i=1}^n i \times (a_i + 1) xori=1ni×(ai+1)。
输入格式
两行两个字符串 a , b a,b a,b。
输出格式
第一行一个整数,表示 z z z 的权值。
第二行一个整数,表示 p p p 的权值。
输入样例1
aaaabaa
aaaaa
输出样例
6
21
说明/提示
样例解释:
z = { 5 4 3 2 1 } z = \{5\ 4\ 3\ 2\ 1\} z={5 4 3 2 1} , p = { 4 3 2 1 0 2 1 } p = \{4\ 3\ 2\ 1\ 0\ 2\ 1\} p={4 3 2 1 0 2 1}。
数据范围:
对于第一个测试点, ∣ a ∣ , ∣ b ∣ ≤ 2 × 1 0 3 |a|,|b| \le 2 \times 10^3 ∣a∣,∣b∣≤2×103。
对于第二个测试点, ∣ a ∣ , ∣ b ∣ ≤ 2 × 1 0 5 |a|,|b| \le 2 \times 10^5 ∣a∣,∣b∣≤2×105。
对于 100%100% 的数据, 1 ≤ ∣ a ∣ , ∣ b ∣ ≤ 2 × 1 0 7 1 \le |a|,|b| \le 2 \times 10^7 1≤∣a∣,∣b∣≤2×107,所有字符均为小写字母。
(此题在洛谷上测试,编号为 P 5410 P5410 P5410,开 O 2 O_2 O2可过。)
参考程序1
#include <iostream>
#include <cstring>
using namespace std;
const int N = 2e7 + 15;
typedef long long ll;
char a[N], b[N];
int n, m;
ll z[N], ext[N];
void get_z (char *s, int n) {
ll l = 0, r = 0;
z[1] = n;
for (int i = 2; i <= n; i ++) {
if (i > r) {
while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
l = i, r = i + z[i] - 1;
}
else if (z[i - l + 1] < r - i + 1) z[i] = z[i - l + 1];
else {
z[i] = r - i + 1;
while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
l = i, r = i + z[i] - 1;
}
}
}
void get_ext (char *s1, int n, char *s2, int m) { // s1的每一个后缀与s2的lcp
ll l = 0, r = 0;
for (int i = 1; i <= n; i ++) {
if (i > r) {
while (i + ext[i] <= n and ext[i] + 1 <= m and s1[i + ext[i]] == s2[ext[i] + 1]) ext[i] ++;
l = i, r = i + ext[i] - 1;
}
else if (z[i - l + 1] < r - i + 1) ext[i] = z[i - l + 1];
else {
ext[i] = r - i + 1;
while (i + ext[i] <= n and ext[i] + 1 <= m and s1[i + ext[i]] == s2[ext[i] + 1]) ext[i] ++;
l = i, r = i + ext[i] - 1;
}
}
}
int main () {
// freopen ("2.in", "r", stdin);
cin >> (a + 1) >> (b + 1);
n = strlen(a + 1), m = strlen (b + 1);
get_z (b, m);
ll t1 = 0;
for (int i = 1; i <= m; i ++)
t1 ^= ((z[i] + 1) * i);
cout << t1 << endl;
get_ext (a, n, b, m);
t1 = 0;
for (int i = 1; i <= n; i ++)
t1 ^= ((ext[i] + 1) * i);
cout << t1 << endl;
return 0;
}
两个串合并:
参考程序2
#include <iostream>
#include <cstring>
using namespace std;
const int N = 2e7 + 15;
typedef long long ll;
string a, b;
ll n, m;
ll z[N * 2];
void get_z (string s, int n) {
ll l = 0, r = 0;
z[1] = n;
for (int i = 2; i <= n; i ++) {
if (i > r) {
while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
l = i, r = i + z[i] - 1;
}
else if (z[i - l + 1] < r - i + 1) z[i] = z[i - l + 1];
else {
z[i] = r - i + 1;
while (s[i + z[i]] == s[z[i] + 1]) z[i] ++;
l = i, r = i + z[i] - 1;
}
}
}
int main () {
// freopen ("2.in", "r", stdin);
cin >> a >> b;
n = a.length(), m = b.length();
get_z (" " + b + a, m + n);
ll t1 = 0;
for (int i = 1; i <= m; i ++)
t1 ^= ((min(z[i], m - i + 1) + 1) * i);
cout << t1 << endl;
t1 = 0;
for (int i = 1; i <= n; i ++)
t1 ^= ((min(z[i + m], m) + 1) * i);
cout << t1 << endl;
return 0;
}