KPKPKMP
KMP算法
真のKMP算法
在线性时间复杂度内匹配字符串(判断串 B B B是否是串 A A A的子串,并找出串 B B B在串 A A A中出现的位置)。
暴力匹配方法:
把
B
B
B的第一位与
A
A
A的每一位进行比较,若匹配则继续向后比较,否则将
B
B
B后移一位。
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
当比较到
B
B
B的某个位置与
A
A
A不匹配时,这个位置前的部分已经被比较过了,若继续逐个比较就会导致效率极低。效率高的做法是,若
B
B
B的某个前缀在已匹配部分的其它位置出现过(如图中的"AB"),可以直接将
B
B
B移至这些位置;否则已匹配部分中的字符都不可能与
B
B
B的首字符匹配,直接跳过。
而KMP算法通过针对
B
B
B计算出一张回退表来实现这个过程。
搜索词 B B B | A | B | C | D | A | B | D |
---|---|---|---|---|---|---|---|
回退表 f a i l fail fail | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
串
B
B
B中每个位置的回退值就是当前已匹配部分的前缀与后缀集合中最长相同元素的长度。例如"ABCDABD"中第六位的回退值为
2
2
2,因为"ABCDAB"的前缀与后缀集合中最长相同元素为"AB",其长度为
2
2
2。
根据回退表,移动位数
=
=
=已匹配的字符数
−
-
−对应的回退值。
记两个串长度分别为 n , m n,m n,m。分别给串 A , B A,B A,B两个指针 i , j i,j i,j,始终满足 B B B的前 j j j个字符正好匹配 A A A的以 A [ i ] A[i] A[i]结尾的长度为 j j j的子串,即 B [ 1 … j ] B[1\dots j] B[1…j]与 A [ i − j + 1 … i ] A[i-j+1\dots i] A[i−j+1…i]匹配。
- 若 A [ i + 1 ] = B [ j + 1 ] A[i+1]=B[j+1] A[i+1]=B[j+1],则可以增加 i , j i,j i,j的值,继续往后比较。若某个时候 j = m j=m j=m,说明 B B B是 A A A的字串。
- 若 A [ i + 1 ] ≠ B [ j + 1 ] A[i+1]\neq B[j+1] A[i+1]̸=B[j+1],则将 j j j改为 f a i l [ j ] fail[j] fail[j],相当于 B B B向右移动了 j − f a i l [ j ] j-fail[j] j−fail[j]位。
如何求串
B
B
B的
f
a
i
l
fail
fail数组?显然
f
a
i
l
[
1
]
=
0
fail[1]=0
fail[1]=0。然后从
i
=
2
i=2
i=2开始,拿两个
B
B
B串进行KMP,一边匹配一边记下每个位置的回退值。因为
i
i
i始终大于
j
j
j,当
j
j
j需要改为
f
a
i
l
[
j
]
fail[j]
fail[j]时,
f
a
i
l
[
1
…
j
]
fail[1\dots j]
fail[1…j]都已经算好了。
时间复杂度:
O
(
n
)
O(n)
O(n)
void kmp(char *a,char *b,int *fail){
int n=strlen(s+1),m=strlen(b+1);
for(int i=2,j=0;i<=m;i++){
while(j&&b[j+1]!=b[i])j=fail[j];
if(b[j+1]==b[i])j++;fail[i]=j;
}
for(int i=1,j=0;i<=n;i++){
while(j&&b[j+1]!=a[i])j=fail[j];
if(b[j+1]==a[i])j++;
if(j==m){cout<<i-m+1<<endl;j=fail[j];}
}
}
求最短循环节
f
a
i
l
fail
fail数组有一个奇妙的性质:对于长度为
l
e
n
len
len的字符串,其最短循环节长度为
l
e
n
−
f
a
i
l
[
l
e
n
]
len-fail[len]
len−fail[len]。
但这样算出的循环节可能在原串中是不完整的。例如"ABCAB"算出的最短循环节为"ABC",长度为
3
3
3。所以有些题目需要特判。
求最长公共子串
利用KMP可以在 O ( n L 2 ) O(nL^2) O(nL2)的时间内求 n n n个长度不超过 L L L的字符串的最长公共子串的长度。原因是在匹配过程中, B B B串的长度为 j j j的前缀会一直是 A A A串的子串。随便取一个串,枚举这个串的后缀,把这个后缀与其它的串进行KMP,每个串记录匹配过程中出现过的最大的 j j j,取被匹配的每个串的最小值,再取匹配串每个后缀的最大值。