首页 > 技术文章 > ACM之路(12)—— KMP & 扩展KMP & Manacher

zzyDS 2016-05-15 19:30 原文

  最近做完了kuangbin的一套关于kmp的题目(除了一道字典树的不会,因为还没学字典树所以先放放),做个总结。(kuangbin题目的链接:http://acm.hust.edu.cn/vjudge/contest/view.action?cid=70325#problem/A

  说实话,kmp这个东西真的理解了很久,因为网上模板的下标都不一样,而且讲解的部分和代码实现又有点差别。话说当初看到一个比较好的博客忘记保存了。。= =

  首先给个最简单的题目:http://oj.acm.zstu.edu.cn/JudgeOnline/problem.php?id=4194

  这就是直接赤裸裸的kmp,这里直接给出代码当作模板:

 1 #include <stdio.h> 
 2 #include <algorithm> 
 3 #include <string.h> 
 4 using namespace std; 
 5   
 6 char a[1000000+5],b[1000000+5]; 
 7 int next[1000000+5],lena,lenb; 
 8 void getnext() 
 9 { 
10     next[1]=0; 
11     int j=0; 
12     for(int i=2;i<=lenb;i++) 
13     { 
14         while(j>0&&b[j+1]!=b[i]) j=next[j]; 
15         if(b[j+1]==b[i]) j++; 
16         next[i]=j; 
17     } 
18 } 
19 int kmp() 
20 { 
21     int cnt=0,j=0; 
22     for(int i=1;i<=lena;i++) 
23     { 
24         while(j>0&&b[j+1]!=a[i]) j=next[j]; 
25         if(b[j+1]==a[i]) j++; 
26         if(j==lenb) cnt++,j=0; 
27     } 
28     return cnt; 
29 } 
30 int main() 
31 { 
32     while(scanf("%s%s",a+1,b+1)==2) 
33     { 
34         lena = strlen(a+1); 
35         lenb = strlen(b+1); 
36         getnext(); 
37         printf("%d\n",kmp()); 
38     } 
39     return 0; 
40 } 

  这是kmp最基本的应用,在这里,子串不能重复,即aaa中aa只出现一次,如果要aaa中不同起点但是子串有重复部分的也算的话,很简单,只要把kmp中的j=0改成j=nxt[j]即可(只要理解了kmp这还是比较容易想通的)。

  

  接着,就是关于kmp求循环节了,对于求循环节的话,其循环节等于len-nxt[len],这个比较巧妙,自己动手画画图还是可以想通的。

  例题的话推荐kuangbin的D题(HDU3746)。

  

  然后,还可以在中间加个'#'再把原串的反过来的形式接在后边,这样,再使用kmp就可以求得最长回文前缀了,同理,两边都倒过来就可以求得最长回文后缀。加个'#'可以保证新串的公共前缀一定在'#'前面,即回文前缀在'#'前面(巧妙啊!!)。

  题目就是kuangbin的S题(HDU3613)。

  题意就是一个串将其截断,任意一边如果是回文就给出其价值,否则价值为0。要注意的一个地方是,每个字符的价值可能是0,所以ans应当初始化为负无穷以免出错(不过好像本来不是回文就是0所以不需要?我也搞不清楚了,反正保险点没错的。。)。

  AC代码如下:

 1 #include <stdio.h>
 2 #include <string.h>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 int value[30];
 7 char s[500000*2+10];
 8 int nxt[500000*2+10];
 9 int pre[500000*2+10],pos[500000*2+10];
10 int sum[500000+10];
11 int len;
12 void getnxt()
13 {
14     int j=0;
15     memset(nxt,0,sizeof(nxt));
16     for(int i=2;i<=len;i++)
17     {
18         while(j>0&&s[j+1]!=s[i]) j=nxt[j];
19         if(s[j+1]==s[i]) j++;
20         nxt[i]=j;
21     }
22 }
23 int main()
24 {
25     int T;
26     scanf("%d",&T);
27     while(T--)
28     {
29         memset(sum,0,sizeof(sum));
30         memset(pre,0,sizeof(pre));
31         memset(pos,0,sizeof(pos));
32         for(int i=1;i<=26;i++) scanf("%d",&value[i]);
33         scanf("%s",s+1);
34 
35         int k=strlen(s+1);     //k是原串的长度
36         for(int i=1;i<=k;i++) sum[i]=sum[i-1]+value[s[i]-'a'+1];   //sum[i]表示到i为止的和
37 
38         len=k;
39         int sit=k+1;
40         s[k+1]='#';
41         while(len>=1) s[++sit]=s[len--];   //把字符串变为以#为中心的对称字符串
42         s[sit+1]=0;
43 
44         len=sit;    //len是变化后串的总长度
45         getnxt();
46 
47         while(nxt[sit]) pre[sit]=T+1,sit=nxt[sit]; //表示sit处及以前是回文
48         pre[1]=T+1;         //第一个字母一定是回文的
49 
50         for(int i=1;i<=k;i++)
51         {
52             swap(s[i],s[i+k+1]);
53         }
54         getnxt();
55         sit=len;
56         while(nxt[sit]) pos[sit]=T+1,sit=nxt[sit];  //sit是反转以后原串的sit位置及以前是回文
57         pos[1]=T+1;       //第一个字母一定是回文的
58 
59         int num=0;
60         int ans=-0x3f3f3f3f;
61 
62         for(int i=1;i<k;i++) //在i的后面的位置切断
63         {
64             num=0;
65             if(pre[i]) num+=sum[i];
66             if(pos[k-i]) num+=sum[k]-sum[i];
67             ans=max(ans,num);
68         }
69         printf("%d\n",ans);
70     }
71     return 0;
72 }

 

  扩展kmp说实话我也不会- -,然后要说的就是kmp有个类似的库函数strstr(a,b),判断b是不是a的字串,不是的话返回0,否则返回第一次出现的首地址,如果只要判断有没有出现而不需要知道几次的话这个还是比较方便的。。

 

  另外,再讲一个最大最小表示法。即关于一个字符串不断的将第一个字符拿到最后,这出现的所有情况中总有一个字典序最大一个字典序最小的,这就是最大最小表示法。也是通过一个例题给出模板,但是我还不能完全证明,只能通过动手画图大概判断它的正确性,还是比较巧妙的,我也没有找出反例(怎么可能找出反例- 。-)。话说字符串的东西都是比较巧妙的。。

  例题是kuangbin的O题(HDU3374)。

  下面给出AC代码:

 1 #include <stdio.h>
 2 #include <algorithm>
 3 #include <string.h>
 4 using namespace std;
 5 
 6 char s[1000000+10];
 7 int nxt[1000000+10];
 8 int len;
 9 void getnxt()
10 {
11     int j=0;
12     nxt[1]=0;
13     for(int i=2;i<=len;i++)
14     {
15         if(j>0&&s[j+1]!=s[i]) j=nxt[j];
16         if(s[j+1]==s[i]) j++;
17         nxt[i]=j;
18     }
19 }
20 int min_max_press(int flag)
21 {
22     int i=1,j=2,k=0;
23     while(i<=len&&j<=len&&k<=len)
24     {
25         int sit1,sit2;
26         sit1=(i+k)%len==0?len:(i+k)%len;
27         sit2=(j+k)%len==0?len:(j+k)%len;
28         int t=s[sit1]-s[sit2];
29         if(t==0) k++;
30         else
31         {
32             if(flag)
33             {
34                 if(t>0) j+=k+1;
35                 else i+=k+1;
36             }
37             else
38             {
39                 if(t<0) j+=k+1;
40                 else i+=k+1;
41             }
42             if(i==j) j++;
43             k=0;
44         }
45     }
46     return min(i,j);
47 }
48 int main()
49 {
50     int n;
51     while(scanf("%s",s+1)==1)
52     {
53         len = strlen(s+1);
54         memset(nxt,0,sizeof(nxt));
55         getnxt();
56         int looplen = len-nxt[len];
57         int ans=len%looplen?1:len/looplen;
58         int minn=min_max_press(0);
59         int maxx=min_max_press(1);
60         printf("%d %d %d %d\n",minn,ans,maxx,ans);
61     }
62     return 0;
63 }

 

  最后要讲的是manacher(马拉车)算法。这个算法主要就是用来求一个串中的最长回文长度(当然不限于字符串)。主要思想就是在每两个元素以及开头结尾都插上一个不可能出现的字符如'#',然后对于每一个位置都求出p[i],即新串的每一个点的最大回文长度,然后对于原串来说,这个值减1就是了。具体的也不证明了(其实是我说不清楚- -),和上面的构造得出最大回文前缀有点类似。

  下面给出例题:

  X题(HDU3068):简单暴力地套模板就好了。

  AC代码如下:

 1 #include <stdio.h>
 2 #include <algorithm>
 3 #include <string.h>
 4 using namespace std;
 5 
 6 char s[110000+5],str[3*110000+5];
 7 int p[3*110000+5];
 8 int main()
 9 {
10     while(scanf("%s",s+1)==1)
11     {
12         int sit=1;
13         int len=strlen(s+1);
14         for(int i=1;;i++)
15         {
16             if(i&1)
17             {
18                 str[i]='#';
19             }
20             else
21             {
22                 str[i]=s[sit++];
23             }
24             if(sit==len+1) {str[i+1]='#';len=i+1;break;}
25         }
26         int mx=0,index=0;
27         str[0]='$';
28         for(int i=1;i<=len;i++)
29         {
30             if(mx>i)
31             {
32                 p[i]=min(p[2*index-i],mx-i);
33             }
34             else p[i]=1;
35             for(;str[i+p[i]]==str[i-p[i]];p[i]++);
36             if(mx<i+p[i])
37             {
38                 mx=i+p[i];
39                 index=i;
40             }
41         }
42         int ans=1;
43         for(int i=1;i<=len;i++) ans=max(p[i],ans);
44         printf("%d\n",ans-1);
45     }
46     return 0;
47 }

  当然对于不是字符串的也可以用这个算法的,比方说V题(HDU4513)。

  

  这套题目里面还有许多好题,但是我在这里只再讲一题,觉得比较复杂的一题。

  L题(HDU4300)。题意是,每次给出两个串,a串是有26个的长度,每个位置表示原来字母表的那个位置转化为密码后的字母。而b串,可以表示成,密码串+原串的形式。密码串是原串通过表a翻译成密码串的。密码串一定完整,而原串可能缺损,或完整或全部没有。然后要你输出全部的完整两个串。举个例子abcde经过翻译以后是qwert。那么如果串b给的是qwertabc,显然后面完整的是abcde所以输出qwertabcde。然后我的思路就是,因为串b中原串的长度一定不会超过串b长度的一半,那么从b中的后一半截断。同时给串b按表翻译成原来的样子。比方说上面这个例子,截断的后半是tabc,然后b串翻译回去是abcdexxx(后面3个不重要了)然后把截断的tabc和abcdexxx的前缀匹配一下,得到tabc的后缀能和他匹配到的最大长度匹配出来就好了,匹配出来是abc嘛,然后在接着输出de就好了。

  思路大致如此,具体见代码:

 1 #include <stdio.h>
 2 #include <algorithm>
 3 #include <string.h>
 4 using namespace std;
 5 
 6 int nxt[100000+5];
 7 char s1[100000+5],s2[100000+5];
 8 void getnxt(char *s)
 9 {
10     int j=0;
11     nxt[1]=0;
12     int len = strlen(s+1);
13     for(int i=2;i<=len;i++)
14     {
15         while(j>0&&s[j+1]!=s[i]) j=nxt[j];
16         if(s[j+1]==s[i]) j++;
17         nxt[i]=j;
18     }
19 }
20 int kmp(char *s,char *t)
21 {
22     memset(nxt,0,sizeof(nxt));
23     int lent=strlen(t+1),lens=strlen(s+1);
24     getnxt(t);
25     int i=0,j=0;
26     for(int i=1;i<=lens;i++)
27     {
28         while(j>0&&t[j+1]!=s[i]) j=nxt[j];
29         if(t[j+1]==s[i]) j++;
30     }
31     return j;
32 }
33 
34 int main()
35 {
36     int T;
37     scanf("%d",&T);
38     while(T--)
39     {
40         char str[30];
41         scanf("%s%s",str+1,s1+1);
42         int len=strlen(s1+1);
43         strcpy(s2+1,s1+1+(len+1)/2);
44         printf("%s",s1+1);
45         for(int i=1;i<=len;i++)
46         {
47             for(int j=1;j<=26;j++)
48             {
49                 if(s1[i]==str[j])
50                 {
51                     s1[i]='a'+(j-1);
52                     break;
53                 }
54             }
55         }
56         int flag = kmp(s2,s1);
57         for(int i=flag+1;i<=len-flag;i++) printf("%c",s1[i]);
58         puts("");
59     }
60 }

  要注意红色的部分,因为这里WA了好多次QAQ。。

  那么,到这里kmp的就总结完了,还有什么以后想起来再补上好了。。

 

推荐阅读