【匹配字符串S与T,判断T是否为S的子串】
即在主串S中快速匹配是否存在一个子串等同于模式串T
S为被匹配串(主串),T为匹配串(模式串)
实现方式:
在最普通的算法中,我们总是拿两个指针指向两个字符串的不同位置来匹配
如果某个字符匹配成功就把两个光标同时移动到下一个位置,即
如果不匹配,那么模式串就该整体右移一位,重复上述方式进行匹配
模式串的光标要返回模式串的第一个位置,主串的光标移动到移动后模式串开头对应的主串的位置,即
然后再继续匹配下去,直到模式串中的光标到达模式串的末尾,且最后一个字符也能匹配时(说明在原串中找到了一个子串等同于模式串),此时就能作为答案输出
但是,每一次不匹配就必须回溯光标到模式串开头对应,然后模式串整体往右移动一位,再继续匹配下去
为了能够做到O(n)时间内完成匹配,可以从这两点入手,去优化这一匹配的过程
引入Next数组概念,Next数组是从模式串中获得的
Next[i] 表示现在模式串已经匹配到了第 i-1 位,但是第 i 位的字符与原串不匹配
可以发现已匹配的串abcab中,前缀ab等于后缀ab
所以抓住这个特征,下一步就可以直接让模式串右移到后缀位置
可以发现移动后后缀位置是已经完成匹配的,所以原串的光标位置不变,继续匹配下去(O(n)的原因)
如果这样还是不能匹配,继续抓住已匹配的串ab继续右移下去
发现ab没有前缀等于后缀的情况出现(前缀后缀不能等于原串)
说明模式串只能直接移动到此时不匹配的位置上继续下去,即
所以求出Next数组是关键
void getNext(){
int i,j=-1;
Next[0]=-1;
for(i=1;i<Tlen;i++){
while(j>-1 && T[ j+1 ]!=T[ i ])
j=Next[j];
if(T[j+1]==T[i])
j++;
Next[i]=j;
}
}
Next数组就是寻找前 i 个字符 前缀=后缀 的最长长度,且前缀=后缀≠原子串
在执行过程中,有以下几个例子://下标从0开始
对于aba,在查找ab时可以得知Next[1]=-1,此时j=-1,所以i=2时对比的是j+1=0,发现a=a成立,所以j=0,Next[2]=0
再比如aaaaa,在查找aaaa时得知Next[3]=2 , j=2 所以i=4时对比的是j+1=3,发现a=a成立,j++,Next[4]=j=3
再比如aaaab,在查找aaaa时得知Next[3]=2 , j=2 所以i=4时对比的是j+1=3,发现a=b不成立,j=Next[j]=Next[3]=2,对比i=4,j=2不成立,继续,直到j=-1,所以最后Next[4]=-1
然后到应用部分
int KMP_Position(){
int i,j=-1;
for(i=0;i<Slen;i++){
while(j>-1&&T[j+1]!=S[i])
j=Next[j];
if(T[j+1]==S[i])
j++;
if(j==Tlen-1)
return i-Tlen+1;//找到后返回位置
}
return -1;//没找到
}
其中,对于下面这一句
while(j>-1&&T[j+1]!=S[i])
j=Next[j];
这句说明在第 i 个位置原串S与模式串T失配了,但是由上面可以得知我们不需要回溯原串的光标i,只需要回溯模式串光标 j 即可,而j=Next[j] 就是指应该回溯到哪个位置(指模式串应该右移到什么位置),然后继续匹配此时的原串 i 位置和模式串 j 位置,直到完全不匹配时(j = -1)或者找到匹配位置(T[j+1] != S[i] )时退出这个循环
if(T[j+1]==S[i])
j++;
这句判断跳出while循环的条件是不是由匹配才退出的,如果是匹配的话,j可以+1
然后记录此时Next[i] 的值
如果j==Tlen-1 成立,说明模式串的光标已经到了模式串的末尾,且最后一个位置也是匹配的,所以此时就能返回答案位置了—— i -Tlen + 1
另外,KMP还能快速查找模式串在原串中出现的次数,只要把输出条件更改成
if(j==Tlen-1){
cnt++;
j=Next[j];
}
然后用cnt作为答案即可
完整程序:
#include<bits/stdc++.h>
using namespace std;
string S,T;
int Slen,Tlen,Next[1000050];
void getNext(){
int i,j=-1;
Next[0]=-1;
for(i=1;i<Tlen;i++){
while(j>-1&&T[j+1]!=T[i])
j=Next[j];
if(T[j+1]==T[i])
j++;
Next[i]=j;
}
}//模式串T的Next数组预处理
int KMP_Position(){
int i,j=-1;
for(i=0;i<Slen;i++){
while(j>-1&&T[j+1]!=S[i])
j=Next[j];
if(T[j+1]==S[i])
j++;
if(j==Tlen-1)
return i-Tlen+1;
}
return -1;
}//匹配模式串第一次出现在主串中的位置
int KMP_Count(){
int i,j=-1,cnt=0;
for(i=0;i<Slen;i++){
while(j>-1&&T[j+1]!=S[i])
j=Next[j];
if(T[j+1]==S[i])
j++;
if(j==Tlen-1){
cnt++;
j=Next[j];
}
}
return cnt;
}//匹配模式串在主串中出现的次数
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>S>>T;
Slen=S.size();
Tlen=T.size();
getNext();
//cout<<KMP_Position()<<'
';
//cout<<KMP_Count()<<'
';
return 0;
}
附:
因为KMP算法的 getNext 函数求的是T字符串的前 i 个字符最长的 前缀=后缀 的长度,且这个长度不等于自身长度
所以如果求出的 Next[ LEN ] 满足
LEN % ( LEN - Next[ LEN] ) == 0
就可以说明这个字符串是由某个子串循环 LEN / ( LEN - Next[ LEN] ) 次得到的
且这个子串是最短循环子串
也就是说, LEN / ( LEN - Next[ LEN] ) 是字符串子串中最多的循环次数
例题1 POJ 2406
题目求的就是某个子串在整个字符串中循环的最多次数
例题2 POJ 1961
求的是所有循环次数大于等于 2 的循环节的循环次数