题目来源:AcWing 831. KMP字符串
一、题目描述
给定一个模式串 S S S,以及一个模板串 P P P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串 P P P 在模式串 S S S 中多次作为子串出现。
求出模板串 P P P 在模式串 S S S 中所有出现的位置的起始下标。
输入格式
第一行输入整数
N
N
N,表示字符串
P
P
P 的长度。
第二行输入字符串 P P P。
第三行输入整数 M M M,表示字符串 S S S 的长度。
第四行输入字符串 S S S。
输出格式
共一行,输出所有出现位置的起始下标(下标从
0
0
0 开始计数),整数之间用空格隔开。
数据范围
1
≤
N
≤
1
0
5
1≤N≤10^5
1≤N≤105
1
≤
M
≤
1
0
6
1≤M≤10^6
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2
二、算法思路
KMP算法是字符串匹配算法,对暴力的那种一一比对的方法进行了优化,使时间复杂度大大降低。
基本定义:
s[ ]
:是模式串,即较长的字符串。p[ ]
:是模板串,即较短的字符串。非平凡前缀
:指除了最后一个字符之外,字符串的全部头部字符组合,简称前缀。非平凡后缀
:指除了第一个字符之外,字符串的全部尾部字符组合,简称后缀。部分匹配值
:前缀和后缀的最长公共元素的长度。next[ ] 部分匹配表
:即 n e x t next next 数组,它存储的是每一个下标对应的“部分匹配值”,是KMP算法的核心
核心思想:在每次匹配 s s s 串失败时,不是把 p p p 串整体往后移动一位继续匹配,而是把 p p p 串整体往后移动至下一次可以和前面部分匹配的位置,这样就可以跳过大多数失败的步骤。而每次 p p p 串整体移动的步数就是通过查找 n e x t next next 数组确定的。
next数组的含义及手动模拟
然后来说明一下
n
e
x
t
next
next 数组的含义:对
n
e
x
t
[
j
]
next[ j ]
next[j],是
p
[
1
,
j
]
p[ 1, j ]
p[1,j] 串中前缀和后缀相同的最大长度(部分匹配值),即
p
[
1
,
n
e
x
t
[
j
]
]
=
p
[
j
−
n
e
x
t
[
j
]
+
1
,
j
]
p[ 1, next[ j ] ] = p[ j - next[ j ] + 1, j ]
p[1,next[j]]=p[j−next[j]+1,j]。
例如:
手动模拟求
n
e
x
t
next
next 数组,对
p
=
“
a
b
c
a
b
”
p = “abcab”
p=“abcab”
p p p | a a a | b b b | c c c | a a a | b b b |
---|---|---|---|---|---|
下标 | 1 1 1 | 2 2 2 | 3 3 3 | 4 4 4 | 5 5 5 |
n e x t [ ] next[ ] next[] | 0 0 0 | 0 0 0 | 0 0 0 | 1 1 1 | 2 2 2 |
对 n e x t [ 1 ] next[ 1 ] next[1] :前缀 = 空集 ——————————— 后缀 = 空集 ———————————— n e x t [ 1 ] = 0 next[1] = 0 next[1]=0;
对 n e x t [ 2 ] next[ 2 ] next[2] :前缀 = { a } ——————————— 后缀 = { b } ————————————— n e x t [ 2 ] = 0 next[2] = 0 next[2]=0;
对 n e x t [ 3 ] next[ 3 ] next[3] :前缀 = { a , ab } ————————— 后缀 = { c , bc} ——————————— n e x t [ 3 ] = 0 next[3] = 0 next[3]=0;
对 n e x t [ 4 ] next[ 4 ] next[4] :前缀 = { a , ab , abc } ——————— 后缀 = { a , ca , bca } ———————— n e x t [ 4 ] = 1 next[4] = 1 next[4]=1;
对 n e x t [ 5 ] next[ 5 ] next[5] :前缀 = { a , ab , abc , abca } ———— 后缀 = { b , ab , cab , bcab} ————— n e x t [ 5 ] = 2 next[5] = 2 next[5]=2;
KMP算法匹配过程
KMP主要分两步:求 n e x t next next 数组、匹配字符串。个人觉得匹配操作容易懂一些,疑惑我一整天的是求next数组的思想。所以先把匹配字符串讲一下。
s
s
s 串和
p
p
p 串都是从下标
1
1
1开始的。
i
i
i 从
1
1
1开始,
j
j
j 从
0
0
0开始,每次由
s
[
i
]
s[ i ]
s[i] 和
p
[
j
+
1
]
p[ j + 1 ]
p[j+1]进行比较:
当匹配过程到上图所示时,s[a, b] = p[1, j] && s[i] != p[j + 1]
,此时要整体移动
p
p
p 字符串(不是移动
1
1
1格,而是直接移动到下次能够匹配的位置)
其中①串为[1, next[j]]
,③串为[j - next[j] + 1, j]
。由匹配可知①串等于③串,③串等于②串。所以直接整体移动
p
p
p 串使①串到③串的位置即可。这个操作可由j = next[j]
直接完成。 如此往复下去,当j == m
时匹配成功。
匹配部分代码模板
for(int i = 1, j = 0; i <= n; i++)
{
while(j && s[i] != p[j + 1]) j = ne[j];
//如果j有对应p串的元素, 且s[i] != p[j+1], 则失配, 移动p串
//用while是由于移动后可能仍然失配,所以要继续移动直到匹配或整个p串移到后面(j = 0)
if(s[i] == p[j + 1]) j++;
//当前元素匹配,j移向p串下一位
if(j == m)
{
//匹配成功,进行相关操作
j = next[j]; //继续匹配下一个子串
}
}
注:采用上述的匹配方法,即 s [ i ] s[i] s[i] 与 p [ j + 1 ] p[j+1] p[j+1] 比较,而不推荐下标从 0 0 0开始的原因我认为是:若下标从 0 0 0开始的话, n e x t next next 数组的值都会相应 − 1 -1 −1,这就会导致它的实际含义与其定义的意思不符(部分匹配值和 n e x t next next 数组值相差 1 1 1),思维上有点违和,容易出错。
KMP算法求next数组
next数组的求法是通过模板串自己与自己进行匹配操作得出来的(代码和匹配操作几乎一样)。
求next部分代码模板
for(int i = 2, j = 0; i <= m; i++)
{
while(j && p[i] != p[j + 1]) j = next[j];
if(p[i] == p[j + 1]) j++;
next[i] = j;
}
代码和匹配操作的代码几乎一样,关键在于每次移动 i i i 前,将 i i i 前面已经匹配的长度记录到 n e x t next next 数组中。
三、代码
#include <iostream>
using namespace std;
const int N = 100010, M = 10010; //N为模式串长度,M匹配串长度
int n, m;
int ne[M]; //next[]数组,避免和头文件next冲突
char s[N], p[M]; //s为模式串, p为匹配串
int main()
{
cin >> n >> s + 1 >> m >> p + 1; //下标从1开始
//求next[]数组
for(int i = 2, j = 0; i <= m; i++)
{
while(j && p[i] != p[j + 1]) j = ne[j];
if(p[i] == p[j + 1]) j++;
ne[i] = j;
}
//匹配操作
for(int i = 1, j = 0; i <= n; i++)
{
while(j && s[i] != p[j + 1]) j = ne[j];
if(s[i] == p[j + 1]) j++;
if(j == m) //满足匹配条件,打印开头下标, 从0开始
{
//匹配完成后的具体操作
//如:输出以0开始的匹配子串的首字母下标
//printf("%d ", i - m); (若从1开始,加1)
j = ne[j]; //再次继续匹配
}
}
return 0;
}