KMP-前缀函数
这里字符串下标默认从开始。
这里字符串下标默认从 1 开始。
前缀函数
一些概念
前缀:一个字符串通过从结尾删除字符得到的子串(可以不删)。
后缀:一个字符串通过从开头删除字符得到的子串(可以不删)。
真前缀:一个字符串通过从结尾删除字符得到的子串(必须删)。
真后缀:一个字符串通过从开头删除字符得到的子串(必须删)。
π \pi π
定义对于字符串 s s s 的 π \pi π 数组, π i \pi_i πi 表示前缀 [ 1 , i ] [1,i] [1,i] 的最长真前缀(且有一个后缀与之相等)长度,这个真前缀称为 border。
求法
暴力
先看一个 O ( N 3 ) O(N^3) O(N3) 的暴力:
for(int i=2;i<=n;i++){
for(int j=i-1;j>=1;j--){
if(s.substr(1,j)==s.substr(i-j+1,j)){
p[i]=j;
break;
}
}
}
没有任何优化,纯暴力。
优化 1
注意到在最好的情况下, π i \pi_i πi 的值每次最多增加 1,所以当前最大的情况也就是 π i − 1 + 1 \pi_{i-1}+1 πi−1+1。
for(int i=2;i<=n;i++){
for(int j=p[i-1]+1;j>=1;j--){
if(s.substr(1,j)==s.substr(i-j+1,j)){
p[i]=j;
break;
}
}
}
这样看似没有改变什么,但实际的时间复杂度是 O ( N 2 ) O(N^2) O(N2) 的了。
因为每次增加 1, π i \pi_i πi 的最大值也只会到达 n − 1 n-1 n−1,而下限是 0,所以外面两层枚举的总时间复杂度是 O ( N ) O(N) O(N) 的。
优化 2
注意到 π i \pi_i πi 的定义,有 [ 1 , π i ] = [ i − π i + 1 , i ] [1,\pi_i]=[i-\pi_i+1,i] [1,πi]=[i−πi+1,i]。
上图中,蓝色部分就是 [ 1 , π i ] [1,\pi_i] [1,πi] 和 [ i − π i + 1 , i ] [i-\pi_i+1,i] [i−πi+1,i],右边的黄色部分就是新加入的 i + 1 i+1 i+1。
若要使新的 π i + 1 = π i + 1 \pi_{i+1}=\pi_i+1 πi+1=πi+1,则需要两个黄色部分的字符相同。
但是如果不相同怎么办?
我们将目光转移到 π π i \pi_{\pi_i} ππi 上面。由于 [ 1 , π i ] = [ i − π i + 1 , i ] [1,\pi_i]=[i-\pi_i+1,i] [1,πi]=[i−πi+1,i],还有 [ 1 , π π i ] = [ π i − π π i + 1 , π i ] [1,\pi_{\pi_i}]=[\pi_i-\pi_{\pi_i}+1,\pi_i] [1,ππi]=[πi−ππi+1,πi],所以一定有 [ 1 , π π i ] = [ i − π π i + 1 , i ] [1,\pi_{\pi_i}]=[i-\pi_{\pi_i}+1,i] [1,ππi]=[i−ππi+1,i],这时候还是可以将 [ 1 , π π i ] [1,\pi_{\pi_i}] [1,ππi] 和 [ i − π π i + 1 , i ] [i-\pi_{\pi_i}+1,i] [i−ππi+1,i] 两个区间视为上图中的蓝色部分,此时若黄色部分的字符相同,就有 π i + 1 = π π i + 1 \pi_{i+1}=\pi_{\pi_i}+1 πi+1=ππi+1。
那如果还是不同怎么办?
就继续看 π π π i \pi_{\pi_{\pi_i}} πππi,直到没有为止。
时间复杂度 O ( N ) O(N) O(N),可以像【优化 1】中的那样分析。
for(int i=2;i<=n;i++){
int j=p[i-1];
while(j&&s[j+1]!=s[i])j=p[j];
if(s[j+1]==s[i])j++;
p[i]=j;
}
那为什么这样一定是对的?
首先正确性是毋庸置疑的。
看到最优性。根据 π \pi π 的定义,其一定是最大的,所以 π π i \pi_{\pi_i} ππi 一定是 π i \pi_i πi 的所以满足条件的前缀中最大的一个, π π π i \pi_{\pi_{\pi_i}} πππi 以此类推。
既然是从最大的开始向更小的找,那么肯定没有遗漏。
字符串匹配
所以 KMP 到底是啥?
其实没有前缀函数的 O ( N ) O(N) O(N) 求法,KMP 啥都不是。
查询模式串 s 2 s_2 s2 在文本串 s 1 s_1 s1 出现的位置。
方案一
既然前缀函数是求一个前缀的 border 长度,那么我们只需要让模式串称为那个固定的前缀,去匹配文本串中的子串。
那么怎么找文本串的子串呢?前缀的后缀就是一个子串。
所以我们将两个字符串拼接起来:
s = s 2 + " # " + s 1 s=s_2+"\#"+s_1 s=s2+"#"+s1
中间的 # \# # 是为了防止匹配的长度超过模式串。
这时候我们遍历过程中,就会找到文本串 s 1 s_1 s1 的每个前缀,并且通过 π i \pi_i πi 数组得到其与模式串的前缀相同的后缀。
当 π i \pi_i πi 的值等于模式串 s 1 s_1 s1 的长度时,就说明匹配成功了。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;//字符串要拼接,要开两倍空间
int m;
int p[N];
signed main(){
string s1,s2;
cin>>s1;
cin>>s2;
m=s2.length();
string s=" "+s2+"#"+s1;
for(int i=2;i<s.size();i++){
int j=p[i-1];
while(j&&s[j+1]!=s[i])j=p[j];
if(s[j+1]==s[i])j++;
p[i]=j;
}
for(int i=m+2;i<s.size();i++){
if(p[i]==m){
cout<<i-2*m<<'\n';
}
}
for(int i=1;i<=m;i++)cout<<p[i]<<' ';
}
方案二
直接比较。
先得求出模式串 s 2 s_2 s2 对应的 π \pi π 数组。
用两个指针 i i i 和 j j j 分别在文本串 s 1 s_1 s1 和模式串 s 2 s_2 s2 中标记匹配位置。
若 s 1 i = = s 2 j + 1 {s_1}_i=={s_2}_{j+1} s1i==s2j+1,那么两个指针各自加一即可。
若不相等,则需要将指针 j j j 回移。

与求前缀函数的【优化 2】类似,蓝色部分即为模式串 s 2 s_2 s2 中 [ 1 , j ] [1,j] [1,j] 的 border。
当匹配不上时,为了防止时间爆炸,要用上之前已经匹配过的地方。所以将 j j j 变为 π j \pi_j πj。由于两端蓝色部分相同,所以不会影响前面的匹配。同理,若还是不匹配,则转到 π π j \pi_{\pi_j} ππj。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5;
int n,m;
int p[N];
signed main(){
string s1,s2;
cin>>s1;
cin>>s2;
n=s1.length(),m=s2.length();
s1=" "+s1;
s2=" "+s2;
for(int i=2;i<=m;i++){
int j=p[i-1];
while(j&&s2[j+1]!=s2[i])j=p[j];
if(s2[j+1]==s2[i])j++;
p[i]=j;
}
int j=0;
for(int i=1;i<=n;i++){
while(j&&s2[j+1]!=s1[i])j=p[j];
if(s2[j+1]==s1[i])j++;
if(j==m){
cout<<i-m+1<<'\n';
j=p[j];
}
}
for(int i=1;i<=m;i++)cout<<p[i]<<' ';
}
其他应用
字符串的周期
字符串 s s s 的周期 t t t 通过重复若干次可以得到一个字符串 S S S,使得 s s s 是 S S S 的前缀。
第一种情况:border 未重叠
此时的周期很明显就是长度为 n − π n n-\pi_n n−πn 的 [ 1 , n − π n ] [1,n-\pi_n] [1,n−πn] 和 [ π n + 1 , n ] [\pi_n+1,n] [πn+1,n] 了。
第二种情况:border 重叠
上图中两个黑框框就是两个 border。根据 border 的定义,有两个橙色部分,两个蓝色部分相同。
但是这俩玩意本质上是一个字符串里面的,所以又有两条竖着的线连起来的部分相同。
根据 border 的定义,又有斜着的线连接的部分相同。
……
以此类推。
所以,最终橙色部分/蓝色部分就是字符串的一个周期,即 [ 1 , n − π n ] [1,n-\pi_n] [1,n−πn] 和 [ π n + 1 , n ] [\pi_n+1,n] [πn+1,n]
并且这是最短的周期,因为 π n \pi_n πn 还可以找到 π π n \pi_{\pi_n} ππn,会获得一个更长的周期。
以此类推。
字符串的压缩/循环节
循环节就是重复若干遍刚好为原字符串的子串。
那这就是个有条件的周期。
那么就是周期长度刚好是 n n n 的某个因数。
所以若 n ≡ 0 ( m o d n − π n ) ) n\equiv0(\mod n-\pi_n)) n≡0(modn−πn)), [ 1 , n − π n ] [1,n-\pi_n] [1,n−πn] 就是循环节(或 [ π n + 1 , n ] [\pi_n+1,n] [πn+1,n])。
若 n ≢ 0 ( m o d n − π n ) ) n\not\equiv0(\mod n-\pi_n)) n≡0(modn−πn)),则循环节长度就是 n n n。
为什么不继续尝试 n − π π n n-\pi_{\pi_n} n−ππn?因为 n − π n n-\pi_n n−πn 是最小周期的长度,其他的周期都比他长。
更多推荐




所有评论(0)