【字符串系列】字符串匹配中的位并行算法

最近一段时间看了一点”柔性字符串匹配”, 发现位并行算法在字符串匹配这个领域还是很有用的, 下面抒发一下鄙见.

首先, 字符串位并行算法在acm界用的貌似并不多, 这跟算法本身的局限和人们对算法的了解有关.

字符串位并行算法受限于机器字长, 所以不能用于模式串长度超过机器字长的情况, 这局限了该类算法的推广.

但是由于位并行算法思想比较简单, 一般易于实现, 而且, 位并行这种手段本身也能保证算法性能相当可观.

在字符串中的位并行算法, 基本是以单字符串匹配中的ShiftAnd算法和BNDM算法为基础, 扩展衍生的, 延伸到了多字符串匹配, 扩展字符串匹配和正则匹配里面.(这里的分类依据柔性字符串匹配)

做一些变量的声明, 模式串p, p的长度是m, 文本是t.

ShiftAnd算法, 是单字符串匹配里面的一种前缀匹配算法. 算法维护一个字符串的集合, 集合中的每个字符串既是p的前缀, 同时也是已读文本的后缀.

而在kmp算法里面, 就是用一个预计算出来的next数组, 来求已读入文本同p的最长后缀, 而ShiftAnd算法里面的集合也是完成类似的功能.

而, 二进制位是表达集合的一种很好的方式, 只要这样表达恰当的话.

用一个位掩码D来表示这个集合, D的第j位被置为1, 当且仅当p1…pj是t1…ti的后缀, 而当Dm也是1的时候, 就是说, p本身是t1…ti的后缀, 找到了一个匹配.

那么在从t中读入一个新的字符时, 如何更新这个后缀集合(即位掩码D)呢?

首先构造一个表B, 记录字母表(a-z,A-Z)里面每个字符的位掩码, 如果pj==c, 则掩码B[c]的第j位是1, 这里的1, 用于后面说的”与”运算, 这也正是ShiftAnd算法的得名.

《【字符串系列】字符串匹配中的位并行算法》

上面这个式子, 就是集合更新过程的本质, 每读入一个新字符t(i+1), 就利用上面这个公式对位掩码D进行更新.

式子中D<<1, 是要利用读入的新字符, 来构造更长的后缀, 如果能一直构造出更长的串, 那么最终Dm==1, 就找到了一个匹配.

利用B[t(i+1)]和移位后的D的&运算, 来找那些满足t(i+1)==p(j+1)的位置并置为1.

位运算可以在常数时间内完成, 那么ShiftAnd的时间复杂度就是O(n).

下面是伪代码, 来自<<柔性字符串匹配>>:

《【字符串系列】字符串匹配中的位并行算法》

其实上面的东西大部分就是从”柔匹”里面抄来的 XDD。

ShiftAnd算法的一个变体, 就是ShiftOr算法, 你可以看到上面的那个式子里面, D以为的同时, 与1进行了”或”运算, 这是因为空字符串也是文本的后缀, 即空字符串无条件地可以匹配.

而ShiftOr就是通过使用对位去翻去掉了这个”或”运算的过程,详情见”柔匹”原书.

在”柔匹”里面, 对单模式串匹配分了3类, 前缀匹配(如kmp和ShiftAnd), 后缀(如BM), 子串(如BNDM, BDM), 当然, 对于多模式串这个分类也适用.

但是, 后缀匹配分类里面却没有位并行算法的影子, 这是为什么?

那么, 首先, 字符串算法为什么存在? 为了加速匹配, 尽量在趋近O(n)的时间复杂度内完成匹配, 那么kmp里面的next或者ShiftAnd里面的集合的作用呢, 是为了加速搜索窗口的移动.

可以理解为, ShiftAnd就是kmp的一个位并行版本, 因为两者都在找p和已读入文本的最长后缀.(关于两者的实质区别, 可以从自动机的角度来考虑.)

那么反观后缀匹配里面的BM算法, BM本身比较复杂, 即使d1(后缀u在p中出现的位置)可以利用位并行也没什么好说的.

而Horspool没有利用d1, 即后缀u在模式串p中的出现的位置, 而另外的两个d的计算基本不需要用位并行算法.

改天再继续介绍一下BNDM(子串匹配里面的位并行算法)..

在讨论BNDM之前, 从自动机的角度来讨论一下并行算法, 其实并行算法在做的就是个NFA(非确定性自动机), 用位掩码的方式来表达不同的跳转状态, 而通过位运算的方法, 就可以轻易实现多个状态同时进行跳转. 本来NFA就比DFA好理解, 但是NFA编程不好操作, 所以有了DFA, 而位并行算法却可以在一定程度上很好地解决这个问题…

BNDM算法,其实实现手法, 跟BM里面求d1的方式一样, 利用位并行, 构造出来的自动机, 可以找到已读入的后缀u在p里面的所有子串, 用位1标示.

那如果读入的后缀的长度==p的长度了, 而且子串集合不为空(即位掩码最高位是1), 那么就是找到了一个匹配.

下面我最想贴出来的就是不同算法的性能分析表了:

《【字符串系列】字符串匹配中的位并行算法》

解释见”柔匹”.

平均时间复杂度最优的是子串匹配, 即BNDM和BOM.

对于多模式串的匹配, 位并行算法很容易拓展, 这也体现了位并行算法的灵活性.

但是, 有一个前提是, 所有子串的长度和不要超过一个机器字的长度限制.

Multiple Shift-And算法的思想, 就是在一个机器字里面对r个模式串进行Shift-And算法的位操作.

因为位并行可以处理NFA中多个状态的同时跳转, 那么处理多个模式串的匹配自然也是不在话下, 只是需要特殊的掩码来测试一下哪一个模式串被匹配到了, 其实这个掩码就是Shfit-And里面测试掩码的拼接而已.

但是, 却没有Multiple Shift-Or算法, 因为, Shift-Or需要在末尾给掩码引入0, 可以构造出一个掩码 , 然后拼接一下, 但是这样就没有Shift-Or原来那种只通过移位就引入0的意味了, 也就是, Multi Shift-Or没有存在的意义, so…

单模式串中的子串匹配也可以扩展到多模式串来, 这就是Multiple BNDM, 基本思想也是对于子串匹配的拼接, 不赘述了XDD.

 PS: 在模式串完全随机的情况下, 其实暴力匹配是最快的, 因为可以做到概率上为O(n)的效率, 而一般的匹配算法都会有比较大的常数.

下面是证明:

假定text的长度是n, 而模式串的长度是p, 设字符集为S, |S|=s.

对于text里面的某个位置i, 从i开始匹配1个格的概率是1/s, 匹配2个格的概率是1/s^2, …, 匹配p个格的概率是1/s^p.

那么对于i位置, 如果这个位置匹配失败的话, 需要的代价是, 1+1/s+1/s^2+…+1/s^(p-1)+(s-1)/s^p, 而这是个常数.

对于某个位置, 从这个位置匹配失败的代价是O(1), 那么最坏情况, 就是跑完了整个text, 代价是O(n).

而剩下的就是比较哪个算法的常数更大的问题了, 而在绝对随机的情况下, 一般是暴力的常数最小~!~~

~~~

点赞