kmp总结及其应用
kmp含义
克努斯-莫里斯-普拉特算法,一种字符串查找算法。
字符串算法主要是用于主串 S( s1,s2,s3,...,sn ), 模式串T( t1,t2,...,tm ), 之间的匹配问题.
相对与模式匹配O(n^2)而言: 当 Si != Tj 失配时, 主串下标i不回溯, 而是将模式串下标j回溯到合适的地方,再继续比较 Tj ,Si.
时间复杂度极端情况是 O(N*M), 但是一般情况下总能保证O(N+M).
假定串 S( i-j+1, i ) 与 模式串 T( 1, j ) 匹配时, Si != Tj 不匹配,此时需j最短回溯到 k,
则存在 T(1,k-1) = T( j-k+1, j-1 ), 此时 k = next[j], 再令 Si 与 Tk 比较.
则我们得出 next[] 的定义:
next[i] = 0, 当 i = 0
next[i] = Max{ k | 1 < k < j, 且 T(1,k-1) = T(j-k+1,j-1),当此集合不空时 }
next[i] = 1, 其它情况.
1 int kmp( char *S, char *T ){ // 主串S,模式串T, 下标皆从1开始. 2 int la = strlen(S), lb = strlen(T); 3 int i = 1, j = 1; 4 while( i <= la && j <= lb ){ 5 if( j == 0 || S[i] == T[j] ) i++, j++; 6 else j = next[j]; //模式串向前滑动到 nxt[j]位置,继续比较 7 } 8 if( j > lb ) return i-j; //匹配成功,返回最初匹配点 9 return -1; //匹配失败 10 }
next数组
next函数,表示对于模式串而言,其最长的前缀与后缀相同的长度.
有定义知道 next[1] = 0;
设 next[j] = k, 这表明在模式串中存在下列关系
T( 1, k-1 ) = T( j-k+1, j-1 )
此时 next[ j+1 ]的取值有两种情况:
1. 当 T[k] == T[j] 时, 此时有 T( 1,k ) = T( j-k+1, j ), 则此时 next[ j+1 ] = next[j] + 1
2. 但 T[k] == T[j] 时, 此时可把求 next函数值的问题看作是一个模式匹配的问题.整个模式串既是主串又是模式串.
按照前面主串与模式串匹配的思路, 则当 T[k] != T[j] 时, 应将模式串下标 k滑动到 next[k]时, 再与 T[j] 比较,
最终可能出现两种情况:
1. 匹配到, 此时 next[ j+1 ] = next[ k` ] + 1;
2. 一直无法匹配则最后会得到, next[ j+1 ] = 1.
1 void GetNext( char *T, int *nxt ){ 2 int len = strlen(T); 3 int i = 0, j = 1; 4 nxt[1] = 0; 5 while( j <= len ){ 6 if( i == 0 || T[i] == T[j] ) 7 nxt[ ++j ] = ++i; 8 else i = nxt[i]; 9 } 10 }
应用模型
1. 模式串是否在主串中出现.
枚举其中一个串的主串,然后与其他串进行KMP匹配即可. 此题细节处理使用了STL.string.substr( 起点l, 数量num ).
#include<cstdio> #include<cstdlib> #include<cstring> #include<string> #include<algorithm> using namespace std; char str[15][100]; int n, next[100]; string res; bool flag; void GetNxt( string T, int *nxt, int len ){ int i = 0, j = 1; while( j <= len ){ if( i == 0 || T[i-1] == T[j-1] ) nxt[++j] = ++i; else i = nxt[i]; } } bool kmp(char *S, string T){ int la = strlen(S), lb = T.size(); int i = 1, j = 1; GetNxt( T, next, lb ); while( i <= la && j <= lb ){ if( j == 0 || S[i-1] == T[j-1] ) i++, j++; else j = next[j]; if( j > lb ) return true; } return false; } void solve(){ flag = false; string st = str[0], tmp; for(int L = 60; L >= 3; L--){ for(int i = 0; i+L <= 60; i++){ tmp = st.substr(i,L); bool a = true; for(int k = 1; k < n && a; k++) if( kmp( str[k], tmp ) == false ) a = false; if( a == true ){ if( flag == false ) flag = true, res = tmp; if( res > tmp ) res = tmp; } } if( flag ) return; } } int main(){ int T; scanf("%d", &T); while( T-- ){ scanf("%d", &n ); for(int i = 0; i < n; i++) scanf("%s", str[i] ); solve(); if( flag == false ) puts("no significant commonalities"); else printf("%s\n", res.c_str() ); } return 0; }
同上题差不多.但是这题 N达到了4000,串长度为200, 暴力肯定不行,二分枚举长度,然后进行匹配.
#include<cstdio> #include<cstring> #include<cstdlib> #include<string> #include<algorithm> using namespace std; const int N = 4010; char str[N][210]; int n, next[N], Len[N]; string res, st; bool flag; void GetNxt(string T,int *nxt, int len){ int i = 0, j = 1; nxt[1] = 0; while( j <= len ){ if( i == 0 || T[i-1]==T[j-1] ) nxt[++j] = ++i; else i = nxt[i]; } } bool kmp( char *S, string T, int la, int lb ){ int i = 1, j = 1; GetNxt(T,next,lb); while( i <= la && j <= lb ){ if( j == 0 || S[i-1] == T[j-1] ) i++, j++; else j = next[j]; if( j > lb ) return true; } return false; } bool find( int L ){ string st = str[0],tmp; for(int i = 0; i+L <= Len[0]; i++){ tmp = st.substr( i, L ); bool f = true; for(int k = 1; k < n && f; k++) if( kmp( str[k], tmp, Len[k], L ) == false ) f = false; if( f ) return true; } return false; } void solve(){ flag = false; int l = 0, r = Len[0], maxlen = -1; while( l < r ){ int m = (r+l)>>1; if( find(m) ) maxlen = m, l = m+1; else r = m; } if( maxlen != -1 ){ string tmp, st = str[0]; l = maxlen; for(int i = 0; i+l <= Len[0]; i++){ tmp = st.substr( i, l ); bool f = true; for(int k = 1; k < n && f; k++) if( !kmp( str[k], tmp, Len[k], l) ) f = false; if( f ){ if(flag ==false) flag=true, res = tmp; if( res > tmp ) res = tmp; } } } } int main(){ while( scanf("%d", &n), n ){ for(int i = 0; i < n; i++){ scanf("%s", str[i] ); Len[i] = strlen(str[i]); } solve(); if( flag ) printf("%s\n", res.c_str() ); else puts("IDENTITY LOST"); } return 0; }
本质还是一样求模式串在主串中是否出现. 拿一个串从大到小暴力分解子串. 与其他原串与inverse串匹配.
#include<cstdio> #include<cstring> #include<cstdlib> #include<string> #include<algorithm> using namespace std; const int N = 110; char str[120][N]; string bap[120]; int n, m, minlen; int Len[120], next[120]; void GetNxt(const char *T, int len){ int i = 1, j = 0; next[1] = 0; while( i <= len ){ if( j == 0 || T[i-1]==T[j-1] ) next[++i] = ++j; else j = next[j]; } } bool kmp(const char *S,int la,const char *T,int lb){ int i = 1, j = 1; GetNxt(T,lb); while( i<=la && j<=lb ){ if( j == 0 || S[i-1] == T[j-1] ) i++,j++; else j = next[j]; if( j > lb ) return true; } return false; } int solve(){ string st = str[0]; for(int L = minlen; L >= 1; L-- ){ for(int i = 0; i+L <= Len[0]; i++){ bool find = true; string tmp = st.substr( i, L ); for(int j = 1; j < n && find; j++){ if( !kmp(str[j],Len[j],tmp.c_str(),L) && !kmp(bap[j].c_str(),Len[j],tmp.c_str(),L) ) find = false; } if(find) return L; } } return 0; } int main(){ int T; scanf("%d", &T); while( T-- ){ scanf("%d", &n); scanf("%s", str[0] ); minlen = (Len[0]=strlen(str[0])); for(int i = 1; i < n; i++){ scanf("%s", str[i] ); bap[i] = str[i]; Len[i] = strlen(str[i]); minlen = min( minlen, Len[i] ); reverse( bap[i].begin(), bap[i].end() ); } int d = solve(); printf("%d\n", d ); } return 0; }
这一题还是暴力过去的.不过据说有 dp(i,j)的状态压缩, 字符逆序处理,然后KMP.string.substr挺管用..
#include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<string> using namespace std; const int N = (int)1e6+1100; char str[N]; int n, m; int next[N]; void GetNxt(string T,int *nxt, int len){ int i = 1, j = 0; nxt[1] = 0; while( i <= len ){ if( j == 0 || T[i-1]==T[j-1] ) nxt[++i] = ++j; else j = nxt[j]; } } int kmp(string S, int la, string T, int lb){ int i = 1, j = 1; GetNxt(T,next,lb); while( i <= la && j <= lb ){ if( j == 0 || S[i-1] == T[j-1] ) i++, j++; else j = next[j]; if( j > lb ) return i-j; } return -1; } int main(){ while( scanf("%d%d", &n,&m) != EOF){ scanf("%s", str); int start = n; for(int i = 0; i < m; i++){ string s = str; reverse( s.begin(), s.end() ); bool find = false; for(int L = min(13,n); L >= 1 && !find; L-- ){ int la = n-1, lb = L; string s1 = s.substr(1,la), t1 = s.substr(0,lb); // printf("s1 = %s, t1 = %s\n", s1.c_str(), t1.c_str() ); int d = kmp( s1, la, t1, lb ); if( d != -1 ) find = true, str[n++] = s[d]; } if( find == false ) str[n++] = '0'; str[n] = '\0'; //printf("str = %s\n", str); } for(int i = start; i < n; i++) printf("%c",str[i]); } return 0; }
2. 模式串在主串中的出现次数.
因为next函数值意义为最长的前缀与后缀相同长度. 当模式串Tj与主串Si 在 (i,j)匹配完成,此时下一个可能出现的匹配的起始位置为 (i+1,lenS) , 若我们使主串下标i回溯时,则会使时间复杂度达到O(N*M), 因为是要找与模式串相同的. 则我们只需要令j = next[j], 此时 T( 1, nxt[j]-1 ) = S( i-nxt[j]+1, i-1 ) , 表示其最长的前缀和后缀,此时i就无需回溯,然后继续匹配.统计次数即可.
核心点是主串下标不回溯, 并利用 next函数意义(最长的相同前缀和后缀)
#include<cstdio> #include<cstdlib> #include<cstring> const int N = (int)1e6+10; char s1[N], s2[10010]; int next[10010]; void GetNxt( char *T, int *nxt, int len ){ int i = 0, j = 1; nxt[1] = 0; while( j <= len ){ if( i == 0 || T[i-1] == T[j-1] ) nxt[++j] = ++i; else i = nxt[i]; } } int kmp( char *S, char *T ){ int la = strlen(S), lb = strlen(T), cnt = 0; GetNxt( T, next, lb ); int i = 1, j = 1; while( i <= la && j <= lb ){ if( j == 0 || S[i-1] == T[j-1] ) i++, j++; else j = next[j]; if( j > lb ) cnt++, j = next[j]; } return cnt; } int main(){ int T; scanf("%d", &T); while( T-- ){ scanf("%s", s2); scanf("%s", s1); printf("%d\n", kmp( s1, s2 ) ); } return 0; }
poj 3167 Cow Patterns 有点难度.
这题是求一模式串 与 主串的相对大小匹配,所有位置.
如果给我们的是绝对大小,那么我们就能用 poj 3461的解法,每次匹配到了再令j = next[j] 即可,得出所有匹配位置.
而对于相对大小,我们需要使用到一个结论:
两个偏序序列, 对于其每一位, 其前面比起小的数量,和与其相等的数量, 都相等, 则两个偏序序列相同. (小,和等于都一样,则大于也一样- -..)
利用这个结论,我们就可以判定快速判定两个偏序序列是否相同. 从宏观的角度上看, 还是一样对 模式串求个next函数,然后再对 模式串与主串kmp匹配.
这里比较特殊的地方, 就在于, 两个值的比较, 根据定义, (1,k) = (i-k+1,i) 时, next[ i+1 ] = k+1 . 模式串中的总是用的前缀,而主串中一直用的后缀.
那么我们就可以预处理出 模式串的 m1(小于数量), m2(等于数量), 对于主串则使用 树状数组来维护, 当失配时,则为维护树状数组.具体如下.
若当前模式串 T(1,j) 与主串S( i-j+1, i ) 比较时, Tj != Si, 此时失配, 需要令 j = next[j] 再进行匹配. 模式串我们预处理了前缀.可以O(1)得出.无需处理.
而,对于主串而言, 前面的树状数组中存放的元素是, ( i-j+1, i ), 当令 j = next[j], 再与 Si比较时, 此时 树状数组中 应该存放序列 S( i-next[j]+1, i ) , 那么我们就
需要手动的删除掉 S( i+j-1, i-next[j] ) 这一段. 对于模式串自身求next函数,操作一样.
//poj 3167 kmp + binary index tree //yefeng1627 #include<cstdio> #include<cstring> #include<cstdlib> const int N = (int)1e5+10; const int K = (int)3e4+10; int a[N], b[K], c[30]; int nxt[K], m1[K], m2[K]; int n, k, S; int cnt, res[N]; void add(int x,int v){ while(x<30) c[x]+=v, x+=(x&(-x)); } int sum(int x){ int res = 0; while(x>=1) res += c[x],x-=(x&(-x)); return res; } void GetNxt(){ memset( c, 0, sizeof(c)); int i = 1, j = 0; nxt[1] = 0; while( i <= k ){ // printf("i:%d,j:%d b-1=%d, b=%d\n", i,j, sum(b[i]-1),sum(b[i]) ); if( j == 0 || (sum(b[i]-1)==m1[j]&&sum(b[i])==m2[j]) ) { nxt[++i] = ++j; if(i<=k) add(b[i],1); } else{ for(int x = i-j+1; x <= i-nxt[j]; x++) add(b[x],-1); j = nxt[j]; } } //printf("k = %d, i = %d\n", k, i ); //for(i = 1; i <= k+1; i++) // printf("%d ", nxt[i] ); puts(""); } void kmp(){ cnt = 0; GetNxt(); int i = 1, j = 1; memset(c,0,sizeof(c)); add(a[1],1); while( i<=n && j<=k ){ if( j == 0 || (sum(a[i]-1)==m1[j]&&sum(a[i])==m2[j]) ){ ++i,++j; if(i<=n) add(a[i],1); } else{ for(int x = i-j+1; x <= i-nxt[j]; x++) add(a[x],-1); j = nxt[j]; } if( j > k ){ // printf("i = %d, k = %d\n", i, k); res[cnt++] = i-k; for(int x = i-j+1; x <= i-nxt[j]; x++) add(a[x],-1); j = nxt[j]; } } } int main(){ while( scanf("%d%d%d", &n,&k,&S) != EOF){ for(int i = 1; i <= n; i++) scanf("%d", &a[i] ); memset(c,0,sizeof(c)); for(int i = 1; i <= k; i++){ scanf("%d", &b[i] ); add( b[i], 1 ); m1[i] = sum(b[i]-1),m2[i] = sum(b[i]); // printf("i:%d, m1 = %d, m2 = %d\n", i, m1[i], m2[i] ); } kmp(); printf("%d\n", cnt ); for(int i = 0; i < cnt; i++) printf("%d\n", res[i] ); } return 0; }
3. 求循环节长度 / 最小覆盖子串长度 图形介绍 http://blog.csdn.net/fjsd155/article/details/6866991
kmp的nxt函数过程,会将模式串一个周期一个周期的构造, 对于 (i+1) - nxt[ i+1 ] (因为我们是通过 Ti与Tj 得到nxt[i+1]的),
即是其周期长度, 当 目前总长度 i % { (i+1)-nxt[i+1] } = 0, 时, 则意味着最后一个周期构造完成, 否则 i % { (i+1)-nxt[i+1] }表示目前最后一个周期串已构造出了多少个.
poj 2185 Milking Grid 有点难度,且题意不是很好懂.
这题所指的最小覆盖长度,其实就是最小循环周期长度.当然并非是完成循环,换句话说是 单元串a,重复k次可以覆盖str, 其中streln(a*k) >= strlen(str),
并且我们知道 N-next(N)是最小覆盖长度, 之后的 j = next( next(N) )逐渐增大, 解决此题的思路是:
首先处理宽度width, 寻找所有行都有的最小覆盖宽度 w`, 极端情况是 c. 因为每个串都能覆盖本身.
之后在将 r长度为c的串(1,c). 截断成 r个长度为width的串(1,width), 然后对这c个串进行一个HASH值.得到一个数组key[C].
然后对这个数组求一个next函数, 高度 high 即为 C - next(C),
#include<cstdio> #include<cstring> #include<cstdlib> const int N = (int)1e6+10; char s[N]; int nxt[N]; int main(){ while( scanf("%s", s) != EOF ){ if( s[0] == '.' ) break; int len = strlen(s); int i = 0, j = 1; nxt[1] = 0; while( j <= len ){ if( i == 0 || s[i-1] == s[j-1] ) nxt[++j] = ++i; else i = nxt[i]; } if( len%(len+1-nxt[len+1]) ) puts("1"); else printf("%d\n", len/(len+1-nxt[len+1]) ); } return 0; }
对于模式串本身求next函数值时,其实其是一个一个周期在构造串, N-next(N)表示串的循环周期, 而N%(N-next(N))即为最后一个周期已构造串的数量.
#include<cstdio> #include<cstring> #include<cstdlib> #include<string.h> #include<algorithm> using namespace std; const int N = (int)1e6+10; int n, nxt[N]; char s[N]; int main(){ int Case = 1; while( scanf("%d",&n), n ){ scanf("%s", s); int len = strlen(s); int i = 1, j = 0; nxt[1] = 0; while( i <= len ){ if( j == 0 || s[i-1] == s[j-1] ) nxt[++i] = ++j; else j = nxt[j]; } printf("Test case #%d\n", Case++); for(int i = 2; i <= len; i++){ if( (i%(i+1-nxt[i+1])==0) && (nxt[i+1]>1) ) printf("%d %d\n", i, i/(i+1-nxt[i+1]) ); } puts(""); } return 0; }
4. 求串的 前缀最大长度, 且其前缀与后缀相同. (最大前缀与后缀)
对于串T(1,i), 我们考虑 next函数定义
next[ i ] = Max{ k | 1 < k < i && T( 1,k-1 ) = T( i-k+1, i-1 ) 且集合不为空, } , 则可以知道,
字串(1,next[ i-1 ]) 即为串 T(1,i-1) 的最大前缀与后缀. 此时 再考虑串 [1,next[i-1] ]的最大前缀与后缀,
如此反复,直到 i = 0 结束. 因为定义 k < i, 其实其本身 (1,i)也是其最大前缀和后缀. 逆序输出即可.
再重复说明下, kmp的next函数值是通过比较 Ti 与 Tj , 若 Ti = Tj ,则 next[ j+1 ] = i+1, 所以,
我们要获取i位置的最后匹配位置,则需要用next[ i+1 ], 因为其包含了 Ti = T[ next[i+1] - 1 ].
#include<cstdio> #include<cstring> #include<cstdlib> const int N = 400010; char s[N]; int res[N], nxt[N]; int main(){ while( scanf("%s", s) != EOF){ int len = strlen(s); int i = 0, j = 1; nxt[1] = 0; while( j <= len ){ if( i == 0 || s[i-1] == s[j-1] ) nxt[++j] = ++i; else i = nxt[i]; } int cnt = 0, x = nxt[len+1]; res[cnt++] = len; while( x > 1 ) { res[cnt++] = x-1; x = nxt[x]; } for(int i = cnt-1; i >= 0; i--) printf( i == 0 ? "%d" : "%d ", res[i] ); puts(""); } return 0; }