K M P 超简单的理解
U1S1,学了那么久的KMP至今不理解,
每自往思,三顾谷歌百度之中,咨询以当前之急,似懂非懂,遂许粘贴以瞎改。
后值反思,亡羊补牢。
然后凭着以前学过用过的基础,深度复习一遍,想不到竟如此简单
也就几个步骤就可以理解了,
假设:
母串为A,使用遍历变量i,子串为B,使用遍历变量j
1. 以暴力匹配为基础
2. 清楚暴力匹配在每次失配的时候,都是i+1,j=0从头开始匹配的操作。
3. 然后优化掉j=0的操作,延伸出了(子串的)最长前后缀和next数组(带有证明)
4.再优化掉i+1的操作,延伸出了一系列证明发现——每次匹配到例如A[i+k]==B[j+k],然后A[i+k+1]!=B[j+k+1]也就是刚好下一位失配的时候,保证此时的母串匹配到的位置i+k+1不变
(也就是,在暴力匹配思维中被优化掉的i=i+1,等效于变成i=i+k+1)
结束!!!
-------------------------------------分割线--------------------------------------------------下面来分步解释3,4步骤。
3.1最长前后缀

有证明:B[i-k~i-1]==B[j-k~j-1]
也就是,B串的第i个的 和 B串第j的 前k个字母相同
在这个“ABCDABD”串中,i,j,k分别表示的值就是3,6,2
肉眼看出来思考一下可以怎么利用呢?
也就是当子串B匹配母串A已经匹配到B串的 j 这个位置的时候,发现不能匹配,
只需要移动到B i 这个位置去与母串重新匹配即可!!!
(这里就替换掉了之前所说的j=0的操作)

当发现母串的C与子串D不匹配的时候,直接将子串B按上述操作去重新匹配,得到一个步所示的匹配位置。
这里也就涉及到实际匹配过程中的证明:A[i-k~i-1]==B[j-k~j-1]
也就是,A串的第i个的 和 B串第j的 前k个字母相同
咱们这就没必要去多搞证明了,觉得自己脑子还行的,可以看最后我推荐的博客里的证明。
-----------------小分割线-----------------
3.2 next数组
如上,咱们已经发现了,子串B的一些东西有助于优化,那么这个东西就是由 最长前后缀 推出来的 next数组
细看公式:A[i-k~i-1]==B[j-k~j-1] ,这个“-1”是否有些碍事?
那么最长前后缀得到的一串数字:

处理成,整体向右移一位,首项设为-1,则就变成了如下:

首先说明next数组的定义——next[i]的值 表示,在 i 这个位置的最长前后缀的长度值
那么,这样一来,就恰好可以满足:上述所说的 替换掉 暴力思维里的 j=0 的操作 的操作 就是 j=next[j]
这里要深度思考理解一下,就按照上面那个失配图的那里,代码逻辑思考一下j的变换过程
(实际等效于子串向右移了j-next[j]位,这个右移不需要记忆,理解就行)
(还是解释一下吧,那里的 j=5 , next[5]=1,也就是实际右移了5-1位,4位)
如此理解了之后,再来说如何求得next数组的问题
这里暂时就不解释了,想知道的也可以跳转一下到我推荐的博客里去看,这里就贴上我写的标码吧
void Getnext(char s[]) { n[0]=0; for(int i=1,k=0;s[i];++i) { while(k&&s[i]!=s[k])k=n[k-1]; n[i+1]=s[i]==s[k]?++k:0; } }
还不理解的,可以按上述样例输出一下得到的n数组
那么,步骤3就过了
--------------------------------------------------------------分割线-------------------------------------------------------------------
4.优化i+1的操作,这一步,,唔,,,简单思考一下糊弄自己过去就行,
强行理解运用了next数组子串去匹配的匹配,i变为i+1是多余的就行,i只管++,其他都是子串的匹配变量变化
当然,真想深度理解的,也可以跳去看我最后推荐的博客吧。
那么最后就贴一下这替换i+1的实际匹配代码吧,因为本身代码不固定的原因,就找一个例题来作为代码了
http://120.78.128.11/Problem.jsp?pid=2149(福工院fjut的2149题)
for(i=0,k=0;a[i];++i)
{ while(k&&b[k]!=a[i])k=n[k-1]; if(b[k]==a[i])++k; if(k==lb) { printf("%d\n",i-lb+1); break; } } if(i==la)puts("-1");
完整代码:
#include<cstdio> #include<cstring> #define N 1000009 char a[N],b[N]; int n[N]; /* 等效移动k-next[k] p[i]!=p[j] p[i-k`i-1]=p[j-k`j-1] 也即,next数组表示失配时下一个可匹配的位置, */ void Gn(char s[]) { n[0]=0; for(int i=1,k=0;s[i];++i) { while(k&&s[i]!=s[k])k=n[k-1]; n[i+1]=s[i]==s[k]?++k:0; } } int main() { while(~scanf("%s %s",a,b)) { Gn(b); int la=strlen(a),lb=strlen(b),i,k; for(i=0,k=0;a[i];++i) { while(k&&b[k]!=a[i])k=n[k-1]; if(b[k]==a[i])++k; if(k==lb) { printf("%d\n",i-lb+1); break; } } if(i==la)puts("-1"); } return 0; }
更新代码,推荐使用这个封装好了的code:
class Solution { vector<int> next; int x, y; void getnext(string a) { next.emplace_back(0); for (int i = 1, k = 0; i < y; ++i) { while (k && a[i] != a[k]) k = next[k - 1]; next.emplace_back(a[i] == a[k] ? ++k : 0); } } public: int strStr(string haystack, string needle) { x = haystack.length(), y = needle.length(); if (y == 0) return 0; next.clear(); getnext(needle); for (int i = 0, k = 0; i < x; i++) { while (k && haystack[i] != needle[k]) k = next[k - 1]; if (haystack[i] == needle[k]) ++k; if (k == y) return i - y + 1; } return -1; } };
KMP,结束!!!
推荐博客:https://blog.csdn.net/v_july_v/article/details/7041827(图也是这里来的)
总结一下KMP的用法(也体现next数组相应的性质or作用):
1. 基本的string.find
2. 字符串中的前缀次数和:http://120.78.128.11/Problem.jsp?pid=1303
(解法是,ans=遍历 next数组 非零k=next[k]递归次数 +字符串长度)
3. 字符串中的相同前后缀:http://120.78.128.11/Problem.jsp?pid=2150
(解法是:从末尾失配跳转一遍next数组即可)
4. 字符串中的循环周期和周期次数:http://120.78.128.11/Problem.jsp?pid=1304
解法是:遍历一遍next数组,找到i%(i-next[i])==0的位置,
这个位置即满足循环周期,长度为i,循环次数为i/(i-next[i])
在next数组中有个性质是,next数组的值,有i-next[i]表示失配时,子串右移的位数
那么,当位移位数和长度成倍数关系时候,这个子串就是循环串了
5. 两串最长前后缀匹配:http://120.78.128.11/Problem.jsp?pid=1305
(深度理解next数组,不多解释就贴一份带分析的ac代码吧)
#include<iostream> #include<algorithm> using namespace std; #define N 202100 void cs(string x){cout<<x<<endl;} string a,b; int n[N*2]; int Gn(string s) { n[0]=-1; int k=0,l=min(a.length(),b.length()); for(int i=1;s[i];++i) { while(k&&s[i]!=s[k])k=n[k]; n[i+1]=s[i]==s[k]?++k:0; } return min(l,k); } /* 分析: 考虑最短连接的字母串 有a+b和b+a两种情况, 也就是比较 a串末尾与b串开始的匹配长度 与 b串末尾与a串开始的匹配长度 那么如何找这个匹配呢? next数组前身是最长前后缀,那么也就是说 a+b串后去找的字母串的next数组应该是b+a的最长匹配长度 b+a串后去找的字母串的next数组应该是a+b的最长匹配长度 有两点需要注意: 因为要求是首尾连接,所以取值必须是next数组最后一位值 因为next数组求值本身是两字符串的匹配,所以next数组取值可能超过a,b串的本身长度,所以要取最小值 */ int main() { while(cin>>a>>b) { int la=Gn(a+b),lb=Gn(b+a); if(lb>la)cs(a+string(b,lb)); else if(la>lb)cs(b+string(a,la)); else { string A=a+string(b,lb); string B=b+string(a,la); cs(A>B?B:A); } } return 0; }
6. 字符串中的所有循环节:https://vjudge.net/problem/FZU-1901
(分析见代码)
#include<iostream> #include<cstring> #include<cstdlib> #include<vector> #include<queue> #include<cmath> #include<map> #include<algorithm> using namespace std; #define N 2021 #define mt(x) memset(x,0,sizeof x) typedef long long ll; void cn(ll x){cout<<x<<endl;} void cs(string x){cout<<x<<endl;} int n[N],ans[N]; string a; void Gn(string s) { n[0]=-1; for(int i=1,k=0;s[i];++i) { while(k&&s[i]!=s[k])k=n[k]; n[i+1]=s[i]==s[k]?++k:0; } } /* 4 ooo acmacmacmacmacma fzufzufzuf stostootssto */ void solve() { int t; cin>>t; for(int i=1;i<=t;++i) { string s; cin>>s; Gn(s); /* 题意:找出所有p,使得s[0~i]==s[0~i+p] 分析:找循环子串, next[i]=p表示字符串 s[0~p-1]==s[i~i+p-1] ,即匹配的长度p next从末尾值跳转k=n[k],k即为所有循环节的初始下标 那么每次 累加 位移长度i-n[i]即可 另外也有做法,遍历next,输出:字符串总长l - 每一次跳转值p 这个不太好理解, 我的理解是, i-next[i]是表示当前串在位置i失配时候的下一次匹配的右移 那么 l-next[i]是表示当前串 从头 开始的 右移 */ int l=s.length(),num=0; int p=l; while(p) { ans[num++]=p-n[p]+ans[num-1]; p=n[p]; //ans[num++]=l-p; } printf("Case #%d: ",i); printf("%d\n",num); for(int j=0;j<num;++j) printf("%d%c",ans[j],j==num-1?'\n':' '); } } int main() { ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); solve(); return 0; }

浙公网安备 33010602011771号