这篇博客继续上篇博客开的坑,把字符串算法里,很重要也很基础的KMP算法总结一下。
简介
KMP算法主要用来处理字符串匹配问题(单模式串)。即给你两个字符串A,B,问是否B是A的子串。如果使用朴素的算法暴力查找,那么时间复杂度为 O(nm),这是我们不能接受的,KMP算法就可以把时间复杂度降到 O(n)。
实现方法
这里有两个字符串,A=“abababaababacb”,B=“ababacb”。我们用两个指针 i 和 j 分别表示,A[i-j+1…i]与B[1…j]完全相等。也就是说,i 是不断增加的,随着 i 的增加,j 相应地变化,且 j 满足以 A[i] 结尾的长度为 j 的字符串正好匹配 B 串的前 j 个字符,现在需要检验 A[i+1] 和 B[j+1] 的关系。当 A[i+1] = B[j+1] 时,i 和 j 各+1,当 j=m 时,我们就说 B 是 A 的子串(B串已经完整匹配了),并且可以根据这时的值算出匹配的位置。当 A[i+1] ≠ B[j+1],KMP的策略是调整 j 的位置(减小 j 值)使得 A[i-j+1…i] 与 B[1…j] 保持匹配并尝试匹配新的 A[i+1] 与 B[j+1]。
继续看如下分析
此时i=j=5,A[6]≠B[6],这表明,此时 j 不能等于 5 了,我们要把 j 改成比它小的值 j’。j’ 可能是多少呢?仔细想一下啊,我们发现,j’ 必须要使得B[1…j]中的头 j’ 个字母和末 j’ 个字母完全相符(这样 j 变成了 j’ 后才能继续保持 i 和 j 的性质)。这个 j’ 当然要越大越好(匹配得尽量长)。在这里,B[1…5]=ababa,头 3 个字母和末 3 个字母都是 aba。而当新的 j 为 3 时,A[6]恰好和B[6]相等。于是,i 变成了 6,而 j 则变成了 4。
从上面我们就可以看到,新的 j 取多少与 i 无关,只与 B 串有关。我们完全可以预处理出这样一个数组 nxt[j],表示当匹配到 B 数组的第 j 个字母而第 j+1 个字母不能匹配了时,新的 j 最大是多少。nxt[j] 应该是所有满足 B[1…k]=B[j-k+1…j]的k(k<j) 的最大值(即最长的相同前缀和后缀)。
再来,A[7]=B[5],i 和 j 又各增加 1。这时又出现了 A[i+1]≠B[j+1]的情况
由于 nxt[5]=3,因此新的 j=3
这时,新的 j=3 仍然不能满足 A[i+1]=B[j+1],此时我们再次减小 j 值,将 j 更新为 nxt[3] ("aba"最长公共前后缀长度为 1,因此nxt[3]=1)
现在,i 还是 7,j 已经变成 1 了。A[i+1]≠B[j+1]。这样,j 必须减小到 nxt[1],即 0
终于,A[8]=B[1],i 变为 8,j 为 1。事实上,有可能 j 到了 0 仍然是 A[i+1]≠B[]。因此,准确的说法是,当 j=0 时,我们增加 i 值,但忽略 j 直到出现 A[i]=B[1]为止。
代码如下
void kmp()
{
int j=0;
for(int i=0;i<n;++i)
{
while(j>0&&B[j+1]!=A[i+1])
j=nxt[j];//不能继续匹配且j还没减到0,减小j值
if(B[j+1]==A[i+1])
++j;//能继续匹配,j+1
if(j==m)//找到一处匹配
{
printf("%d\n",i+1-m+1);//子串串首在母串中的位置
j=nxt[j];//继续匹配
}
}
}
最后的 j=nxt[j] 是为了让程序继续做下去,因为我们可能找到多处匹配,两处匹配可以重叠。
这个程序或许比想象的要简单,因为对于 i 值的不断增加,代码用的是 for 循环。因此,这个代码可以形象地理解:扫描字符串 A,并更新可以匹配到 B 的什么位置。
现在,我们还遗留两个重要的问题:为什么这个程序是线性的?如何快速预处理 nxt 数组?
虽然for循环里嵌套了一层while循环,但是for循环j最多加 n 次,所以while循环最多执行 n 次,所以时间复杂度最坏情况下也是 O(n)。
如果朴素处理 nxt 数组,那么时间复杂度为 O(m2) 甚至 O(m3)。其实我们像刚才模式串匹配的过程一样去预处理 nxt 数组。
例如,B=“ababacb”,假如我们已经求出了nxt[1],nxt[2],nxt[3],nxt[4],看如何求出nxt[5],nxt[6]。
易知,B[5]=B[3],所以 nxt[5]=nxt[4]+1=3。
B[6]≠B[4],此时没匹配成功,我们需要将前缀减小,即此时指针应该指向 B[1],即通过 nxt[3],将指针调至 B[1]。因为当前前后缀不相等,所以我们需要看更小的前后缀是否满足,这和之前模式串匹配过程是一样的。
B[6]≠B[2],指针应该指到0了,此时B[6]≠B[1],那么 nxt[6]=0。
此时,B[7]≠B[1],所以 nxt[7]=0。
代码如下
void getnxt()//求next数组
{
nxt[1]=0;
int j=0;
for(int i=1;i<m;++i)
{
while(j>0&&T[j+1]!=T[i+1])//不能继续匹配且j还没减到0,考虑退一步
j=nxt[j];
if(T[j+1]==T[i+1])//能匹配,j+1
++j;
nxt[i+1]=j;
}
}
样例分析
剪花布条(HDU2087)
题目大意
给一个母串,一个模式串,看母串能分割出多少与模式串相同的子串。
解题思路
就是利用KMP,找母串中是否有与模式串相同的子串,找到相同的直接从母串中去掉,从下个位置继续找。在原先KMP的模板上稍作修改即可。
AC代码
#include <iostream>
#include <stdio.h>
#include <cstring>
using namespace std;
const int maxn=1e3+5;
int nxt[maxn];
char S[maxn],T[maxn];
int ans;
void getnxt()//求next数组
{
int len=strlen(T+1);
nxt[1]=0;
int j=0;
for(int i=1;i<len;++i)
{
while(j>0&&T[j+1]!=T[i+1])//不能继续匹配且j还没减到0,考虑退一步
j=nxt[j];
if(T[j+1]==T[i+1])//能匹配,j+1
++j;
nxt[i+1]=j;
}
}
void kmp()//kmp找母串子串有多少模式串
{
int lent=strlen(T+1);
int lens=strlen(S+1);
int j=0;
for(int i=0;i<lens;++i)
{
while(j>0&&T[j+1]!=S[i+1])//不能继续匹配且j还没减到0,减小j的值
j=nxt[j];
if(T[j+1]==S[i+1])//能继续匹配j,j的值+1
j++;
if(j==lent)//找到一处匹配
{
ans++;
j=0;
}
}
}
void init()//初始化
{
ans=0;
}
int main()
{
while(~scanf("%s",S+1)&&S[1]!='#')
{
scanf("%s",T+1);
init();
getnxt();
kmp();
printf("%d\n",ans);
}
return 0;
}