luogu
给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。然后输出next数组
这篇题解鸽了好久,因为我一直都没有完全理解看猫片,这东西是真的难。KMP
的用处就是在母串里面找子串。
朴素做法是一位一位地匹配,判断子串和母串的字符是否相同,所以时间复杂度是O(mn)。(m、n是母串和字串的长度)
KMP
就是通过一些神奇的操作跳过一些字符,以达到优化时间复杂度的效果。
具体的做法理解了其实并不复杂,一共就是3步。
1.找出子串内最长相同前后缀
举个例子:子串是AABAABAA
那么A和AA都是相同前后缀,但由于AA比较长,所以我们就选择它
很简单吧。。。
2.在后缀的最后设置一个指针,指向其最长相同前缀的最后
后缀的最后其实就是字符串的最后啦。。。
这个指针就是所谓的next
数组
我们依次对子串的每一位进行这两步操作,第i位的指针就是next[i]
3.看代码。。。
int KMP(int len1,int len2){ //母串的长度为len1,子串的长度为len2
//s1时母串,s2是子串
k=0; //字串指针
for(int i=0;i<len1;i++){ //和朴素算法一样,依次匹配母串的每一位
while(k>0&&s1[i]!=s2[k]) k=next[k];
if(s1[i]==s2[k]) k++; //如果匹配成功,匹配下一位
if(k==len2) cout<<i-len2+2<<endl; //如果全部匹配成功,输出答案
}
}
重点解释一下没加注释的那行代码,在朴素算法中如果匹配失败我们要把字串的指针归零(下面有朴素算法可以自行比较一下),但在KMP算法中我们直接把指针返回到next[i]记录的点,也就是我上面说的"相同前缀的最后“,因为既然这一段后缀和前缀是相同的,那么也就没有匹配再次匹配这一段的必要了,可以直接从相同部分的后一位开始匹配,这样就能达到节省时间的目的。
再举个例子:
A | B | C | B | C | B | C | A | A |
---|---|---|---|---|---|---|---|---|
i |
||||||||
B | C | B | C | A | ||||
next[k] |
k |
这里母串是ABCBCBCAA,子串是BCBCA
匹配到第6位时k>0&&s1[i]!=s2[k]
朴素算法这时就会i++,k=1
重新开始匹配
但是我们已经匹配了4位,就差一位了,这么好的成功机会,怎么能放过呢?
所以我们让k=next[k]=3
,这样做是对的,因为字串的1、2位和3、4位是完全一样的,所以可以直接把第3位移到第5位的位置匹配
朴素:
A | B | C | B | C | B | C | A | A |
---|---|---|---|---|---|---|---|---|
i |
||||||||
B | ||||||||
k |
|
KMP:
A | B | C | B | C | B | C | A | A |
---|---|---|---|---|---|---|---|---|
i |
||||||||
B | C | B | ||||||
k |
朴素算法:
bool solved;
int solve(int len1,int len2){
solved=0;
for(int i=0;i<len1;i++){
for(int k=1;k<=len2;k++){
if(s1[i+k-1]!=s2[k]){
break;
}
if(k==len2) solved++;
}
if(solved) return i;
}
}
其实我也不知道这段写的对不对…
那么,就只剩下最后一个问题了
怎么求next数组?
好问题。
可惜我也不会
每当发现失配时,就要往前跳,如果实在找不到,就跳到0,表示没有一个字符能够匹配。这时有字符匹配就将next设为当前位置+1,没有就是0
其实光看代码挺简单的,所以背下来就行了
void find_next(int len2){
k=0;
for(int i=1;i<len2;i++){
while(k>0&&s2[i]!=s2[k]){
k=next[k];
}
if(s2[i]==s2[k]){
next[i+1]=++k;
}
else{
next[i+1]=0;
}
}
}
首先解释一下前面可能没有说清楚,next
表示的是字符串后缀的最后,也就是目前的长度i
+1的指针,而不是i
,同样它指向的点也是++k
而不是k
,因为只有相同前后缀的后面那个点才是我们真正想要的那个点。
这是字符串ABAABCABAAC的next值
s2 | A | B | A | A | B | C | A | B | A | A | C |
---|---|---|---|---|---|---|---|---|---|---|---|
next | 0 | 0 | 1 | 1 | 1 | 2 | 0 | 1 | 2 | 3 | 4 |
注意: 这里next表示的位置比实际位置大1,也就是说next[1]表示的数是0的next值,next[len2]表示的数是len2-1的next的值,因为输入时是0~len2-1
,输出时却是1~len2
呼,终于写完了
刚学完KMP时,我有一个问题:如果子串的所有字符都不相同,那么字符串每一项的next值就都是0了,那么此时用KMP不就和朴素还有什么区别?
答案是:没有区别
是的,如果所有字符都不同,KMP确实一点用都没有QwQ
所以,对于KMP,重复度越高的字符串,越能节省时间。如果重复度基本为0,还是乖乖打暴力吧!
#include
using namespace std;
const int maxn=1e6+10;
char s1[maxn],s2[maxn];
int next[maxn];
int k=0;
void find_next(int len2){
k=0;
for(int i=1;i<len2;i++){
while(k>0&&s2[i]!=s2[k]){
k=next[k];
}
if(s2[i]==s2[k]){
next[i+1]=++k;
}
else{
next[i+1]=0;
}
}
}
int KMP(int len1,int len2){
k=0;
for(int i=0;i<len1;i++){
while(k>0&&s1[i]!=s2[k]) k=next[k];
if(s1[i]==s2[k]) k++;
if(k==len2) cout<<i-len2+2<<endl;
}
}
int main(){
cin>>s1>>s2;
int len1=strlen(s1),len2=strlen(s2);
find_next(len2);
KMP(len1,len2);
for(int i=1;i<=len2;i++){
cout<<next[i]<<" ";
}
return 0;
}
2019-8-11