中间跳过了几章,先看自己认为比较容易看懂了几章,结果发现,证明真是难呀。虽然没有怎么看过其他的算法书,但是觉得算法导论虽然在证明,把问题形式化方面稍微有点罗嗦了,但是感觉还是不错了,它不会直接抛给你一个最有效的算法,然后直接跟你讲,它会从最朴素的算法逐渐讲更有效率的算法,这样让读者对问题有更清晰的把握,而且有些高效率的算法往往是建立在朴素的算法上的。字符串匹配就是这样,朴素算法-自动机识别法-KMP算法。Rabin-Karp算法看得我有点头昏脑胀,暂时先放一下。
字符串匹配问题问题很明白。就是给定模式串P,去匹配串T中找P是否出现,返回匹配时T位置的偏移。
我们设P长度为m, T长度为n , Σ 字母表
1.朴素匹配算法就是从T的第一个字符逐个跟P比较,当出现不匹配时,向右移动一位,在重新和P开始比较,T中的每一位都有机会和P从头开始比较,但它的效率并不高,T中的n-m+1位都要比,而且在此位进行匹配时最坏情况下都要比较m个字符,所以最坏情况下运行时间是(n-m+1)*m
如何改进呢,就是过滤掉那些无效的偏移。
2.自动机法就是利用构造的转移函数,一步过滤掉了所有的无效偏移,但是构造自动机转移函数的代价可能会比较大。下面简要的说明一下自动机,自动机算法的证明还是看得比较明白,后面KMP算法的证明hold不住了。
关于自动机的定义大家看书,或者随便找本编译原理的书都有。有限自动机M(Q,q0,A,Σ,δ)
在这里构造的自动机是基于模式P构造的,
状态集合Q一共有m+1个状态,状态为0,1,2,3,4....m , q0=0为起始状态,A=m为接受状态。 δ为状态p遇到字符a转移到状态 q的一个映射。 δ(p,a)=q;而在自动机字符匹配里我们把这个转移函数定义为 δ(p,a)=q=σ((Pp)a),其中Pp表示P的长度为p的前缀。
σ() 函数为后缀函数,它的定义是 σ(x) 是字符串x的后缀模式P的最长前缀的长度。 有点拗口,举例如下:
P:a b a b a c a
σ(abdcaba)=3 ,因为x a b d c a b a
x的后缀 aba 为P的最长前缀。长度为3.
然后定义了这个有什么用呢。我们先不管这个转移函数如何计算。来看自动机如何工作的,自动机是从状态q0出发,读入字符,根据状态转移函数进行相应的转移。现在我们的自动机输入文本为T,依次输入的是T1,T2,T3......T[i]。 现在我们来看根据我们定义的转移函数Ti的状态会是多少,定义Φ(Ti)为字符串Ti读入后所处的状态。
现在我们证明Φ(Ti)=σ(Ti)。证明了这个我们就可以看到自动机是如何工作的了。
PS: Ti 表示T的前i个字符组成的字符串,T[i]表示T字符串的第i个字符
进行归纳 i=0; T0=空 σ(空)=0 , Φ(T0)=0 成立,,就是初始状态。一个字符都没有读入。
假设Φ(Ti)=σ(Ti) 来证明Φ(Ti+1)=σ(Ti+1) , 状态p为Φ(Ti), 字符a为T[i+1] 。
Φ(Ti+1)=Φ(Ti a) (根据Ti+1的定义,读入T[i+1]个字符后所处的状态)
=δ(Φ(Ti),a ) (根据Φ的定义)
=δ(p,a) (根据p的定义,它为Φ(Ti))
=σ(Pp a)(根据转移函数σ的定义)
=σ(Ti a)()
根据假设 p=Φ(Ti)=σ(Ti)
Ti: T1T2T2 .... T[i-p+1]. . . T[i-2] T[i-1] T[i] a
Pp: P[1]P[2]........... P[p] a
上下都加一个字符a 很明显还是相等的。
(接上面)=σ(Ti+1) (Ti+1=Ti+a)
有了Φ(Ti)=σ(Ti) 我们就可以看到只有读入Ti字符串后 状态为m的时候得到一个匹配。这样自动机就可以正常工作了。起始状态为0,读入一个字符根据转移函数进入下一个状态,每次进入下一个状态查看是不是为m,如果是m则得到一个匹配。
而计算转移函数我们暂时就用根据定义暴力搜索的方法吧,具体看代码。可以利用后面KMP相关方法进行改进。
算法导论上的证明看得我晕头转向,一会儿给出转移函数,一会儿又貌似通过证明来推出转移函数。 我觉得自动机方法的得出应该是先有自动机理论,然后想出转移函数,然后证明可行性,这样比较不乱!
具体实现代码:
#include
#include
#include
#include
#include"MyTimer.h"
#define MAXSIZE 1000
using namespace std;
unsigned int status[MAXSIZE][26];
/* σδ
根据给定的模式P 来造自动机,最重要的部分是转移函数的定义 ,P的长度 m
一共m+1个状态,起始状态0,接受状态m
q为任意状态(0-m), a为字符 转移函数 δ(q,a)=σ((Pq)a)
设 σ((Pq)a)=k 含义: P的一个最长前缀P[1....k],且是 P[1...q]a 这个字符串的后缀
*/
void computeTransitionFunction(string p) //O( m^3|Σ| ) 可改进到O( m|Σ| )
{ //通过定义直接计算转换函数 一共有4层循环,所有看上去代价很大,,
int m=p.size();
for(int q=0;q!=m+1;++q){ //从状态0开始计算 一共0-m个状态 m+1 次
for(int j=0;j!=26;++j){ //26个字符 每个都要试 Σ次
int k=min(m,q+1); // 求 δ(q,a)的值, 它最大值为 min(m,q+1)
while(k!=0){ //最多减小到0 返回状态0 最多m+1次
int i;
for(i=k;i!=0;--i){ //逐个查看 P[1..k]是否满足要求 是 P[1...q]a 这个字符串的后缀 m次
if(i==k){ //p[k]要与a比较
if(p[i-1]==char(j+97)){ //相等继续
continue;
}else
{//否则 这个k不符合要求
break;}
}
else{ //继续比较P[1..k-1]和P[1..q]
if(p[i-1]==p[q-(k-i)])continue;
else break;
}
}//for
//如果i减小到了0 则说明这个k符合要求,
if(i==0){ status[q][j]=k;break;} //赋值
else { //否则减小k
--k;
}
}//while
}
}
}
//构造好了自动机 接下来的的匹配就很简单了
void finiteAutomationMachine(string t,int m){ //O(n)
int n=t.size();
int q=0; //初始状态
for(int i=0;i!=n;++i){ //依次读入字符
q=status[q][int(t[i]-97)]; //转移状态
if(q==m)cout< //这个要匹配的字符串外面文件读取的,,大家可以自己指定
int i=0;
char x;
while(!infile.eof()&&infile>>x){
T.push_back(x);
}
string P="fsdfsadsadfadsf"; //模式P
MyTimer times;
times.Start();
computeTransitionFunction(P);
finiteAutomationMachine(T,P.size());
times.End();
cout<
自己回顾知识虽然可以加深理解,但好费时间,有时间再写KMP。
KMP算法利用前缀函数来避免无用偏移,他做的工作没有自动机的转移函数做的彻底,但是也可以达到目的。而且使得预处理的时间降到了O( m)。
计算前缀函数计算方法的证明,和KMP正确性的证明看了几遍也没看明白,只能直接理解代码了。,
1.模式P通过 跟自己的比较计算出前缀函数,pai。
pai[q]表示 P[1......q]的真后缀 且是P的前缀的最长长度
如 P =a b a b a c a
pai[5]= P[1...5] a b a b a
P a b a b a c a
最大长度是3
所以pai[5]=3
知道了这个有什么用呢 在识别字符串T的时候 根据这个信息可以 略去无用偏移
1 23 4 5 6 7 8
T a b a b a a b c
P a b a b a c a
假设此时我们搜索到第6 个字符,结果发现不匹配,
在朴素算法里 仅仅推进偏移到2 ,在自动机匹配里状态5 读入字符a 回到状态1 也就是推进偏移到7,直接比较第
7个字符,而在KMP里我们不管读入的字符a ,先推进偏移5- pai[5]=2 ,无效偏移2被避免 , 继续比较T的第六个字符和P[3+1]字符
1 2 3 4 5 6 7 8
T a b a b a a b c
P a b a b a c a
又不相同的,再看pai[3]=1 ,继续推进,但还是比较第六个字符
1 2 3 4 5 6 7 8
T a b a b a a b c
P a b a b a c a
又不相同,此时pai[1]=0
相当于从T的第六个字符截断,重新开始与P的匹配
所以还是可以看到自动机是一步到位,因为他把比较不相同的那位也考虑了进去,但是提前做的工作很多,而KMP利用pai函数,即时地计算需要的信息也可以达到自动机的效果,且效率提高很多。
前缀函数的计算我只能通过代码来理解,证明就让它随风而去吧。
#include
#include
#include
#include
#include"MyTimer.h"
using namespace std;
void computePrefixFuction(string p,int *pai) //运用摊还分析 O(m)
{ //求解前缀函数 ,证明实在看不懂,看代码吧
int m=p.size();
pai[1]=0; //pai[q]0 而P[k+1]!=p[q] 那么就需要在前面匹配的Pk里继续寻找,也就是说进一步缩小k值
//3.如果k>0 而P[k+1]!=p[q] 那么很简单 匹配的长度又加1 k++
for(int q=2;q!=m+1;++q){
while(k>0&&p[k]!=p[q-1]){ //2
k=pai[k];
}
if(p[k]==p[q-1]){ //1 ,3
++k;
}
pai[q]=k;
}
}
void kmpMatch(string T,string P)
{ //有了前缀函数 匹配过程就相对容易一点
int m=P.size();
int n=T.size();
int pai[m+1];
computePrefixFuction(P,pai);
int q=0;
for(int i=1;i!=n+1;++i) //从第一个字符扫到最后一个字符 O(n)
{ //从上面的图解通过前缀函数的匹配方法,每次比较的都是第i个字符,
//变的是在这个字符之前和P匹配的字符数q, 初始为0
while(q>0 && P[q]!=T[i-1]){ //q大于0 ,而P的第q+1个字符和T的第i个字符不匹配,那么就找
q=pai[q]; //P[1...q] 的后缀,P的前缀的最大匹配,也就是pai[q]
}
if(P[q]==T[i-1]){ //如果q为0 拿P的第一个字符和T的第i个字符比较 相等则q加1 不相等 进入比较下一个字符i+1
++q; //q大于0 且P的第q+1个字符和T的第i个字符匹配,q加1 很简单
}
if(q==m){ //在比较完第i个字符的时候 发现q==m 就是匹配到了一个P
cout<>x){
T.push_back(x);
}
string P="ewqw";
//string P="ababaca";
MyTimer times;
times.Start();
kmpMatch(T,P);
times.End();
cout<