约定下标从 0 开始。
Z 函数
定义 z[i] 表示字符串 s 和 s[i..](即从 i 开始的部分)的最长公共前缀。
我们可以在 O(n) 复杂度内计算出一个字符串的 Z 函数。(当然也可以哈希单 log 完成)
假设我们已经求出了 z[0],z[1],…,z[i−1],对于每个 i,将 [i,i+z[i]−1] 称为它的 Z-box。
我们找出 1∼i−1 中右端点最靠右的 Z-box,记作 [l,r]。(初始时 l=r=0,不计 z[0] 的 Z-box)
分类讨论:
- i≤r:根据 Z 函数定义可知 s[0..r−l]=s[l,r],则 s[i..r]=s[i−l,r−l],那么 z[i]≥min(r−i+1,z[i−l])。令 z[i]=min(r−i+1,z[i−l]),然后再暴力往后一位一位直至失配。
- i>r:直接往后一位一位匹配直至失配。
复杂度说明:对于情况 1,只有当 z[i−l]≥r−i+1 时才会扩展,此时每往后匹配一位 r 就会后移一位。对于情况 2,因为 i>r,所以每往后匹配一位 r 就会至少后移一位。
因此在 O(n) 时间内求出了整个字符串的 Z 函数。
可以发现这个过程和 Manacher 算法 很相似。
代码:
1 2 3 4 5 6 7 8 9 10
| vector<int> z_function(string s) { int n = s.length(); vector<int> z(n); for(int i=1,l=0,r=0;i<n;i++) { if(i<=r)z[i]=min(r-i+1, z[i-l]); while(i+z[i]<n && s[z[i]]==s[i+z[i]])++z[i]; if(i+z[i]-1>r)l=i,r=i+z[i]-1; } return z; }
|
(注意上面的代码没有算 z[0],因为是平凡的,用处不太大就不管它了)
为了节省一点常数可以稍微修改一下(和上面代码是等价的,至于写哪个就见仁见智了):
1 2 3 4 5 6 7 8 9 10 11 12 13
| vector<int> z_function(string s) { int n = s.length(); vector<int> z(n); for(int i=1,l=0,r=0;i<n;i++) { if(i<=r && z[i-l]<r-i+1)z[i]=z[i-l]; else{ z[i] = max(0,r-i+1); while(i+z[i]<n && s[z[i]]==s[i+z[i]])++z[i]; } if(i+z[i]-1>r)l=i,r=i+z[i]-1; } return z; }
|
模板题
P5410 【模板】扩展 KMP/exKMP(Z 函数)
构建字符串 s=b+∘+a。然后计算 s 的 Z 函数。(其中 ∘ 是任意不在 a,b 中出现的字符)
b 自己的后缀的 LCP 长度就是 s 前面一部分的 Z 函数的值。
而 b 与 a 的每一个后缀的 LCP 长度是 s 中后面部分 Z 函数的值,因为我们插入了一个在字符串中没有出现的 ∘ 字符,根据定义就能得到结果。
这启发我们 Z 函数可以 O(n+m) 处理字符串匹配,而且可以求出从某一个位置开始最多可以匹配几个字符(这是 KMP 无法做到的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include<bits/stdc++.h> using namespace std;
using ll=long long;
vector<int> z_function(string s) { int n = s.length(); vector<int> z(n); for(int i=1,l=0,r=0;i<n;i++) { if(i<=r && z[i-l]<r-i+1)z[i]=z[i-l]; else{ z[i] = max(0,r-i+1); while(i+z[i]<n && s[z[i]]==s[i+z[i]])++z[i]; } if(i+z[i]-1>r)l=i,r=i+z[i]-1; } return z; }
int main(){ ios::sync_with_stdio(0);cin.tie(0);
string a,b; cin>>a>>b; string s = b+"#"+a; auto z = z_function(s); z[0] = b.size();
ll ans1 = 0; for(ll i=0;i<b.size();i++)ans1 ^= (i+1)*(z[i]+1);
ll ans2 = 0; for(ll i=0;i<a.size();i++)ans2 ^= (i+1)*(z[i+b.size()+1]+1);
cout << ans1 << '\n' << ans2 << '\n'; return 0; }
|
QOJ9901. 匹配
题意
QOJ9901. 匹配
给定一个字符串 S 和一个与 S 等长的 01 串 P。
构造一个非空字符串 T 满足以下条件:
- 对于 1≤i≤∣S∣−∣T∣+1,P[i]=1 当且仅当 T 作为子串恰好从 S 的下标 i 开始出现;
- 对于 ∣S∣−∣T∣+1<i≤∣S∣,P[i]=0。
若不存在输出 -1
。
∣S∣≤106。
题解
如果 P 里面没有 1,可以比较简单的构造,这里随便给出一种方法:构造一个长度和 S 相同但不等于 S 的字符串。
如果 P 里面有 1,记 P 里面最后一个 1 的位置为 r,那么我们可以知道 T 是 S[r..n] 的一段前缀,只需要求出 T 的长度 len 就能解决本题。
将 S[r..n] 作为模板串对 S 进行匹配,求出 S[r..n] 和 S 每一个后缀的最长公共前缀长度记作 lcp[n]。(用 Z 函数可以线性完成)
则若 P[i]=0,那么不能是子串,所以要求 len>lcp[i]。
若 P[i]=1,必须是子串,要求 len≤lcp[i]。
对所有的 i 遍历一遍,如果 len 有合法取值,那么 S[r..(r+len−1)] 就是一个合法答案,否则不存在解。
复杂度 O(∣S∣)。
参考代码:
https://qoj.ac/submission/876990