KMP 学习笔记
前言—— \(char\) 与 \(string\)
-
有的时候 \(char\) 数组确实比 \(string\) 好用,且字符串长度很大时 \(string\) 会被卡掉,所以不要犯懒,老实用 \(char\) ,\(string\) 可以用但是慎用。
-
同时很多情况下为了方便和减少出错,我们会想办法把字符串的坐标从 \(0\sim len-1\) 变成 \(1\sim len\) ,对于 \(char\) 和 \(string\) 都有办法,但不尽相同。
- \(char:\)
cin>>s+1; int len=strlen(s+1);
- \(string:\)
或cin>>s; s=" "+s; int len=s.size()-1;
cin>>s; int len=s.size(); s=" "+s;
- \(char:\)
定义与基本求法
-
定义:
用于匹配两字符串时的大幅度优化、\(border\) 问题、模式串在主串出现的次数以及位置等一系列问题,应用广泛,下面会依次解释。
-
\(|s|:\) 字符串 \(s\) 的长度。
\(sub(l,r):\) 区间 \((l,r)\) 子串的长度。
-
\(pre(s,i):\) \(s\) 长度为 \(r\) 的前缀。
\(suf(s,i):\) \(s\) 长度为 \(r\) 的后缀。
-
\(border\):(经常应用 \(border\) 的性质 )
若 \(0\leq r<|s|,pre(s,r)=suf(s,r)\) ,则称 \(pre(s,r)\) 为 \(border\) 。
\(eg:\) \(abababab\) 中 \(ab,abab,ababab\) 均为其 \(border\) 。其中前后缀追均为严格意义上,长度小于总串长度的前后缀。
-
\(next\) 数组:(重中之重)
-
又名前缀表, \(next[i]\) 表示 \(pre(s,i)\) 的最长 \(border\) 长度。(基本定义)
-
\(next[i]\) 表示两字符进行匹配,到该元素匹配失败时,重新匹配调到的位置,避免从 \(0\) 开始重新匹配。故此 \(next[i]\) 作为 \(i\) 的备选存在。
-
\(pre(s,next[i])\) 一定是 \(pre(s,i)\) 的 \(border\) ;由此,\(pre(s,next[n])\) 一定是 \(s\) 的 \(border\) ( \(n\) 表示 \(s\) 的长度 )。
以上均可以根据其基本定义和 \(border\) 的性质得出。
-
-
-
基本求法:
-
和自己匹配——求 \(next[i]\)
解决模式串匹配主串问题时,需要先处理出模式窜的 \(next\) 数组。
顾名思义,就是和自己匹配.
先定义一个 \(i,j\) ,先用 \(s_{j+1}\) 区匹配 \(s_i\) 。\(i\) 从 \(2\) 开始, \(j\) 从 \(0\) 开始。因为 \(next[1]\) 显然 \(=0\) 。
若当前匹配失败且 \(j\neq 0\) ,根据 \(next[j]\) 的基本定义,作为 \(j\) 的备选,另 \(j\) 不断跳 \(next[j]\) ,直到 \(s_i=s_{j+1}\) ,那么此时匹配成功,\(j++,next[i]=j\) 。如果一直跳到 \(j=0\) 还不能满足,便是匹配不上了,当前 \(next[i]=0\) 。
明确一个问题,在不断跳 \(j=next[j]\) 的过程中,跳到 \(s_{j+1}=s_i\) 时,此时得到的这个 \(pre(s,j)\) 必定是 \(pre(s,i-1)\) 的 \(border\) ,现在又满足 \(s_{j+1}=s[i]\) ,那么 \(pre(s,j+1)\) 就成了 \(pre(s,i)\) 的 \(border\) ,且一定是最长的 \(border\) ,即 \(next[i]\) 。
通过上述方式从前往后枚举 \(i\),枚举到 \(i+1\) 时, \(j\) 原先值保留,此时 \(j=next[i-1]\) ,从而方便继续向前跳和接下来的步骤,这里需详细理解一下上一段文字。
打个比方,如 \(aabaaf\) :
- \(a\) ,显然 \(next[1]=0\) 。
- \(aa\) ,\(s_{0+1}=s_2,j=1,next[2]=1\) 。
- \(aab\) ,\(s_{1+1}\neq s_3\),不断往前跳 \(j=next[j]\) ,始终不存在 \(s_{j+1}=s_3\) ,故 \(next[3]=0\) 。
- \(aaba\) ,现在经历过上一步的跳 \(next\) 使 \(j=0\) ,\(s_{0+1}=s_4\) ,故 \(j=1,next[4]=1\) 。
- \(aabaa\) ,\(s_{1+1}=s_5,j=2,next[5]=2\) 。
- \(aabaaf\) ,\(s_{2+1}\neq s_6\) ,不断向前跳 \(j=next[j]\) ,和第三次操作一样,始终不满足 \(s_{j+1}=s_6\) ,故 \(j=0,next[6]=0\) 。
也就得到了该串的 \(next\) 数组,即前缀表,同时表示 \(pre(s,i)\) 的最长 \(border\) 长度 :
-
代码如下:
void kmp() { int j=0,l=strlen(s+1); for(int i=2;i<=l;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[i]==s[j+1]) j++; nxt[i]=j; } }
-
和主串匹配
在此带入一道例题的情景,当然 \(kmp\) 的作用还有好多,下面的例题中还会有一定涉及。主串 \(s\) ,模式串 \(t\) 。
现已经将模式串的 \(next\) 处理出来,那么匹配主串就是轻而易举的了。
先来看一下暴力是怎么匹配的:
可以看的出,每次匹配失败后,就从头开始重新匹配。
但使用 \(kmp\) 遍不用这样。
依旧是上述的 \(i,j\) ,当匹配 \(s_i\) 和 \(t_{j+1}\) 时,如果匹配失败, 遍不断往前跳 \(next\) 直至可以匹配,思路和打法几乎和求 \(next\) 是完全一样的。
如上面的例子,采用 \(kpm\) 就可以:
而不必从头开始。
那么这道题要求出现的次数,那么每次 \(j\) 匹配到 \(m\) 时,也就表示模式串匹配完一遍了,记录答案 \(ans++\) ,另 \(j=nxt[m]\) 继续匹配即可。( \(m\) 表示模式串的长度 )。
-
代码如下:
int ask(string s,string t) { int j=0,n=s.size()-1,m=t.size()-1,ans=0; for(int i=1;i<=n;i++) { while(j&&t[j+1]!=s[i]) j=nxt[j]; if(s[i]==t[j+1]) j++; if(j==m) ans++,j=nxt[j]; } return ans; }
-
-
子串周期循环问题。
该问题下面的例题中会有详细描述,需要注重理解好 \(next\) 和 \(border\) 的含义。
-
-
关于复杂度
玄学玩意,虽然有个 \(while\) 但最多执行 \(n\) 次,最后还是 \(O(n)\)
看一下课件吧:
例题
\(OKR-Periods of Words\)
-
题面:
对于一个串 \(s\) ,存在一个子串(长度小于主串)周期,例如 \(ab,abab,ababab\) 均为 \(abababab\) 的周期,其中 \(ababab\) 为最长周期,而 \(abc\) 没有周期,则最长周期长度为 \(0\) 。给定一个字符串 \(s\) ,求其所有前缀的最大周期长度之和。
-
解法:
先来看一张图:
也就完美的解释了这道题,这样的话就不断跳 \(next[i]\) ,使得到 \(>0\) 的最小的一个 \(next\) 设其为 \(j\) ,\(ans+=j\) 即可,当然如果他的 \(next\) 最大就是 \(0\) 了,\(ans+=0\) 。
-
代码如下:
#include<bits/stdc++.h> #define int unsigned long long #define endl '\n' using namespace std; const int N=1e6+10,P=1e9+7; template<typename Tp> inline void read(Tp&x) { x=0;register bool z=1; register char c=getchar(); for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0; for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48); x=(z?x:~x+1); } int n,ans,nxt[N]; char s[N]; void kmp() { int j=0,l=strlen(s+1); for(int i=2;i<=l;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[i]==s[j+1]) j++; nxt[i]=j; } } signed main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); #endif read(n); cin>>(s+1); kmp(); for(int i=2;i<=n;i++) { int j=i; while(nxt[j]) j=nxt[j]; if(nxt[i]) nxt[i]=j; ans+=i-j; } cout<<ans; }
-
扩展:如果求最小周期呢?
根据上面的题不难相出,改成最大的 \(next\) 就可以了,其实就是直接的 \(next[i]\) 。然后 \(ans+=i-next[i]\) 即可,似乎更简单一点,但我们仍应该证明一下。
其实也就是这道题:Radio Transmission
-
代码如下:
#include<bits/stdc++.h> #define int long long #define endl '\n' using namespace std; const int N=1e6+10,P=1e9+7; template<typename Tp> inline void read(Tp&x) { x=0;register bool z=1; register char c=getchar(); for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0; for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48); x=(z?x:~x+1); } int n,nxt[N]; string s; void kmp(string s) { int j=0; for(int i=2;i<=n;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[i]==s[j+1]) j++; nxt[i]=j; } } signed main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); #endif read(n); cin>>s; s=" "+s; kmp(s); cout<<n-nxt[n]; }
-
动物园
-
题面:
给定一字符串 \(s\) ,求其每一个前缀的长度 \(<\dfrac{len}{2}\) 的 \(border\) 的个数。( \(len\) 指该前缀的长度 )
-
解法:
在此处换一种想法,不一定非要求自身的个数,对于一个 \(s_i\) ,我们求其后面可能出现的 \(s_j\) 的 \(num\) ,此处 \(s_j\) 可以通过跳 \(next\) 跳到 \(s_i\) 的位置,且 \(i\) 为其跳 \(next\) 过程中第一个 \(<\dfrac{j}{2}\) 的位置。
可能听起来不太好理解,就比方说,我现在是 \(s_i\) ,那么我的后面将有一个 \(s_j\) 需要我,那么我将要给 \(s_j\) 贡献多少的 \(num\) 。
不同于题面,重新定义 \(num_i\) 表示 \(s_i\) 将为 \(s_j\) 贡献的值,继续上面的情景,既然我是他跳 \(next\) 跳过来的,那么我一定能和他的后缀构成 \(border\) ,那么到我这里,他将继续向前跳一直到 \(0\) ,那么此时他往前继续跳的 \(next\) 也一定是我的 \(next\) ,既然到我这里已经 \(<\dfrac{j}{2}\) 了,那么我前面的一定也满足,我不妨将我前面 \(next\) 的数量算上我自己一起给他,这样他就不用费劲的向前跳了。(就不会 \(TLE\) 了)
看到这里好像发现了,就是对于每一个长度为 \(j\) 的前缀,他不断跳 \(next\) ,当他跳到 \(<\dfrac{j}{2}\) 时,再往前跳多少步跳到 \(0\) ,就是他的 \(ans\) 值,把这些 \(ans\) 加起来就是最后要求的值。
那么思考上面的情景,每一个 \(s_i\) 他的 \(num_i\) 就是他不断往前跳 \(next\) 跳多少次到 \(0\) 。又发现 \(num_i=num_{next[i]}+1\) ,于是可以线性求,在处理 \(next\) 数组时可以顺便求出来。
-
代码如下:
#include<bits/stdc++.h> #define int unsigned long long #define endl '\n' using namespace std; const int N=1e6+10,P=1e9+7; template<typename Tp> inline void read(Tp&x) { x=0;register bool z=1; register char c=getchar(); for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0; for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48); x=(z?x:~x+1); } int n,nxt[N],num[N]; char s[N]; void kmp() { int j=0,l=strlen(s+1); num[1]=1; for(int i=2;i<=l;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[j+1]==s[i]) j++; nxt[i]=j; num[i]=num[j]+1; } } int ask() { int j=0,l=strlen(s+1),ans=1; for(int i=2;i<=l;i++) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[j+1]==s[i]) j++; while(j>(i/2)) j=nxt[j]; ans=ans*(num[j]+1)%P; } return ans; } signed main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); #endif read(n); while(n--) { memset(nxt,0,sizeof(nxt)); cin>>s+1; kmp(); cout<<ask()<<endl; } }
剪花布条
-
题面:
和模式串与主串的匹配十分类似,不同的是每个匹配不可重叠:
\(eg:\) \(aaaa\) 直接匹配 \(aa\) 应是 \(3\) 个,但此处顾名思义 “剪”,所以只能剪出来 \(2\) 个。
-
解法:
与基本求法中的匹配十分相似,只需要在匹配完一遍后不让 \(j=next[j]\) ,而是让 \(j=0\) 即可。
-
代码如下:
#include<bits/stdc++.h> #define int long long #define endl '\n' using namespace std; const int N=1e6+10,P=1e9+7; template<typename Tp> inline void read(Tp&x) { x=0;register bool z=1; register char c=getchar(); for(;c<'0'||c>'9';c=getchar()) if(c=='-') z=0; for(;'0'<=c&&c<='9';c=getchar()) x=(x<<1)+(x<<3)+(c^48); x=(z?x:~x+1); } void wt(int x){if(x>9)wt(x/10);putchar((x%10)+'0');} void write(int x){if(x<0)putchar('-'),x=~x+1;wt(x);} string s,t; int n,m,nxt[N],ans,j; signed main() { #ifndef ONLINE_JUDGE freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); #endif while(1) { //memset(nxt,0,sizeof(nxt)); cin>>s; n=s.size(); if(s=="#"&&n==1) return 0; cin>>t; m=t.size(); s=" "+s,t=" "+t; j=0; for(int i=2;i<=m;i++) { while(j&&t[j+1]!=t[i]) j=nxt[j]; if(t[i]==t[j+1]) j++; nxt[i]=j; } j=0,ans=0; for(int i=1;i<=n;i++) { while(j&&t[j+1]!=s[i]) j=nxt[j]; if(t[j+1]==s[i]) j++; if(j==m) ans++,j=0; } write(ans); puts(""); } }
-
教训:
关于此题有一个深痛教训,对于 \(next\) 数组,即使多测,每一次也都会重新处理每个 \(next\) 的值,不必清空,而由于我多次 \(memset\) 导致常数过大多次超时。
所以:\(kmp\) 题目中,不必对 \(next\) 数组 \(memset\) 。
总结
当时课件讲 \(kmp\) 时,那个直播的学长讲的实在难平,根本不知道在说什么,所以利用其他网站和各种途径去学。写完 \(oj\) 上少有的几道 \(kmp\) 后,这里面甚至有好几道是用哈希水过的,所以感觉掌握实在不扎实,就去 \(loj\) 上刷了一些,感觉差不多真正理解了,于是决定写一篇博客加深一下理解,防止只会搞板子,要知道板子是怎么来的。在写博客的过程中也是思考了一段时间,才搞明白到底为什么这么写,比如动物园这道题,打完一直感觉有几点是错的不知为何能过,写完博客后终于是说服了自己。\(next\) 数组的处理过程值最不容易理解的,在打这一部分的时候也是费解了好久的,发现课件讲得实在不明白后去自己理解,上网上找动图。同时上面的图除了那个动图其他基本都是自己画的,比如周期那两道,用图来理解非常的好。\(kmp\) 的做法还有很多,不能局限于匹配,在处理 \(next\) 过程中。可以处理处很多别的东西,同时在查询过程中也是可以修改 \(next\) 的,用于减少时间复杂度,仔细看周期那题的代码可以发现。最重要的,熟练掌握 \(next\) 和 \(border\) 的各种含义与应用。