题目描述
给定一个模式串S,以及一个模板串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串P在模式串S中多次作为子串出现。
求出模板串P在模式串S中所有出现的位置的起始下标。
输入描述
第一行输入整数N,表示字符串P的长度。
第二行输入字符串P。
第三行输入整数M,表示字符串S的长度。
第四行输入字符串S。
数据范围:
1 ≤ N ≤ 1 0 4 1≤N≤10^4 1≤N≤104
1 ≤ M ≤ 1 0 5 1≤M≤10^5 1≤M≤105
输出描述
共一行,输出所有出现位置的起始下标(下标从0开始计数),整数之间用空格隔开。
输入样例
3
aba
5
ababa
输出样例
0 2
算法思想
KMP算法的核心是一个被称为部分匹配表的数组:next[]
。例如对于字符串abababca
,为了方便处理,第一个字符从位置1开始,那么它的各部分如下:
字符 | a | b | a | b | a | b | c | a |
---|---|---|---|---|---|---|---|---|
位置i |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
next[i] |
0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |
就像上图中所示,如果模式字符串(待匹配的字符串)有8个字符,那么next[]
数组就会有8个值。
字符串的前缀和后缀
如果字符串 S 1 S_1 S1和 S 2 S_2 S2,存在 S 1 = S 2 X S_1=S_2X S1=S2X,其中 X X X是任意的非空字符串,那就称 S 2 S_2 S2为 S 1 S_1 S1的前缀。
例如, H a r r y Harry Harry的前缀包括 { H , H a , H a r , H a r r } \{H, Ha, Har, Harr\} { H,Ha,Har,Harr},我们把所有前缀组成的集合,称为字符串的前缀集合。
同样可以定义后缀 S 1 = X S 2 S_1=XS_2 S1=XS2, 其中 X X X是任意的非空字符串,那就称 S 2 S_2 S2为 S 1 S_1 S1的后缀。
例如, P o t t e r Potter Potter的后缀包括 { o t t e r , t t e r , t e r , e r , r } \{otter, tter, ter, er, r\} { otter,tter,ter,er,r},然后把所有后缀组成的集合,称为字符串的后缀集合。要注意的是,字符串本身并不是自己的前缀或后缀。
next[]
数组的意义
next[]
表示的是字符串的前缀集合与后缀集合的交集中最长元素的长度。
例如,对于 A B A ABA ABA,它的前缀集合为 { A , A B } \{A, AB\} {
A,AB},后缀集合为 { A , B A } \{A, BA\} {
A,BA},两个集合的交集为 { A } \{A\} {
A},那么前缀集合与后缀集合的交集中最长元素的长度为1
。
再比如,对于字符串 A B A B A ABABA ABABA,它的前缀集合为 { A , A B , A B A , A B A B } \{A, AB, ABA, ABAB\} { A,AB,ABA,ABAB},它的后缀集合为 { A , B A , A B A , B A B A } \{A, BA, ABA, BABA\} { A,BA,ABA,BABA}, 两个集合的交集为 { A , A B A } \{A, ABA\} { A,ABA},其中最长的元素为 A B A ABA ABA,长度为3。
查找匹配
在源字符串S="ababababca"
中查找模式字符串P="abababca"
,如下图所示:
如果在源串i
处与模式串j+1
处的字符不匹配,即s[i] != p[j + 1]
,说明源字符串中i
之前的next[j]
位字符就一定与模式字符串的第1~j
位是相同的,即源串在第i
位失配,那么 S [ i − j . . . i − 1 ] = P [ 1... j ] S[i - j...i - 1] = P[1...j] S[i−j...i−1]=P[1...j]。
而上面表格中记录了当j = 6
时,ne[j] = 4
,在图1.12 (a)中就是 a b a b a b ababab ababab,其前缀与后缀集合的最长元素为 a b a b abab abab,长度为4
。那么就可以断言,源字符串中i
指针之前的 4
位一定与模式字符串的第1
位至第 4
位是相同的,即长度为 4
的后缀与前缀相同。
这样一来,就可以将这些字符段的比较省略掉。具体的做法是,保持i
指针不动,然后将j
指针指向模式字符串的ne[j]
位即可,如图1.12 (b)所示。
简言之,以图中的例子来说,在 i
处失配,那么源字符串和模式字符串的前边6
位就是相同的。又因为模式字符串的前6
位,它的前4
位前缀和后4
位后缀是相同的,所以推知源字符串i
之前的4
位和模式字符串开头的4
位是相同的。就是图中的灰色部分。那这部分就不用再比较了。
重复这个过程,如果查找过程中,在源串中找到和模式串长度相同的子串,那么就找到一个匹配字符串。
构造next[]
求next[]
的过程完全可以看成字符串匹配的过程,即以模式字符串为源字符串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next
值就是匹配成功的字符串的长度。
具体来说,就是从模式字符串的第2位(注意,next[1] = 0
)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next
值。如下图所示。
时间复杂度
在匹配过程中,源字符串指针i
只向前移动、不回溯,所以时间复杂跟源字符串的长度 n n n相关,近似于 O ( n ) O(n) O(n)。
代码实现
#include <iostream>
using namespace std;
const int N = 100010, M = 1000010;
char p[N], s[M];
int n, m, ne[N];
int main()
{
cin>> n >> p + 1 >> m >> s + 1; //模式串和文本串都从1开始
//求ne[]
ne[1] = 0; //初始状态,第一个字母匹配失败就只能从0开始重新匹配
for(int i = 2, j = 0; i <= n; i ++)
{
//j没有回到起点,并且没有匹配成功
while(j && p[j + 1] != p[i]) j = ne[j];
if(p[j + 1] == p[i]) j++; //匹配成功
ne[i] = j; //将当前匹配个数赋值给ne[i]
}
//kmp匹配
for(int i = 1, j = 0; i <= m; i ++)
{
//j没有回到起点(即没有重新开始匹配),并且模式串的下一个字符跟文本串当前字符匹配失败
while(j && p[j + 1] != s[i])
j = ne[j]; //回溯模式串的指针j,从ne[j]开始继续匹配,相当于将模式串后移
if(p[j + 1] == s[i]) //匹配成功
j++;//模式串指针j后移,继续匹配下一个字符
if(j == n)
{
//模式串匹配完毕
cout << i - n << ' '; //输出匹配起点位置,位置从0开始
j = ne[j];//移动模式串指针j,继续下一次匹配
}
}
return 0;
}