数据结构课上学到了kmp算法,顺便就深入的学习一下相关的单模板匹配问题。为之后学习ac自动机和后缀数组等字符串算法做一个铺垫。
关于单模板匹配问题参考了:
《字符串匹配算法总结》
http://blog.csdn.net/WINCOL/article/details/4795369
【kmp】
作为经典字符串匹配算法,kmp有相当广泛的应用。所以虽然比较难以理解,并且实际效率可能不如一些字符串单模板匹配算法,却仍然是需要完全掌握,理解其操作过程的一个重要算法。
我认为,整个算法的核心在next数组的求取上。虽然匹配过程不如Sunday算法简洁高效,但是next数组的实现过程是值得研究,借鉴的。这也使得kmp算法有着更广阔的应用空间。、
那么问题来了,什么是next数组?
0...i这个串的前next[i]个字符和后next[i]个字符完全匹配。
保存这样的信息的数组叫做next数组
next的实现过程,网上很多blog往往直接跳过(私以为是舍本逐末)。下面的文章详细分析了next的实现过程,可以帮助初学者理解next数组是如何实现的:
《字符串匹配的KMP算法》-阮一峰
http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
《【经典算法】——KMP,深入讲解next数组的求解 》
http://www.cnblogs.com/c-cloud/p/3224788.html
当我们求得next数组之后,kmp的匹配过程就没有什么难点了。
匹配过程如下:
对于匹配串S,如果在q点失配,模板串已匹配了k-1个字符。
我们想让q点匹配,所以需要在模板串里找一个结点来匹配q点。
因为模板串0..k-1的前next[k-1]个字符和后next[k-1]个字符匹配,所以我们直接将串移动k-1-next[k-1]个位置,检测q点是否可以匹配。
一直重复这个过程即可。
求next数组的代码:
#include
#include
#include
using namespace std;
char T[1000];
int nxt[1000];
void get_next()
{
int len_T=strlen(T);
int k=0;
nxt[0]=0;
for( int q=1; qwhile( k>0 && T[q]!=T[k] )k=nxt[k-1];
if(T[q]==T[k])
{
k++;
}
nxt[q]=k;
}
}
int main()
{
cin>>T;
get_next();
for( int i=0; i<strlen(T); i++ )
{
cout<" "<return 0;
}
除了单模板匹配,next数组还可以用来判循环节。下面是一道典型例题。
poj2406.
http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=10758
想想next数组如何应用在这里?
或者说如何来求出最小循环节?
这点是值得思考的。结论放在代码里。
【参考代码】
#include
#include
#include
using namespace std;
char T[1000000+10];
int nxt[1000000+10];
int get_nxt( int len )
{
nxt[0]=0;
for( int q=1,k=0; q 0 && T[q]!=T[k] )k=nxt[k-1];
if( T[q]==T[k] )k++;
nxt[q]=k;
}
if( len%( len-nxt[len-1] )==0 )return len/(len-nxt[len-1]);
else return 1;
}
int main()
{
while( ~scanf( "%s",&T ) )
{
int len=strlen(T);
if( (len==1) && T[0]=='.' )break;
printf( "%d\n", get_nxt(len) );
// for( int i=0; i" "<return 0;
}
/*
poj 2406 判循环节
当它是一个a^n形式的时候, 设t=len-next[len-1]
若 len%t==0; 则t是它的最小循环节长度。
证明:
设a^n=AAAAA...A(n个A) //A是最小循环节
那么必有前缀 AAAA....A(n-1个A)和后缀AAAA...A ( n-1个A )相同。
所以最小循环节长度为 len-next[len-1]=t,此时必有len%t==0.
若p则q <=> 若!q则!p,所以我们可以得出结论:
若len%t!=0,则t不是a^n的最小循环节
现在我们证,当len%t==0的时候t是它的最小循环长度
令A为前t个字节
设前缀=ABC...
后缀= A'B'C'.....
由next定义知A=A'
所以前缀可以表述为AABC......
因为后缀和前缀匹配,所以有后缀可以表述为A'A'......
依次类推得到A......A这样一个串,
所以A是最小循环节
*/
再来做一做其它求(判)循环节的例题:
poj1961
http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=10934
poj2752(理解了next的实现过程才能做这道题目):
http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=10055
hdu3746(最小覆盖子串)
http://acm.hdu.edu.cn/showproblem.php?pid=3746
理解题意理解了好久….我误以为会出现abcabcde这样的串,实际上串珠子的规则是将一个color序列循环至少两次,这保证了给定的初始序列一定是某个串循环了i次加上它的前j个珠子。那么这就变成了一道水题。
利用前面例题的结论即可做。
下面是代码:
【poj1961】
#include
#include
#include
using namespace std;
char T[1000000+10];
int nxt[1000000+10];
int len;
void get_nxt( int len )
{
nxt[0]=0;
for( int q=1,k=0; qwhile( k>0 && T[q]!=T[k] )k=nxt[k-1];
if( T[q]==T[k] )k++;
nxt[q]=k;
}
}
int main()
{
int kase=0;
while( ~scanf("%d",&len) && len )
{
printf( "Test case #%d\n",++kase );
scanf("%s",&T);
get_nxt( len );
for( int i=1; iint t=i+1-nxt[i];
if( ((i+1)%t==0) && (t!=(i+1)) ) printf( "%d %d\n", i+1, (i+1)/t );
}
puts("");
}
return 0;
}
【poj2752】
#include
#include
#include
#include
using namespace std;
char T[400000+10];
int nxt[400000+10];
int len;
void get_nxt( int len )
{
nxt[0]=0;
for( int q=1,k=0; qwhile( k>0 && T[q]!=T[k] )k=nxt[k-1];
if( T[q]==T[k] )k++;
nxt[q]=k;
}
}
int main()
{
int kase=0;
while( ~scanf("%s",&T) )
{
int len=strlen(T);
get_nxt( len );
stack<int>sta;
int k=len-1;
// for( int i=0; i
for( int k=len-1; k>=0; k=nxt[k]-1 )
{
sta.push(k);
}
while( !sta.empty() )
{
printf( "%d ",sta.top()+1 );
sta.pop();
}
puts("");
}
return 0;
}
【hdu3746】
#include
#include
#include
using namespace std;
const int maxn=100000+50;
char S[maxn];
int nxt[maxn];
void get_nxt( int L )
{
nxt[0]=0;
int k=0;
for( int i=1; iwhile( S[i]!=S[k] && k>0 ) k=nxt[k-1];
if( S[i]==S[k] )++k;
nxt[i]=k;
}
}
int main()
{
int T;
cin>>T;
while( T-- )
{
scanf("%s",&S);
int len=strlen(S);
get_nxt(len);
int t=len-nxt[len-1];
if( len%t==0 && len!=t )printf("%d\n",0);
else
{
printf("%d\n",t-len%t);
}
}
return 0;
}