KMP算法(POJ2406)
由于POJ2406开始研究这个算法。
在这两个博客中得到资料:
http://www.matrix67.com/blog/archives/115 Matrix67的,不多说
http://www.cnblogs.com/dolphin0520/archive/2011/08/24/2151846.html 这个有图解比较详细
July的经典算法研究里也有讲解KMP算法的部分
本文主要是对于前几个博文我不大明白的地方的一个梳理。
首先明确KMP算法是为了计算出字符串B是否为字符串A的子串。基本信息都可以从上面两个博文中了解到很详细,这里不赘述。
讨论KMP算法之前,先提一下BF算法,对于第一次接触这个问题的人来说,第一反应可能就采用枚举的方法,也就是挨个比较目标串和模式串,这就是朴素的BF算法。当模式串和目标串有一个字符不匹配的时候,BF算法采取的是返回模式串的头,然后从目标串的下一个字符重新开始比较。如图


KMP的优化之处就在于它要减少无必要的回溯,也就是在不匹配发生时,因为我们已经知道目标串和模式串前面部分是匹配的,如果知道模式串自身是有部分重复的,比如对于模式串ababa, 在最后一个a不匹配时,说明目标串是有abab部分的(至少前部分是匹配的),在回溯的时候模式串直接向右平移两位,平移到和原模式子串(”ab")相等的模式子串,减少不可能匹配的回溯。
(虽然这一步还是失配,但是至少减少了两次匹配)
为了达到这个目标,首先要对于模式串做一个预处理。
这个预处理是针对子串的(这让KMP有一个好处就是它经过一次预处理就可以和多个目标串匹配,不需要多次预处理),这个处理函数又很多种叫法,什么覆盖函数,getnext(),但是最终目标都是求得一个只与模式串相关的数组:
覆盖的数学定义(转自July)

这个k值数组P[i]是KMP算法思想的精髓所在,你可以采用不同的方法去实现它,初始计数值是0还是1也无所谓。但是你必须要理解它,才算是真正理解了KMP算法。
关于这个数组有两个点,一个是如何计算,二是如何应用。
计算:这个数组的值j'是指"子“模式串中头j'个字母和末j'个字母完全相等,也可以理解成模式串的“覆盖度”。P[j]应该是所有满足B[1..P[j]]=B[j-P[j]+1..j]的最大值。比如说对于模式串abcedabc
|
i |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
|
B[i] |
a |
b |
a |
b |
a |
c |
b |
|
P[i] |
0 |
0 |
1 |
2 |
3 |
0 |
0 |
举个例子,求p[5],由于B[1…2]=B[3…5]="aba",所以P[5]=3(根据初始定义的不同也可以等于2,都减一就行了)。
应用:在应用中你就会明白这样计算的目的是什么
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 1 2 3 4 5 6 7(求失配字符前一个字符的P[j]值)
由于P[5]=3,因此新的j=3:(也就是说,在应用中是将A[i]重新对应到失配字符B[i+1]前一个字符B[i]的P[i]值,从A[6]匹配B[5]变为A[6]匹配B[3], 3就是P[5]的值)
i = 1 2 3 4 5 6 7 8 9 ……
A = a b a b a b a a b a b …
B = a b a b a c b
j = 1 2 3 4 5 6 7
现在明白了吧,因为在比较A[8]和B[6]之前目标串和模式串已经进行了5次比较,并且都匹配,我们不能浪费这个信息啊,所以在预处理后我们知道在模式串中B[3…5]="aba"=B[1…2],就可以将模式串B直接向右平移2位(P[5]-1,也就是不匹配的B[4]的前一个字符的覆盖度减一),因为我们可以确保A[5…7]=B[1…2]。
就讲到这儿,只要明白的KMP算法的思想,再去看前面两个博客会更深入,我这里只是开个窍,计算P[i]有很多种办法,只要明白思路就行。
附 KMP代码(第二个博客中的,整理了一下)
#include <stdio.h> #include <string.h> //next[k]的值表示p[0...k-1]中最长后缀的长度 void getNext(char *p,int *next) { int j,k; next[0]=-1; j=0; k=-1; while(j<strlen(p)-1) { if(k==-1||p[j]==p[k]) //匹配的情况下,p[j]==p[k] { j++; k++; next[j]=k; } else //p[j]!=p[k] k=next[k]; } } int KMPMatch(char *s,char *p) { int next[100]; int i,j; i=0; j=0; getNext(p,next); while(i<strlen(s)) { if(j==-1||s[i]==p[j]) { i++; j++; } else { j=next[j]; //消除了指针i的回溯 } if(j==strlen(p)) return i-strlen(p); } return -1; } //int BFMatch(char *s, char* p) //{ // int i=0,j=0; // while(strlen(s)>strlen(p)&&i<strlen(s)) // { // j=0;//要写在循环内,不然比较不能从子串的起点重新开始 // while(s[i]==p[j]&&j<strlen(p)) // if(s[i]==p[j]&&j<strlen(p)) // { // { // i++; // i++; // j++; // j++; // } // } // if(j==strlen(p)) // else if(s[i]==p[j]&&j==strlen(p)) // { // { // return i-strlen(p); // return 1;//子串匹配 // } // } // i=i-j+1; // else // } // { // return -1; // i=i-j+1;//不匹配,回溯 // // } // //} // // return 0; //} void main() { char a[]={"ababcababa"}; char b[]={"ababa"}; //BFMatch(a,b); KMPMatch(a,b); }
POJ 2406代码
package poj2406; //找出循环子序列的个数 import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scn = new Scanner(System.in); // System.out.println(Main.class.getResource("in.dat")); // Scanner scn = new Scanner(Main.class.getResourceAsStream("in.dat")); PrintWriter out = new PrintWriter(System.out); String input = ""; List<String> inputs = new ArrayList<String>(5000); int index = 0; while (scn.hasNext()) { input = scn.next();//read a string if (input.equals(".")) { break; } index++; inputs.add(input); } for (int i = 0; i < index; i++) { out.println(kmp(inputs.get(i))); } out.flush(); } private static int kmp(String input) { int len = input.length(); int ret[] = new int[len]; ret[0] = -1; int k = -1; for (int i = 1; i < len; i++) { while (k >= 0 && input.charAt(k + 1) != input.charAt(i)) k = ret[k]; if (input.charAt(k + 1) == input.charAt(i)) k++; ret[i] = k; } if (len % (len - ret[len - 1] - 1) == 0) { return len / (len - ret[len - 1] - 1); } return 1; } }
浙公网安备 33010602011771号