字符串单模板匹配学习笔记(一)kmp算法

数据结构课上学到了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; q0 && 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;
}

你可能感兴趣的:(字符串)