打破思维断层之最优美的BNDM
LINK: http://wlh0706-163-com.iteye.com/blog/1858230
BNDM
目的:
本篇博客以BNDM算法为载体,意图在减少思维断层情况下了解算法思想。
目录:
1:其他算法回顾
2:BNDM算法介绍
3:构建辅助表B
4:容器创建和更新
5:过程展示
1:其他算法回顾
在众多单字符匹配算法BF、KMP、Shift-And/Or、BM、Horspool、Sunday,个人最喜欢Shift-And/Or,尽管它不是效率最高的,但是从今以后我更喜欢BNDM。
其实,自己对BM、Horspool和Sunday算法并没有什么太多好感。如果唯一觉得好的,估计就是他们让大家的思维产生了跳跃,这个实际上是BM的功劳,但不得不说后两个对BM的简化,让这个跳跃思维更加有魅力。
在BM中,它利用了两个启发性规则:好后缀规则和坏字符规则。这个过程其实很繁琐,这不要紧,关键是作者将好后缀和坏字符进行了拆分(其实KMP中也是拆分了),根据这两个个体分别建立规则,然后会发现在利用两个规则的时候总觉得不伦不类,缺少整体性,让人觉得不爽。
Horspool和Sunday其实摒弃了两规则,但是他们采用的规则和坏字符规则类似,实则对字符的随机性进行了利用,当匹配失败的时候用最后一个字符或者最后一个字符的下一个字符进行匹配,但是这个对比较过的好后缀和坏字符并没有加以利用,其实是难以加以利用。
如果说Shfit-And/Or是对“位并行”的首次展示就让人惊讶,那么BNDM则利用“位并行”大放异彩令人叹服。
下面做一个简单回顾:
字符串匹配的时候,是在目标串中寻找是否包含模式串的过程。
BF算法:最容易想到
查找的过程:指针i和j首先分别指向了目标串和模式串的首字符。当i和j分别指向了目标串和模式串的e与u的时候,发现不等,此时BF采取简单的策略是:i回溯指向了n,j指向了模式串的首字母i,再次从头开始匹配。这种算法思路最简单,效果当然也是很差,原因在于并没有好好利用已经匹配的结果。
KMP实现了指针不回溯前进。
当目标串指针i指向y(此时i=8),模式串指针j指向d的时候(此时j=8),发现不等;KMP算法并没有采取BF的策略只是简单的把i设为1,j设为0,从头开始比较,而是移动了5步,i=8(不变),j=3,使得模式串的abc对准了目标串的abc,如上图。因为BF比较的时候中间4步是没必要的。实际上KMP利用了已经比较的字符abcttabc的特性:abc既是前缀又是后缀。效率得到改善。这样指针i就不用回溯了,算法效率是o(n)。KMP只是利用了已经匹配成功(abcttabc)的结果,并没有关心匹配失败的y和d。
Shift-And/Or:利用神器“位并行”改善KMP。
http://wlh0706-163-com.iteye.com/blog/1845919
BM:首次实现指针跳跃前进
BM采取从后向前比较,
第一次F和T比较,不等;F称为坏字符,检查模式串中发现没有F,移动7
第二次-和T比较,不等;-称为坏字符,检查模式串中发现有-,移动4,对齐。
第三次当比较到L和A的时候,L是坏字符,由于模式串中没有L,移动6;T是已经匹配的好后缀,根据好后缀需要移动3;取两者中大的6.
第四次当比较-和H的时候,根据坏字符-需要移动2步才能对齐;根据好后缀AT需要移动5才能将模式串中AT和目标串的AT对齐。
BM就是这样比较了14次就成功匹配了。
BM利用了已经匹配的结果(好后缀)和没有匹配成功的(坏字符),但是分开利用的。
Horspool和Sunday将指针跳跃前进的效率大大提高。下面以Horspool为例
此时并没像BM一样,而是直接就根据最后一个字符n来匹配,将模式串中的字符和n对齐,相当于移动了7步。
Horspool实际只关心是否匹配成功,只要没有成功则按照最后一个字符移动。但是他的效率真的很高,主要是利用了单词中的字符的很随机的性质。
其实,最一般的想法是最好能够同时利用已经成功匹配(好后缀)和没有成功匹配的(坏字符)字符做些事情,当然BM中已经这样做了,但是他是分开利用的,总觉的不是很舒服。
BDM(不是BNDM)的出现则很好的解决了这个问题:基于子串进行匹配,它把好后缀和坏字符当成一个整体来利用。
BNDM则是利用了位并行提高了BDM的效率,就像Shift-And提高了KMP算法的效率一样,只是BNDM是基于后缀匹配,Shift—And基于前缀。
想要了解Shift-And/Or,请参看http://wlh0706-163-com.iteye.com/blog/1845919
算法介绍
BNDM算法维护一个容器,里面记录的是已经成功匹配的所有字符用u表示在模式串中出现位置,这个容器同样是用一个位向量D=dmdm-1…d1来表示。
当读入u的第一个字符u1时,模式串中第2位,第6位(从左到右数,下标从0开始)含有u1,则容器的第2,6位相应设为1;当读入第二个字符u2的时候,如果第1位,第5位让就是u2,则此时设第1位和第5位为1,如果第1位不是u2,则此时只有第5位是1.(可以先看例子,明白的快些)
构建辅助表B
同样,和Shift-And算法一样需要先构建一个辅助表B,B用位掩码记录了某个字符在模式串中出现的位置。
结果:
容器创建和更新
创建:
容器默认值是1m(m个1,m表示的是模式串的字符的个数)
更新:
第1步:查询辅助表B,得到新字符tj的掩码B[tj]
第2步:左移1位
过程展示:
伪代码:
第一次:
第二次:
第三次:
当读取了8个字符后,发现D的值仍旧不是0,表明模式串中存在这由njection这8个字符组成的子串,此时last=j=1。在读取下一个字符n前,D在更新操作:左移1位后,发现D变成了000000000 这表明匹配失败,模式串向后移动last位,即1位。
第四次:
其实,对于last的值,从本案例第三次匹配的时候用到了一次,
由于D是100000000,这个的意思实际上就是KMP中既是前缀又是后缀的字符的判断。已经匹配的字符”injection”其实是目标串的后缀同时又是模式串的前缀,判断后last被赋值为j即1.下一次移动不再是9步而是1步。
附上另外2个案例:第一个是作者论文中的案例
案例1:
目标串Target:abbabaabbaab
模式串Pattern:aabbaab
案例2:
同样运用“位并行”的算法Shift-And/or的出现是在1992年,它的出现将基于前缀匹配的KMP算法的效率大大提高,。但是和BNDM算法相比,不论是效率上还是优美程度上都不可相提并论。当然,Shift-And/or算法一样,这种基于位并行算法会和机器有一定联系(字长),还会有各种各样的算法来解决字长限制的问题,有兴趣可以自己研究BOM算法。
LINK: http://blog.youkuaiyun.com/pandora_madara/article/details/12182475
- #include <iostream>
- #include <cstring>
- using namespace std;
- #define Max 256
- void BNDM( const char* p,const char* t )
- {
- const int lengthT = strlen( t );
- const int lengthP = strlen( p );
- unsigned int B[Max] = { 0 };
- int pos = 0;
- for( int i = 0; i < lengthP; ++i )
- B[p[i]] |= 1 << ( lengthP - i - 1 );
- while( pos <= lengthT - lengthP )
- {
- int j = lengthP - 1;
- int last = lengthP;
- unsigned int D = -1;
- while( D )
- {
- D = D & B[t[pos + j]];
- j -= 1;
- if( ( D & ( 1 << ( lengthP - 1 ) ) ) != 0 )
- {
- if( j > 0 )
- last = j;
- else
- cout << pos << endl;
- }
- D = D << 1;
- }
- pos += last;
- }
- }
- int main()
- {
- char p[] = "annuonce";
- char t[] = "CMP_annual_conference_annuonce_ASA_annuonce";
- BNDM( p, t );
- return 0;
- }
BNDM 算法
LINK: http://www.cnblogs.com/simple-boy/p/3930279.html最近在研究一些字符串匹配算法,也是由于工作上的需要,强力推荐一本书《柔性字符串匹配》,一本很好的书。网上可以随时搜索到。还是说正题吧。
BNDM算法的思想来源于BDM算法思想,类似于shitf-and和kmp之间的区别吧(也不知道是不是准确,有错望大家多指点)。前者都是用位运算模拟后者。好了,那就先介绍一下BDM算法吧!
BDM是基于子串搜索方法,其难点在于怎么搜索子串,书中引入了后缀自动机。对于后缀自动机,我其实没有足够的把握理解它,姑且就当它为一个工具就是了,提供一些状态跳转罢了。现在简单介绍一下它的功能和怎么识别子串。对于这个自动机的实现书上说用一种叫compact suffix tree结构,这些由于时间关系同时这也不是我的重点,故没有去理解。
书中提到后缀自动机的三个性质如下:
第一:字符串u是p(模式串)的一个子串当且仅当p的后缀自动机中存在一条初始状态开始的标号为u的路径。(理解这句话)。
第二:自动机可以识别模式串的所有后缀,从初始状态到某个终止状态的路径上的字符组成的字符串是模式串p的一个后缀。
第三:模式串p=p1p2...pm对应的后缀自动机【用SA(p1p2...pm)表示】可以通过在线的方法在o(0)时间内构建完成,即依次将pj添加到SA(p1p2...pj-1)上,构造SA(p1p2...pj)。
搜索算法:先构建p的反串即pmpm-1...p1的后缀自动机,因为我们是从后向前匹配,搜索模式串的子串。在搜索过程中如果到达了一个终止状态,并且对应的串不是整串p,会得到pmpm-1...p1后缀,即p1p2...pm的前缀,我们将它在窗口中的位置保存在last中,根据性质2,是当前(可不可以更新?不可以!)最长前缀(这是重点)。它是从位置last开始,到窗口末端结束。这种反向搜索有两种结束方式:
第一种:识别子串失败,读入的字符σ,在这后缀自动机的当前状态没有σ的转移。这个窗口向右移动,使它起始位置和last对齐。这样移动窗口不会遗漏任何可能的匹配,(因为我们识别的已经是最长的前缀,不然σ定匹配,因为由于性质2得到的)。
第二种:抵达窗口的起始位置,模式串成功匹配,报告成功,并且像第一种方式移动窗口,使它起始位置和last对齐,因为last最长前缀。
见书本上的图:
理解了BMD算法,现在理解BNDM算法,
BNDM算法和Shift-And算法类似,维护一个集合,这个集合用一个向量D来表示。如果pj...pj+u-1等于u,那么D的第m-j+1位是1,表示p的位置j是一个活动状态。书上的表示这种关系。
表的更新时
当读入新字符σ时,D要更新到D´,D´的一个活动状态j对应于σu在模式串的一个起始位置,也就是说
u出现在模式串的位置j+1,即D的j+1位是活动的,σ在模式串j处出现,从而可以得到表D更新
D´=(D<<1)&B[σ]。
还要注意初始化,因为为了表示空串和模式串的任何位置都匹配,这样D=1m,不然会丢失第一个子串。也可将D大小初始化 为m+1。也可以拆分公式:第一部分D´=D&B[σ],然后在移位D´=D´<<1.其中表B和上面的shift-and类似,可以参考我文章中的shift-and。来理解表B的构建。
见书上伪代码:
自己根据伪代码写的源代码:

1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <cmath> 5 6 using namespace std; 7 8 void matchString(const string& vSrcStr, const string& vPatternStr, vector<int>& voMatchPosVec) 9 { 10 //preprocessing 11 int SrcStrLen = vSrcStr.size(); 12 int PatternStrLen = vPatternStr.size(); 13 unsigned int BitMask[256] = {0}; 14 15 for (int i=0; i<PatternStrLen; i++) 16 { 17 BitMask[vPatternStr[i]] |= 1<<(PatternStrLen-i-1); 18 } 19 20 //searching 21 int Pos = 0; 22 while(Pos <= SrcStrLen-PatternStrLen) 23 { 24 int JPos = PatternStrLen; 25 int LastPos = PatternStrLen; 26 unsigned int DMask = (unsigned int) pow(2.0,PatternStrLen)-1; 27 unsigned int MonitorPos = (unsigned int) pow(2.0,PatternStrLen)-1; //设置防止左移时,高位对判断的影响 28 while (DMask&MonitorPos) 29 { 30 DMask = DMask&BitMask[vSrcStr[Pos+JPos-1]]; 31 JPos = JPos-1; 32 if (DMask&(1<<(PatternStrLen-1))) 33 { 34 if(JPos>0) 35 { 36 LastPos = JPos; 37 } 38 else 39 { 40 voMatchPosVec.push_back(Pos); 41 } 42 } 43 DMask = DMask<<1; 44 } 45 Pos = Pos+LastPos; 46 } 47 } 48 49 int main() 50 { 51 string SrcStr = "aaaaabaaa"; 52 string PatternStr = "a"; 53 54 vector<int> MatchPosVec; 55 56 matchString(SrcStr, PatternStr, MatchPosVec); 57 58 for(vector<int>::iterator Ix=MatchPosVec.begin(); Ix!=MatchPosVec.end(); Ix++) 59 { 60 cout<<*Ix<<endl; 61 } 62 63 system("pause"); 64 return 0; 65 }
BNDM 字符串匹配算法
LINK: http://www.cnblogs.com/dsky/archive/2012/04/26/2470855.html
horspool算法从右往左匹配,它跳转的时候只利用了一个字符的信息,这样使得跳转会比较短,速度比较慢。
BNDM算法是一种跳转时考虑子串的算法。具体实现的时候,为了提高速度,用了跟SHIFT AND一样的技巧。对于字符集里的每个字符,计算它在模式串的哪些位置出现,然后用一个整数表示这个集合。用一个整数D表示当前活跃的状态,第i位为1,表示在模式串i的位置有一个子串匹配到了,子串的具体长度取决于源串匹配的情况。如果D里第m位为1,这时候表示找到了模式串的一个前缀,如果这时候源串也匹配了m个字符,则表示找到了模式串,否则,只是一个子串,这时候要调整一个跳转的长度。跳转的长度为m-这个前缀的长度。每找到一个前缀都要更新一个跳转的距离,所以跳转的距离是越来越短。
1 int BNDMMatch(byte* pSrc, int nSrcSize, byte* pSubSrc, int nSubSrcSize) 2 { 3 unsigned int skip[256] = {0}; 4 for(int i = 0; i < nSubSrcSize; i++) 5 { 6 skip[ pSubSrc[i] ] |= 1 << (nSubSrcSize - 1 - i); 7 } 8 9 int nPos = 0; 10 while(nPos <= nSrcSize - nSubSrcSize) 11 { 12 int j = nSubSrcSize -1; 13 int last = nSubSrcSize; 14 unsigned int D = -1; 15 while(D) 16 { 17 D &= skip[pSrc[nPos + j]]; 18 if (D & (1<<(nSubSrcSize-1))) 19 { 20 if (j > 0) 21 { 22 last = j; 23 } 24 else 25 { 26 return nPos; 27 } 28 } 29 j--; 30 D <<= 1; 31 } 32 nPos += last; 33 } 34 return -1; 35 }