#1015 : KMP算法
描述
小Hi和小Ho是一对好朋友,出生在信息化社会的他们对编程产生了莫大的兴趣,他们约定好互相帮助,在编程的学习道路上一同前进。
这一天,他们遇到了一只河蟹,于是河蟹就向小Hi和小Ho提出了那个经典的问题:“小Hi和小Ho,你们能不能够判断一段文字(原串)里面是不是存在那么一些……特殊……的文字(模式串)?”
小Hi和小Ho仔细思考了一下,觉得只能想到很简单的做法,但是又觉得既然河蟹先生这么说了,就肯定不会这么容易的让他们回答了,于是他们只能说道:“抱歉,河蟹先生,我们只能想到时间复杂度为(文本长度 * 特殊文字总长度)的方法,即对于每个模式串分开判断,然后依次枚举起始位置并检查是否能够匹配,但是这不是您想要的方法是吧?”
河蟹点了点头,说道:”看来你们的水平还有待提高,这样吧,如果我说只有一个特殊文字,你能不能做到呢?“
小Ho这时候还有点晕晕乎乎的,但是小Hi很快开口道:”我知道!这就是一个很经典的模式匹配问题!可以使用KMP算法进行求解!“
河蟹满意的点了点头,对小Hi说道:”既然你知道就好办了,你去把小Ho教会,下周我有重要的任务交给你们!“
”保证完成任务!”小Hi点头道。
输入
第一行一个整数N,表示测试数据组数。
接下来的N*2行,每两行表示一个测试数据。在每一个测试数据中,第一行为模式串,由不超过10^4个大写字母组成,第二行为原串,由不超过10^6个大写字母组成。
其中N<=20
输出
对于每一个测试数据,按照它们在输入中出现的顺序输出一行Ans,表示模式串在原串中出现的次数。
样例输入
5 HA HAHAHA WQN WQN ADA ADADADA BABABB BABABABABABABABABB DAD ADDAADAADDAAADAAD
样例输出
3 1 3 1 0
提示和题目连接:http://hihocoder.com/problemset/problem/1015
解答:
通过查阅资料发现了四种进行字符串匹配的算法,分别为朴素算法、Rabin-Karp、有限自动机算法、Knuth-Morris-Pratt算法
设定已知条件:已知模式P[1..m]需要匹配的文本T[1..n]
首先说朴素算法,朴素算法真的很朴素,就是通过一个循环来寻找有效的移位,该循环对n-m+1个可能的每一个移位值s进行匹配检查P[1..m]=T[s+1..s+m]
算法伪码:
NAIVE-STRING-MATCHER(T,P) n <- length[T] m <- length[P] for s <- 0 to n-m do if P[1..m] = T[s+1..s+m] then print"找到一个匹配的模式"
下面暂时略过Rabin-Karp算法,回头补上,
有限自动机算法:
这个算法的步骤就是根据模式P生成一个状态变迁函数,也可以绘出一个状态变迁图
例如模式P = {ababaca}就是可以定义7个状态
注:对于长度为n的字符串模式,一般定义n+1个状态,其中包含一个初始状态和一个接受状态,在接受状态意味着一个匹配的成功。
继续上面模式P的变迁图建立:
这个是对应于模式P={ababaca}的状态转换图,图中浅蓝的标号0的点为起始状态,红色的标号为0的点为接受状态,图中未画出的输入对应的线均指向状态0.
在状态0位置,输入a时,此时字符串a的最长后缀对应P的最长前缀字符串a,长度为1,状态转移到1;输入b时,对应的最长前缀字符串长度为0,c时也是
在状态1位置,输入a时,此时字符串aa的最长后缀对应P的最长前缀字符串为a,长度为1,状态转移到1;输入b时,此时字符串ab的最长后缀对应P的最长前缀字符串为ab,长度为2,所以状态转移到2;当输入c时,字符串ac的最长后缀对应P的最长前缀字符串为空,长度为0,状态转移到0。
......
在状态7位置,到达接受状态,即此时已经出现了一次成功匹配,开始下一轮,再输入一个字符a时状态转移到1,输入b时状态转移到2
上述即为有限状态机的建立过程,由此可知,当对应模式P的有限状态机建立后,对于文本T的判断是线性的,只需要对于每一个字符输入后的状态变迁,每次到达接受状态就完成一次成功的匹配。
KMP算法:
KMP算法的关键在于找到模式P的前缀函数next。
在此以模式P={ababababca}为例,阐述一下KMP前缀函数的建立意义。
考察朴素的字符串匹配算法的操作过程,当上述模式当中前四个字符匹配成功后,如果第五个字符匹配失败,说明第文本中对应的第五个字符不是a(建议在纸上画一下),还说明了对应的四个字符为abab,将P右移一个位置发现仍然不匹配,右移两个、三个、四个也是,但是右移五个未必。因此能否不像上述方法那样一步一步右移,而是直接右移五个位置开始进行判断。
其实在上面匹配过程当中,每次成功的匹配就包含了一定量的信息,而分析模式当中ab的重复也可给人以匹配失败后,可否每次移动两个位置再进行匹配的启发,这样充分发掘模式本身的特点可以建立一个next函数,从而确定每次匹配失败后移动的长度。
对于上述模式P发掘的next函数为
伪代码如下:
#include <cstdio> #include <iostream> #include <algorithm> #include <cstring> #include <string> #include <vector> using namespace std; int KMP(string t, string p){ int n = p.size(); vector <int> next(n+1, 0); for(int i=1; i<n; i++){ int j=i; while(j > 0){ j = next[j]; if(p[j] == p[i]){ next[i+1] = j + 1; break; } } } int ans = 0; int m = t.size(); for(int i=0, j=0; i<m; i++){ if(j < n && t[i] == p[j]) j++; else{ while(j > 0){ j = next[j]; if(t[i] == p[j]){ j++; break; } } } if(j == n) ans ++; } return ans; } int main(){ string t, p; int n; scanf("%d", &n); while(n--){ cin>>p>>t; cout<<KMP(t, p)<<endl; } return 0; }