首页 > 代码库 > KMP专题总结
KMP专题总结
KMP算法有两种计算失配函数的方法
未优化过的失配函数 , f[i] 就表示 str[0~i-1] 的公共最长前后缀( 不包括str[0~i-1]本身,比如aaa的最长公共前后缀长度为2 ) , 这个特性必须牢记
从下面的例子应该也可以看出
0 1 2 3 4 5 6 7 8 9 10
str a a a a b b a a b a ‘\0’
f 0 0 1 2 3 0 0 1 2 0 1
优化过后的失配函数,虽然匹配的性能要好点 , 不过失去上述这么优美的特性 , 基本也就是废了( 应该不至于出道裸的匹配,然后卡失配函数吧 )
=========================================我是分割线=================================================
模板和原理我就不赘述了,网上资料很多,贴个模板完事
未优化的失配函数
void get_nex( char * str ){ int len = strlen( str ) ; f[0] = 0 , f[1] = 0 ; for( int i = 1 ; i < len ; i ++ ) { int j = f[i] ; while( j && str[i] != str[j] ) j = f[j] ; // 跟某个前缀匹配成功,那么下次从下个前缀可以匹配 if( str[i] == str[j] ) f[i+1] = j + 1 ; // 否则从头开始匹配 else f[i+1] = 0 ; } }
KMP 匹配函数
int KMP( char * str , char * sub_str ) { int len1 = strlen( str ) , len2 = strlen( sub_str ) ; // 当前跟子串的那个字符匹配 int j = 0 ; int ans = 0 ; for( int i = 0 ; i < len1 ; i ++ ) { // 如果成功下次匹配下一个字符 , 否则一直沿失配边走 while( j && str[i] != sub_str[j] ) j = f[j] ; if( str[i] == sub_str[j] ) j ++ ; if( j == len2 ) j = f[j] , ans ++ ; } return ans ; }
=====================================我是华丽丽的分割线==============================================
比较常见的问题 ( 应该要熟练掌握 ) :
1. KMP 求出 S[0...i-1] 的所有的公共前后缀的问题
这个东西在某些题目中会用于统计
很显然 , f[i] 的值就是 S[0...i-1] 的最长公共前后缀 ( 还是一样不包括串本身的 ),因为那个华丽丽的特性
然后怎么求稍微短一点的 ? 仔细一想,s[0...i-1] 次长( 存在的话 ) 的公共前后缀 ,就是 s[0...i-1] 和 s[i-f[i],i-1] 的最长公共前后缀。
那么又因为s[i-f[i],i-1] 本身就是原串是 s[0,f[i]-1] , 所以次长的公共前后缀就是 f[f[i]] 了
后面的就以此类推就行
2. KMP 求循环串
什么是 循环串 就是某个字符串可以由它的前缀重复几次得到这个串本身 。比如 aabaabaabaab ,可以有 aab , aabaab , aabaabaabaab 循环得出
假设我们称 aab , aabaab 这样的为循环节
那么怎么求最长的循环节呢 ? len - f[len] 就是循环节的长度 ,当然要同时满足 len % ( len - f[len] ) == 0
假设字符串的长度为 len ,len - f[len] = K , K * t = len
那么根据上述结论 , 整个字符串就应该是由 t 个 s[0...K-1] 循环组成的 ,如何得到这个呢 ?
S[0...K-1] , S[K,2*K-1] ...................... S[(t-1)*K,t*K-1 ]
S[0,K-1] , S[K,2*K-1] ..... S[(t-2)*K,(t-1)*K-1] , S[(t-1)*K,t*K-1]
因为我们知道 f[len] = len - K , 也就是说 字符串的前 ( t-1) * K 个字符串和 后( t - 1 ) * K 个字符串的相等的
那么我们就可以得到 S[0,K-1] == S[K,2*K-1] .... S[(t-1)*K,t*K-1] = S[(t-2)*K,(t-1)*K-1]
所以我们很容易得到 S[0,K-1] == S[K,2*K-1] = ... = S[(t-1)*K,t*K-1]
OK,我们已经得到答案了
至于再大一点的循环节,我们只要求小一点的最长公共前后缀就行了。
=====================================又是华丽丽的分割线==============================================
KMP题目 :
POJ 3461 Oulipo
POJ 1019 Number Sequence
两道模板题 ...
POJ 2406 Power Strings
求最短的循环节的循环次数
上面已经说过了,直接 len / (len - f[len]) 用这个公式就行
#include <stdio.h> #include <string.h> #include <algorithm> using namespace std; #define MAXN 1000005 char str[MAXN] ; int f[MAXN] ; void get_nex( char * str ) { int len = strlen( str ) ; int j; f[0] = f[1] = 0 ; for( int i = 1 ; i < len ; i ++ ) { j = f[i] ; while( j && str[i] != str[j] ) j = f[j] ; if( str[i] == str[j] ) f[i+1] = j + 1 ; else f[i+1] = 0 ; } } int main(){ while( scanf( "%s" , str ) != EOF ) { if( strcmp( "." , str ) == 0 ) break; get_nex( str ) ; int len = strlen( str ) ; if( len % ( len - f[len] ) == 0 ) { printf( "%d\n" , len / ( len - f[len] ) ) ; }else{ printf( "1\n" ) ; } } return 0 ; }
HDOJ 3336 Count the string
求各个前缀在字符串中出现的次数的总和
dp[i] 表示 以字符i-1为结尾的和前缀相同的子串的有多少
那么 : dp[i] = dp[f[i]] + 1 , 首先以为结尾的字符串本身肯定是一个前缀,所以至少是1
然后dp[f[i]] 是以 f[i] -1 为结尾的前缀数 , 而s[0~f[i]-1]又刚好就是s[0~i-1]的后缀,所以第i-1个字符结尾的长度小于i的字符串总数为dp[f[i]]
最后求个和就行了
#include <stdio.h> #include <string.h> #include <algorithm> using namespace std; #define mod 10007 char str[200005] ; int f[200005] ; int dp[200005] ; void get_nex( char * str , int len ){ f[0] = f[1] = 0 ; int j ; for( int i = 1 ; i < len ; i ++ ) { j = f[i] ; while( j && str[i] != str[j] ) j = f[j] ; if( str[i] == str[j] ) f[i+1] = j + 1 ; else f[i+1] = 0 ; } } int main(){ int cas ; scanf( "%d" , &cas ) ; while( cas -- ) { int len ; scanf( "%d" , &len ) ; scanf( "%s" , str ) ; get_nex( str , len ); dp[1] = 1 ; int sum = 1 ; for( int i = 2 ; i <= len ; i ++ ) { dp[i] = dp[f[i]] + 1 ; sum += dp[i] ; sum %= mod ; } printf( "%d\n" , sum ) ; } return 0 ; }
HDOJ 3336 Theme Section
求既是字符串的前缀,又是字符串的后缀,又在中间出现的最长子串( 三个不能重叠 ) 。
首先我们要求出所有既是前缀又是后缀的字符串的长度,用vis数组标记下
然后枚举中间的字符串的终点,求出 f[i] 在判断下合不合法 ,有没有标记过
最后求出长度最大,既合法,又被vis数组标记过的长度就行了
#include <stdio.h> #include <string.h> #include <algorithm> using namespace std; #define MAXN 1000005 char str[MAXN] ; int f[MAXN] ; bool vis[MAXN] ; void get_nex( char * str , int len ) { f[0] = f[1] = 0 ; int j ; for( int i = 1 ; i < len ; i ++ ) { j = f[i] ; while( j && str[i] != str[j] ) j = f[j] ; if( str[i] == str[j] ) f[i+1] = j + 1 ; else f[i+1] = j ; } } int main(){ int cas ; scanf( "%d" , &cas ) ; while( cas -- ) { scanf( "%s" , str ) ; int len = strlen( str ) ; memset( vis , false , sizeof(vis) ) ; get_nex( str , len ) ; int Len = f[len] ; while( Len ) { vis[Len] = true ; Len = f[Len] ; } int ans = 0 ; for( int i = 2 ; i < len ; i ++ ) { int ll = f[i] ; int tmp = min( min( ll , i / 2 ) , min( f[len] , len - i ) ) ; if( vis[tmp] ) ans = max( ans , tmp ) ; } printf( "%d\n" , ans ) ; } return 0 ; }
POJ 2185 Milking Grid
就是求二维情况的循环串
我是先把一维用多项式hash压缩掉,求一维的循环节 , 然后在压缩另外一维 , 再求一遍就行了
#include <stdio.h> #include <string.h> #include <algorithm> using namespace std ; #define MAXN 10005 char mp[MAXN][85] ; typedef unsigned __int64 uLL ; uLL a[MAXN] ; int f[MAXN] ; void get_nex( uLL * a , int len ){ f[0] = 0 , f[1] = 0 ; int j ; for( int i = 1 ; i < len ; i ++ ) { j = f[i] ; while( j && a[i] != a[j] ) j = f[j] ; if( a[i] == a[j] ) f[i+1] = j + 1 ; else f[i+1] = 0 ; } } int main(){ int n , m ; while( scanf( "%d%d" , &n, &m ) != EOF ) { for( int i = 0 ; i < n ; i ++ ) { scanf( "%s" , mp[i] ) ; a[i] = 0 ; for( int j = 0 ; j < m ; j ++ ) { a[i] = a[i] * 10007 + mp[i][j] - 'A' ; } } get_nex( a , n ) ; int H = n - f[n] ; for( int i = 0 ; i < m ; i ++ ) { a[i] = 0 ; for( int j = 0 ; j < H ; j ++ ) { a[i] = a[i] * 10007 + mp[j][i] - 'A' ; } } get_nex( a , m ) ; int W = m - f[m] ; printf( "%d\n" , H * W ) ; } return 0 ; }
HDOJ 2594 Simpsons’ Hidden Talents
给你两个串, 让你求第一串和第二个串的最长公共前后缀
我直接把两个字符串用#连起来,求下失配函数, f[len]就是要求的答案了
#include <stdio.h> #include <string.h> #include <algorithm> using namespace std; #define MAXN 50005 * 2 char str[MAXN] ; int f[MAXN] ; void get_nex( char * str , int len ){ f[0] = f[1] = 0 ; int j ; for( int i = 1 ; i < len ; i ++ ) { j = f[i] ; while( j && str[i] != str[j] ) j = f[j] ; if( str[i] == str[j] ) f[i+1] = j + 1 ; else f[i+1] = 0 ; } } int main(){ while( scanf( "%s" , str ) != EOF ) { int len = strlen( str ) ; str[len] = '#' ; scanf( "%s" , str + len + 1 ) ; len = strlen( str ) ; get_nex( str , len ) ; if( f[len] ) { str[f[len]] = 0 ; printf( "%s %d\n" , str , f[len] ) ; }else{ printf( "0\n" ) ; } } return 0 ; }HDOJ 3746 Cyclic Nacklace
问至少增加一个字符可以变成至少有两个以上循环节的循环串
如果本身就是循环串 ,如果只有一个循环节,那要在复制一次字符串 , 否则答案就是0
不是的话,就看 len - f[len] 的长度 ,看添加几个能补到被len整数就行了
#include <stdio.h> #include <string.h> #include <algorithm> #include <stack> using namespace std; #define MAXN 400005 char str[MAXN] ; int f[MAXN] ; void get_nex( char * str , int len ){ f[1] = f[0] = 0 ; int j; for( int i = 1 ; i < len ; i ++ ) { j = f[i] ; while( j && str[i] != str[j] ) j = f[j] ; if( str[i] == str[j] ) f[i+1] = j + 1 ; else f[i+1] = 0 ; } } int main(){ while( scanf( "%s" , str ) != EOF ) { int len = strlen( str ) ; get_nex( str , len ) ; stack<int> s ; s.push( len ) ; while( f[len] ) { s.push( f[len] ) ; len = f[len] ; } bool first = true ; while( !s.empty() ) { printf( first?"%d" :" %d" , s.top() ) ; s.pop() ; first = false ; } puts( "" ) ; } return 0 ; }
POJ 2752 Seek the Name, Seek the Fame
就是求所有的前后缀的长度,从小到大输出来
上面已经讲过了 , 直接 len , f[len] , f[f[len]] ... 一路到0就行了
#include <stdio.h> #include <string.h> #include <algorithm> #include <stack> using namespace std; #define MAXN 400005 char str[MAXN] ; int f[MAXN] ; void get_nex( char * str , int len ){ f[1] = f[0] = 0 ; int j; for( int i = 1 ; i < len ; i ++ ) { j = f[i] ; while( j && str[i] != str[j] ) j = f[j] ; if( str[i] == str[j] ) f[i+1] = j + 1 ; else f[i+1] = 0 ; } } int main(){ while( scanf( "%s" , str ) != EOF ) { int len = strlen( str ) ; get_nex( str , len ) ; stack<int> s ; s.push( len ) ; while( f[len] ) { s.push( f[len] ) ; len = f[len] ; } bool first = true ; while( !s.empty() ) { printf( first?"%d" :" %d" , s.top() ) ; s.pop() ; first = false ; } puts( "" ) ; } return 0 ; }
ZOJ 3587 Marlon‘s String
给你两个字符串, 第一个字符串中取出两段来,有多少种方式可以组成第二个字符串。取出来的两段可以重叠
那么我们先统计一下cnt1[i] 表示第一个字符串中有多少子串是第二个字符长度为i的子串
计算的时候我们先根据匹配结果去统计,即到匹配到一次长度为i的就cnt1[i] ++ ; 然后因为s[0...i-1] 出现一次的话,那么s[0...f[i]-1]肯定也要出现一次,所以我们统计到cnt1[i] 的时候,要cnt1[f[i]] += cnt1[i]
当然出现一次 s[0...i-1]更短的也会出现过,但是我们计算到cnt1[i]的时候并不用统计,因为这些会在cnt1[f[i]]统计的时候加上
然后两个字符串反过来在匹配一遍,统计出cnt2[i]
那么最后答案就是Σ( cnt1[i] * cnt2[len-i] ) 了
#include <stdio.h> #include <string.h> #include <algorithm> using namespace std; #define MAXN 100001 typedef long long LL ; char s[MAXN] ; char t[MAXN] ; char tmp[MAXN] ; int cnt1[MAXN] , cnt2[MAXN] ; int f[MAXN] ; void get_nex( char * str , int len ) { f[0] = f[1] = 0 ; int j ; for( int i = 1 ; i < len ; i ++ ) { j = f[i] ; while( j && str[i] != str[j] ) j = f[j] ; if( str[i] == str[j] ) f[i+1] = j + 1 ; else f[i+1] = 0 ; } } void KMP( char * str1 , int len1 , char * str2 , int len2 , int * cnt ) { int j = 0 ; for( int i = 0 ; i < len1 ; i ++ ) { while( j && str1[i]!=str2[j] ) j = f[j] ; if( str1[i] == str2[j] ) { j ++ ; cnt[j] ++ ; } if( j == len2 ) j = f[j] ; } } void stringrev( char * s ) { int len = strlen( s ) ; strcpy( tmp , s ) ; for( int i = 0 ; i < len ; i ++ ) { s[len-i-1] = tmp[i] ; } s[len] = 0 ; } int main(){ int cas ; scanf( "%d" , &cas ) ;getchar() ; while( cas -- ) { gets(s); gets(t); int len1 , len2 ; len1 = strlen( s ) , len2 = strlen( t ) ; get_nex( t , len2 ) ; memset( cnt1 , 0 ,sizeof(cnt1) ) ; KMP( s , len1 , t , len2 , cnt1 ) ; for( int i = len2 ; i >= 1 ; i -- ) { cnt1[f[i]] += cnt1[i] ; } stringrev( s ) ; stringrev( t ) ; get_nex( t , len2 ) ; memset( cnt2 , 0 , sizeof(cnt2) ) ; KMP( s , len1 , t , len2 , cnt2 ) ; for( int i = len2 ; i >= 1 ; i -- ) { cnt2[f[i]] += cnt2[i] ; } LL ans = 0 ; for( int i = 1 ; i < len2 ; i ++ ) { ans += (LL)cnt1[i] * cnt2[len2-i] ; } printf( "%lld\n" , ans ) ; } return 0 ; }
POJ 3167 Cow Patterns
给你两个序列 ,要求匹配模式串 ,怎么算匹配上呢 , 就是两个区间中各个数的相对名次是一样的
比如a[i]的名次比哪些小,和哪些一样,都要匹配上
我们预处理出每个数前面有几个比它小的和几个小于等于它的,然后改写下字符匹配上的标准就行了
#include <stdio.h> #include <string.h> #include <algorithm> #include <vector> using namespace std; int n , m , s ; int a[100005] , b[100005] ; int sa[100005][30] ; int sb[100005][30] ; int f[100005] ; vector<int> ans ; void get_nex( int * a , int (*p)[30] , int len ){ f[0] = f[1] = 0 ; int j ; for( int i = 1 ; i < len ; i++ ) { j = f[i] ; while( j && ( p[j+1][a[j]] != p[i+1][a[i]] - p[i-j][a[i]] || p[j+1][a[j]-1] != p[i+1][a[i]-1] - p[i-j][a[i]-1] ) ) j = f[j] ; if( p[j+1][a[j]] == p[i+1][a[i]] - p[i-j][a[i]] && p[j+1][a[j]-1] == p[i+1][a[i]-1] - p[i-j][a[i]-1] ) f[i+1] = j + 1 ; else f[i+1] = 0 ; } } void KMP( int * a , int (*p)[30] , int len1 , int *b , int (*q)[30] , int len2 ) { int j = 0 ; for( int i = 0 ; i < len2 ; i ++ ) { while( j && ( p[j+1][a[j]] != q[i+1][b[i]] - q[i-j][b[i]] || p[j+1][a[j]-1] != q[i+1][b[i]-1] - q[i-j][b[i]-1] ) ) j = f[j] ; if( p[j+1][a[j]] == q[i+1][b[i]] - q[i-j][b[i]] && p[j+1][a[j]-1] == q[i+1][b[i]-1] - q[i-j][b[i]-1] ) j ++ ; if( j == len1 ) ans.push_back( i - len1 + 2 ) , j = f[j] ; } } int main(){ while( scanf( "%d%d%d" , &n , &m , &s ) != EOF ) { for( int i = 0 ; i < n ; i ++ ) { scanf( "%d" , &a[i] ) ; sa[i+1][a[i]] ++ ; for( int j = 1 ; j <= s ; j ++ ) sa[i+1][j] += sa[i][j] ; } for( int i = 0 ; i < n ; i ++ ) { for( int j = 1 ; j <= s ; j ++ ) sa[i+1][j] += sa[i+1][j-1] ; } for( int i = 0 ; i < m ; i ++ ) { scanf( "%d" , &b[i] ) ; sb[i+1][b[i]] ++ ; for( int j = 1 ; j <= s ; j ++ ) sb[i+1][j] += sb[i][j] ; } for( int i = 0 ; i < m ; i ++ ) { for( int j = 1 ; j <= s ; j ++ ) sb[i+1][j] += sb[i+1][j-1] ; } get_nex( b , sb , m ) ; ans.clear() ; KMP( b , sb , m , a , sa , n ) ; printf( "%d\n" , ans.size() ) ; for( int i = 0 ; i < ans.size() ; i ++ ) { printf( "%d\n" , ans[i] ) ; } } return 0 ; }