题目描述
给定一个模式串S,以及一个模板串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串P在模式串S中多次作为子串出现。
求出模板串P在模式串S中所有出现的位置的起始下标。
输入描述
第一行输入整数N,表示字符串P的长度。
第二行输入字符串P。
第三行输入整数M,表示字符串S的长度。
第四行输入字符串S。
数据范围:
1≤N≤1041≤N≤10^41≤N≤104
1≤M≤1051≤M≤10^51≤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个值。
字符串的前缀和后缀
如果字符串S1S_1S1和S2S_2S2,存在S1=S2XS_1=S_2XS1=S2X,其中XXX是任意的非空字符串,那就称S2S_2S2为S1S_1S1的前缀。
例如,HarryHarryHarry的前缀包括{H,Ha,Har,Harr}\{H, Ha, Har, Harr\}{H,Ha,Har,Harr},我们把所有前缀组成的集合,称为字符串的前缀集合。
同样可以定义后缀S1=XS2S_1=XS_2S1=XS2, 其中XXX是任意的非空字符串,那就称S2S_2S2为S1S_1S1的后缀。
例如,PotterPotterPotter的后缀包括{otter,tter,ter,er,r}\{otter, tter, ter, er, r\}{otter,tter,ter,er,r},然后把所有后缀组成的集合,称为字符串的后缀集合。要注意的是,字符串本身并不是自己的前缀或后缀。
next[]数组的意义
next[]表示的是字符串的前缀集合与后缀集合的交集中最长元素的长度。
例如,对于ABAABAABA,它的前缀集合为{A,AB}\{A, AB\}{A,AB},后缀集合为{A,BA}\{A, BA\}{A,BA},两个集合的交集为{A}\{A\}{A},那么前缀集合与后缀集合的交集中最长元素的长度为1。
再比如,对于字符串ABABAABABAABABA,它的前缀集合为{A,AB,ABA,ABAB}\{A, AB, ABA, ABAB\}{A,AB,ABA,ABAB},它的后缀集合为{A,BA,ABA,BABA}\{A, BA, ABA, BABA\}{A,BA,ABA,BABA}, 两个集合的交集为{A,ABA}\{A, ABA\}{A,ABA},其中最长的元素为ABAABAABA,长度为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)中就是ababababababababab,其前缀与后缀集合的最长元素为abababababab,长度为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只向前移动、不回溯,所以时间复杂跟源字符串的长度nnn相关,近似于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;
}
本文详细介绍KMP算法的核心概念——部分匹配表next[]及其构建过程,通过实例解释如何利用next[]加速字符串匹配,节省不必要的比较步骤。
1183

被折叠的 条评论
为什么被折叠?



