字符串KMP——用途广泛的字符串匹配算法 + 扩展KMP——特殊定义的字符串匹配
引
入
引入
引入
“ SY 和 WYX 在看毛片。(几 毛 钱买到的动作 片,毛 片)
WYX 突然想回味一个片段,但是只记得台词里面有一句挺长的 “∗
∗
∗
∗
****
∗∗∗∗ ”,于是,他们找到剧本,想看 “
∗
∗
∗
∗
****
∗∗∗∗ ”在剧本中出现了几次,分别是在什么地方。
他们遇到了麻烦,这样的剧本随便就是数百万单词,数千万字母,而且 “∗
∗
∗
∗
****
∗∗∗∗ ”长度也有上千万。
为了解决这个问题,SY 发明了一个 O(N) 的字符串匹配算法,以这次的目的命名,就叫 KMP(看毛片) 算法。 ”
但是他们不知道,前人已经发明此算法:
KMP 算法是一种改进的字符串匹配算法,由 D.E.Knuth,J.H.Morris 和 V.R.Pratt 提出的,因此人们称它为克努特—莫里斯—普拉特操作,简称 KMP 算法。
——摘自百度百科
“ 气愤的 SY 只好继续看毛片,并顺便拿了 NOIP2020提高组 CQ前十 ”
K
M
P
算
法
讲
解
KMP算法讲解
KMP算法讲解
引入里面讲的很形象了,
K
M
P
KMP
KMP 算法是用来解决字符串匹配问题的,
问题原型就是在一个大字符串
S
1
S1
S1 里找一个小字符串
S
2
S2
S2 出现了多少次。
暴力怎么做的不用我说了吧,咱们直接进入正题。
K
M
P
KMP
KMP 算法由两个子任务组成,求
S
2
S2
S2 每个前缀的最长
b
o
r
d
e
r
border
border 和 在
S
1
S1
S1 中匹配
S
2
S2
S2 。
Subtask 1 求 border(求 next )
b
o
r
d
e
r
border
border 是 “边界” 的意思,字符串内既是前缀又是后缀(而不等于原串)的一个子串,形象地叫它为该字符串的
b
o
r
d
e
r
border
border.
例:
- abcab d abcab
- SY is such a SY
上面两个字符串中加粗的地方就是该字符串的最长
b
o
r
d
e
r
border
border,字符串的
b
o
r
d
e
r
border
border 并不唯一,比如第一个字符串就还有另一个
b
o
r
d
e
r
border
border :“ ab ”,但是不是最长的。
根据这个定义,我们可以想想怎么线性地求
S
2
S2
S2 每一个前缀的最长
b
o
r
d
e
r
border
border 的长度。
在
K
M
P
KMP
KMP 算法中,我们定义
S
S
S 第
i
i
i 个前缀的最长
b
o
r
d
e
r
border
border 的长度为
n
e
x
t
S
[
i
]
next_S[i]
nextS[i] (为什么叫 “next”,笔者也很好奇 )
从字符串前端算起,很明显,由于
n
e
x
t
[
i
]
<
i
next[i]<i
next[i]<i,所以
n
e
x
t
[
0
]
=
n
e
x
t
[
1
]
=
0
next[0]=next[1]=0
next[0]=next[1]=0. 1
然后往后算,设当前算到的位置为
i
i
i ,
首先,如果
S
[
i
]
=
=
S
[
n
e
x
t
[
i
−
1
]
+
1
]
S[i]==S[next[i-1]+1]
S[i]==S[next[i−1]+1] ,那么
n
e
x
t
[
i
]
=
n
e
x
t
[
i
−
1
]
+
1
next[i]=next[i-1]+1
next[i]=next[i−1]+1 ,而且这是
n
e
x
t
[
i
]
next[i]
next[i] 最好的情况,可以直接完事,去求
i
+
1
i+1
i+1 了,因为如果
n
e
x
t
[
i
]
>
n
e
x
t
[
i
−
1
]
+
1
next[i] > next[i-1]+1
next[i]>next[i−1]+1 的话,
n
e
x
t
[
i
−
1
]
next[i-1]
next[i−1] 肯定可以等于
n
e
x
t
[
i
]
−
1
next[i]-1
next[i]−1.
(next[i] : #### # ####
→
\rightarrow
→ next[i-1] : #### # ###(#))
那么否则就得找
i
−
1
i-1
i−1 的次大的
b
o
r
d
e
r
border
border ,以此类推。由于次大的
b
o
r
d
e
r
border
border 肯定满足是最大的 border 的前缀且后缀,因为:
- 该
b
o
r
d
e
r
border
border 对应最大
b
o
r
d
e
r
border
border 前缀部分的前缀:AAAA B AAAA
- 该
b
o
r
d
e
r
border
border 对应最大
b
o
r
d
e
r
border
border 后缀部分的后缀:AAAA B AAAA
- 最大
b
o
r
d
e
r
border
border 前缀部分和后缀部分显然相同:AAAA B AAAA
于是,可以充分证明,若当前的
b
o
r
d
e
r
border
border 大小为
x
x
x ,则次大的
b
o
r
d
e
r
border
border 为
n
e
x
t
[
x
]
next[x]
next[x] (前缀部分的
n
e
x
t
next
next)。
好,我们就可以处理出
S
S
S 每一个位置的
n
e
x
t
next
next 了。
那为什么它是线性的呢?我们可以隐约地意识到,每个位置的
n
e
x
t
next
next 没有向左扩展的过程,只有向右扩展,
由于
n
e
x
t
[
i
]
≤
n
e
x
t
[
i
−
1
]
+
1
next[i] ≤ next[i-1]+1
next[i]≤next[i−1]+1 ,所以整个计算过程中,
“
i
−
n
e
x
t
[
i
]
”
“ i-next[i] ”
“i−next[i]” 这个量就从来没下降过,而且除了一开始判断
“
S
[
i
]
=
=
S
[
n
e
x
t
[
i
−
1
]
+
1
]
”
“S[i]==S[next[i-1]+1]”
“S[i]==S[next[i−1]+1]” 可能使该量不变以外,找次大
b
o
r
d
e
r
border
border 的操作每次一定会使
“
i
−
n
e
x
t
[
i
]
”
“ i-next[i] ”
“i−next[i]”变大,因此它是线性的。
模板
void INIT(char *ss,int *nxt,int n) {
nxt[0] = nxt[1] = 0;
for(int i = 2;i <= n;i ++) {
int nm = nxt[i-1]; nxt[i] = 0;
while(nm && ss[nm+1] != ss[i]) nm = nxt[nm];
if(ss[nm+1] == ss[i]) nxt[i] = nm+1;
}
return ;
}
Subtask 2 字符串匹配
K
M
P
KMP
KMP 算法实际上是通过求出
S
1
S1
S1 每一个位置
i
i
i 向前延伸出最长的一段,满足是
S
2
S2
S2 的前缀,如果该段长度
=
l
e
n
g
t
h
S
2
= length_{S2}
=lengthS2 ,那么
[
i
−
l
e
n
g
t
h
S
2
+
1
,
i
]
[i-length_{S2}+1\;,\;i]
[i−lengthS2+1,i] 就是
S
2
S2
S2 的一个出现位置,也就是说
K
M
P
KMP
KMP 是间接地解决了这个问题,这表明着该算法的功能可以更强大。
怎么做呢
仿照着求
n
e
x
t
next
next 的推导,我们来求这个……不妨定义它为
F
F
F 吧,设
F
[
i
]
F[i]
F[i] 为位置
i
i
i 向前延伸出最长的一段,满足是
S
2
S2
S2 的前缀的长度。
从左到右依次计算吧,首先
F
[
0
]
=
0
F[0]=0
F[0]=0. 1
接下来对于过程中的
i
i
i ,如果
S
1
[
i
]
=
=
S
2
[
F
[
i
−
1
]
+
1
]
S1[i]==S2[F[i-1]+1]
S1[i]==S2[F[i−1]+1] 那么
F
[
i
]
F[i]
F[i] 直接等于
F
[
i
−
1
]
+
1
F[i-1]+1
F[i−1]+1 完事,否则找
n
e
x
t
[
F
[
i
−
1
]
]
next[F[i-1]]
next[F[i−1]],然后是
n
e
x
t
[
n
e
x
t
[
F
[
i
−
1
]
]
]
next[next[F[i-1]]]
next[next[F[i−1]]] ……直到后面那一位符号匹配。
这时候就会发现
n
e
x
t
[
]
next[]
next[] 有多么大的用处,因为其又是后缀又是前缀的性质,使得
i
i
i 可以正常地从
F
[
i
−
1
]
F[i-1]
F[i−1] 的一个
b
o
r
d
e
r
border
border 出转移过来。
它的复杂度和正确性都和
n
e
x
t
next
next 的证明类似,而且大多数人其实是第一部分看不懂而已,那就留给读者们一个思考空间吧(笔者要写扩展KMP了……)
模板
//这里的代码特别灵活,每个题都不一样,笔者就不贴了
e
x
K
M
P
算
法
讲
解
exKMP算法讲解
exKMP算法讲解
“加了 ‘ex’前缀的算法总会变得高端一些呢 ”
前面说了,
K
M
P
KMP
KMP 算法是求 “
S
1
S1
S1 每一个位置
i
i
i 向前延伸出最长的一段,满足是
S
2
S2
S2 的前缀的长度 ” ,而扩展
K
M
P
KMP
KMP 则是求 “
S
1
S1
S1 每一个位置
i
i
i 向后延伸出最长的一段,满足是
S
2
S2
S2 的前缀的长度 ” ,即,
S
1
S1
S1 每个后缀与
S
2
S2
S2 的最长公共前缀。
该算法也有两个子任务,求
S
2
S2
S2 每个后缀和
S
2
S2
S2 本身的最长公共前缀长度 和 在
S
1
S1
S1 每个后缀中匹配
S
2
S2
S2(如上).
Subtask 1 ···
不妨设
e
x
[
i
]
ex[i]
ex[i] 为
S
2
i
S2\;\,i
S2i 开头的后缀和
S
2
S2
S2 本身的最长公共前缀长度,然后我们开始想怎么线性求它。
首先,
e
x
[
1
]
=
l
e
n
g
t
h
S
2
ex[1]=length_{S2}
ex[1]=lengthS2,
e
x
[
2
]
ex[2]
ex[2] 可以暴力求出来。 1
接下来往后算,到了当前位置
i
i
i ,若
e
x
[
i
−
1
]
−
1
>
e
x
[
2
]
ex[i-1]-1 > ex[2]
ex[i−1]−1>ex[2] ,则
e
x
[
i
]
=
e
x
[
2
]
ex[i]=ex[2]
ex[i]=ex[2]。

因为它不能变得更长了,

如果变得更长的话,

会出问题的,
e
x
[
2
]
ex[2]
ex[2] 就可以变得更大了,因为
e
x
[
i
−
1
]
ex[i-1]
ex[i−1] 涵盖了更大范围的公共前缀,在
e
x
[
i
−
1
]
ex[i-1]
ex[i−1] 范围内都可以当作
S
2
S2
S2 开头考虑。
但是这并不能很好地衔接
i
+
1
i+1
i+1 ,因为这样一来直接进入
i
+
1
i+1
i+1 的话就要回退了,所以我们继续再判断是否
e
x
[
i
−
1
]
−
1
>
e
x
[
3
]
ex[i-1]-1 > ex[3]
ex[i−1]−1>ex[3](决定
e
x
[
i
+
1
]
ex[i+1]
ex[i+1] ) …… 最后起码会止步于
e
x
[
i
−
1
]
−
1
>
e
x
[
i
−
1
]
ex[i-1]-1>ex[i-1]
ex[i−1]−1>ex[i−1] 的判断(因为这肯定不成立),因此不存在访问了未计算部分的情况。
要是对于决定
e
x
[
i
+
x
−
2
]
ex[i+x-2]
ex[i+x−2] 的判断,
e
x
[
i
−
1
]
−
1
≤
e
x
[
x
]
ex[i-1]-1 ≤ ex[x]
ex[i−1]−1≤ex[x] 呢?那就暴力从
i
+
e
x
[
i
−
1
]
−
2
i+ex[i-1]-2
i+ex[i−1]−2 再向右扩展就是了。这样一来就不会向左回退,只会向右扩展,保证了复杂度线性。
模板
void INITex(char *ss,int *ex,int n) {
ex[0] = 0;ex[1] = n;ex[2] = 0;
int l = 0,r = 0;
for(int i = 2;i <= n;i ++) { ex[i] = 0;
if(i <= r) ex[i] = min(ex[i-l+1],r-i+1);
while(i + ex[i] <= n && ss[i+ex[i]] == ss[ex[i]+1]) ex[i] ++;
if(i + ex[i] - 1 > r) l = i,r = i + ex[i] - 1;
}
return ;
}
Subtask 2 ···
也可以仿照
e
x
[
]
ex[]
ex[] 的计算。
不妨设
G
[
i
]
G[i]
G[i] 为
S
1
S1
S1 的第
i
i
i 位的后缀与
S
2
S2
S2 的最长公共前缀。
类似地,首先,
G
[
1
]
G[1]
G[1] 可以暴力跑出来。 1
然后,遍历到每一个
i
i
i ,若
G
[
i
−
1
]
−
1
>
e
x
[
2
]
G[i-1]-1 > ex[2]
G[i−1]−1>ex[2] (注意这里是
e
x
[
2
]
ex[2]
ex[2]),则
G
[
i
]
=
e
x
[
2
]
G[i]=ex[2]
G[i]=ex[2],然后继续判是否
G
[
i
−
1
]
−
1
>
e
x
[
3
]
G[i-1]-1>ex[3]
G[i−1]−1>ex[3] 来决定 G[i+1] …… 这里就不用担心未计算的问题,因为
e
x
[
]
ex[]
ex[] 肯定都处理完了。
直到找到一个不成立的,就从
i
+
G
[
i
−
1
]
−
2
i+G[i-1]-2
i+G[i−1]−2 再向右找,和
S
u
b
t
a
s
k
1
Subtask\;1
Subtask1 类似。
模板
for(int i = 1;i <= n;i ++) {if(s2[i] == s1[i]) G[1] = i;else break;}
int l = 1,r = G[1];
for(int i = 2;i <= n;i ++) { G[i] = 0;
if(i <= r) G[i] = min(ex[i - l + 1],r - i + 1);
while(i + G[i] <= n && s1[i + G[i]] == s2[G[i] + 1]) G[i] ++;
if(i + G[i] - 1 > r) l = i,r = i + G[i] - 1;
}
字符串KMP——用途广泛的字符串匹配算法 + 扩展KMP——特殊定义的字符串匹配的更多相关文章
- 51Nod - 1304 :字符串的相似度 (裸的扩展KMP)
我们定义2个字符串的相似度等于两个串的相同前缀的长度.例如 "abc" 同 "abd" 的相似度为2,"aaa" 同 "aaab& ...
- 【怪物】KMP畸形变种——扩展KMP
问题 参考51nod1304这道题: 很显然我们要求的是S的每个后缀与S的最长公共前缀的长度之和. 暴力 假设我们把next[i]表示为第i个后缀与S的最长公共前缀的长度. 现在我们想了:这个next ...
- 字符串匹配算法(KMP)
字符串匹配运用很广泛,举个简单例子,我们每天登QQ时输入账号和密码,大家有没有想过账号和密码是怎样匹配的呢?登录需要多长时间和匹配算法的效率有直接的关系. 首先理解一下前缀和后缀的概念: 给出一个问题 ...
- 数据结构学习之字符串匹配算法(BF||KMP)
数据结构学习之字符串匹配算法(BF||KMP) 0x1 实验目的 通过实验深入了解字符串常用的匹配算法(BF暴力匹配.KMP.优化KMP算法)思想. 0x2 实验要求 编写出BF暴力匹配.KM ...
- 字符串匹配算法之 kmp算法 (python版)
字符串匹配算法之 kmp算法 (python版) 1.什么是KMP算法 KMP是三位大牛:D.E.Knuth.J.H.MorriT和V.R.Pratt同时发现的.其中第一位就是<计算机程序设计艺 ...
- hdu6153 扩展kmp求一个字符串的后缀在另一个字符串出现的次数。
/** 题目:hdu6153 链接:http://acm.hdu.edu.cn/showproblem.php?pid=6153 题意:给定两个串,求其中一个串t的每个后缀在另一个串s中出现的次数乘以 ...
- kuangbin专题十六 KMP&&扩展KMP HDU2609 How many (最小字符串表示法)
Give you n ( n < 10000) necklaces ,the length of necklace will not large than 100,tell me How man ...
- 【距离GDOI:137天】 扩展KMP...字符串QAQ
上次和黄神两人一合计,干脆我学字符串他学图论,然后两人相互教...但以蒟蒻最近这状态来看,估计会到时候也教不了QAQ 扩展KMP大概是下面这个课件讲的这样 http://wenku.baidu.com ...
- 字符串匹配算法之kmp算法
kmp算法是一种效率非常高的字符串匹配算法,是由Knuth,Morris,Pratt共同提出的模式匹配算法,所以简称KMP算法 算法思想 在一个字符串中查找另一个字符串时,会遇到如下图的情况 我们通常 ...
随机推荐
- 阿里云FTP服务配置
阿里云的CENTOS 7.4 并没有开启防火墙服务 所以好多人配置了FTP后会出现各种不能访问的问题 关键原因在于端口没有开放.设置端口阿里云ECS的管理控制台中"安全组" &qu ...
- iNeuOS工业互联网操作系统,视图建模(WEB组态)增加2154个行业矢量图元、大屏背景及相关图元
1. 概述 现在三维数字孪生(3D)比较流行,各行业各领域的项目也都在上数字孪生项目或是项目中包括数字孪生模块,能做的厂家也很多.从全厂区的应用视觉的冲击力还是比较震撼,但是数字孪生不太可能包括 ...
- 使用.NetCore自带的后台作业,出入队简单模拟生产者消费者处理请求响应的数据
环境:Core:3.1的项目 说明:由于该方案为个人测试项目,重启时队列中的部分数据很可能会丢失, 对数据有要求的该方案不适用,不能照搬需要持久化处理, 另外发布到Linux Docker中通常不会自 ...
- Python教程:执行cmd命令
我们通常可以使用os模块的命令进行执行cmd 方法一:os.system def system(*args, **kwargs): # real signature unknown "&qu ...
- Redis入门到精通01
Redis入门到精通 目录 Redis入门到精通 一.Redis缓存框架基本介绍 1.1Redis的应用场景 二.Redis的安装方式 2.1Windows操作系统安装Redis 2.2Linux操作 ...
- MYSQL--安装2(命令行连接到数据库)
使用命令行窗口连接 MYSQL 数据库[操作示意图]
- javascript与事件
javascript与事件 js常用事件 js捕捉回车键 js回调函数的event参数(一直都有传,就看你有没有接,有没有接的需求) event.keyCode(事件句柄:onkeydown) key ...
- 【一本通基础DP基础模型】摘花生
题面 题目描述 Hello Kitty想摘点花生送给她喜欢的米老鼠.她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来.地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生, ...
- windows版本rabbitmq安装及日志level设置
1.DirectX Repair 安装缺失的C++组件,不安装缺失的组件会造成第二部安装erl文件夹缺少bin文件夹2.安装otp_win64_23.1 1.配置 ERLANG_HOME:地址为Erl ...
- 一个小 Trick
平方变两次 一个状态 \(S\) 有一个贡献,所有状态 \(S\) 组成集合 \(U\) . 然后我们要统计下面这个东西 \[ans=\sum_{S\in U}f^2(S) \] 然后我们就可以看作是 ...