多模式匹配的用法,多了去了! DB 中对 selected patterns 进行数挖;安全中对 suspicious keyword 进行匹配;各种日期形式 2009-5-20 , 2009 年 5 月 20 日 , May,20 的搜索; DNA 配对;各种 replace 功能;等等,太口水了枚举这个。
Wu-Manber 基于 BM 算法思想,如果您佬 BM 还没 OK ,请参照我的 BM 日志 搞搞清楚先。
提到 Wu-Manber ,其实就是 SHIFT 、 HASH 、 PREFIX 三张表,预处理 patterns 先把这三张表填好,搜的过程忒简单。
1. 拿表说事儿:表干嘛用的 |
先有个大概印象: SHIFT[] 就一跳转表,一张记录向右滑动距离的表。当 SHIFT[i]=0 时,说明那么多 patterns 肯定有匹配上的,这时 HASH[] 和 PREFIX[] 就要站出来指明谁暂时匹配上了,并对它们通通匹配一番。
2. 预处理这一堆 patterns :填上三表 |
这堆 patterns 长度不一, m= 最小长度,该算法也就只检测每 pat 前 m 个字符了(那剩余的怎么办呢? o 还没想明白 )。 Patterns 长度基本相当最好,假如有一 pat 掉链子,特短(长度为 2 ),那么每次滑动距离最多也就才 2 ,还滑个什么劲儿呀。所以各 pat 长度要统一要和谐。
我们把 text 切成长度为 B 的块(一般 2 或 3 ), B 怎么算呢?假如有 k 个 pat ,那么各 pat 总长 M=k*m , |∑| 为 c ,那么 B=log c (2M) 。既然这样,那 SHIFT 滑动距离就不由末尾单个字符而由末尾 B 个字符而定了。如果这 B 个字符跟所有 pat 都没匹配上,那么很自然小指针可以后滑 m-B+1 。为什么是 m-B+1 呢?
如上图示,虽然 XAB 不出现在 pat 末,但它的后缀 AB 是 pat 前缀,如果你索性移 m=5 ,那么就错过一个匹配了。其实如果 B 块不出现在 pat 末,那么至多它的 B-1 后缀可能出现在 pat 前缀,因此,安全的移动距离为 m-(B-1)=m-B+1 。这其实是相对保守的策略,最后我们会提出改进算法。
2.1 SHIFT[] |
开 SHIFT[] 表大小为 |∑| B ,因为要组建长度为 B 的 str ,其中每个字符均有 |∑| 种选择。通过 hash function ,每个 str 计算得到一个值 index 而住进 SHIFT 。我们现在扫描 text 的 X=x1…x B , hashFunc(X)=h ,那应该滑动多少呢?
如果 X 跟各 pat 都没匹配上,那么 SHIFT[h]=m-B+1 。如果 X 跟其中某些 pat 匹配上,那么 SHIFT[h]=m-q ,其中 q 是 X 出现在最右的 pat 中(假如 pat j )的 x B 位置,显然它是最小滑动距离。其实,不用等到实际考查 text 时再计算 SHIFT ,我们预处理各 pat 就可以把 SHIFT 表填上。对于每个 pat ,计算其每个长度为 B 的 subpat(a j-B+1 …a j ) 的值, SHIFT[hashFun(subpat)]=min{m-B+1,m-j} 。
哈哈,于是乎 SHIFT[] 里记录了 text 能安全滑过的最大距离,滑的越大当然越快,但是滑的小一点倒也没错。根据这一点,我们可以压缩 SHIFT 表,把不同的 subpat 压缩进一个 entry ,只要值留最小的那个就 OK 了。(在 agrep 这个 Wu-Manber 算法的应用中, B=2 时用的原始 SHIFT 表, B=3 时用的压缩 SHIFT 表)。
2.2 HASH[] |
只要 SHIFT[]>0 ,那尽管滑 text 就好了( 100 个 pat 的应用中, 5% 遇到 0 , 1000 个时候 27% , 10000 个时候 53% )。如果 =0 ,总不至于去跟所有 pat 一一比对看谁暂时匹配上了吧? o 们文明人要用更懒更聪明的方式,去快速锁定那些暂时匹配上的 pat 们。怎么做呢? HASH !
HASH[i] 存放一个指向链表的指针,链表存着这样的 pat (末 B 位通过 hash function 计算是 i )。 HASH[] 表大小同 SHIFT ,但相对就稀疏多了,人那儿存着所有可能的组合的 SHIFT 值。哎,牺牲空间换时间,时空一向两难全。
记现正扫描的 text 的末 B 位的哈希值为 h 。同时引入 PAT_POINT (指向实际 patterns 存储位置的指针链表),该链表按 patterns 末 B 位的哈希值排序,那么 HASH[h] 的值是 p ( p 指向 PAT_POINT 中哈希值为 h 的第一个结点处),此时相当于找到第一个跟 text 末 B 位匹配上的 pat ,那么进行匹配,如果匹配不上就继续 p++ ,一直到 HASH[h+1] 指向的那处地址截止。以上是 SHIFT[h]=0 的情况。对于 SHIFT[h]≠0, 那么 HASH[h]=HASH[h+1] ,因为没哪个 pat 的末 B 位能匹配上,自然这两个 HASH 值应该相等(开始就是结束)。这样,就填好整张 HASH[] 表了。
2.3 PREFIX[] |
如果只有 HASH[] 表,就囧大了。例如自然语言 text 中以 ing , ion 结尾的单词非常多, pat 中出现 ing/ion 结尾的也非常多,如果按 HASH[] 的方法,那就得 HASH[h]~HASH[h+1] 的 pat 一个个匹配。能不能更快些呢?引入 PREFIX[] 。
对每一个 pat ,除了记录其末 B 位字符的哈希值( PAT_POINT ),我们还要记录其首 B’ 位字符的哈希值( PREFIX ),一般取 B’=2 。这是一种有效的过滤手段,因为既末 B 位相同前 B’ 位也相同的 pat 很少,这样就没那么多 pat 需要去匹配了(不像上面那种要一一匹对)。但是也需要权衡啦,因为你计算 PREFIX 哈希值需要时间,存储它需要空间,换回 “ 一一匹配 ” 的时间,不知划不划算。
3. 匹配过程 |
Step1. 现正扫描的 text 的末 B 位 t m-B+1 …t m 通过 hash function 计算其哈希值 h 。
Step2. 查表, SHIFT[h]>0 , text 小指针后滑 SHIFT[h] 位,执行 1 ; SHIFT[h]=0 ,执行 3 ;
Step3. 计算此 m 位 text 的前缀的哈希值,记为 text_prefix ;
Step4. 对于每个 p ( HASH[h] ≦ p<HASH[h+1] )看是否 PREFIX[p]=text_prefix 。如果相等,方才让真正的 pat (即 PAT_POINT[p] )去和 text 匹配。
计算 SHIFT[],HASH[],PREFIX[]; // 开始匹配 while (text<textend) { hashVal=hashBlock(text);// 计算当前块的哈希值 // 查找块的坏字符移动表( SHIFT )得到下一个匹配开始位置 shift_distance=SHIFT[hashval]; // ③ if (shift_distance==0) // 当前块出现在某 pat 末 { shift_distance=1; // ① p=HASH[hashval]; // 得到可能与当前块匹配的所有 pat 的集合的开始位置 while (p) 检验子集中的 pat 是否匹配 ; } text+=shift_distance; // ② 选择下一个可能的匹配入口 } |
这里用 C 给出 main() ,源码( glimpse 代码的一部分)可以从 FTP 下载: cs.arizona.edu
复杂度 O ( BN/M ), B 、 M 含义同前, N 是 text 字符数。 Patterns 很短或很少的时候, Wu-Manber 不是很牛叉。而成千上万的 patterns 一起匹配过去, Wu-Manber 牛气冲天了。
3. Wu-Manber 总结与改进 |
一言以闭之: WM 是 BM 处理多模式的派生形式。用的 BM 算法框架,用块字符来计算的坏字符移动距离( SHIFT[] );在进行匹配的时候,用的 HASH[] 选择 patterns 中的一个子集与当前文本进行匹配,减少无谓的匹配操作。 WM 不会随着 patterns 的增加而成比例增长,它远少于使用每一个 pat 和 BM 算法对文本进行匹配的时间总和。
改进 1 :
但是,上文提到过安全的移动距离最大是 m-B+1 ,这是相对保守的策略。其实像图 1 那种情况的出现次数相对于坏字符情况的出现次数,那真是小乌见大乌了。所以,一次只滑 m-B+1 多慢呀,能滑 m 才够快够刺激。呵呵,怎么办呢?想想 BM 中怎么做地。我们将 SHIFT 作为坏字符转移表,这样算:
这样,坏字符转移函数的值域 0<=y<=m ,而不是原来的 0<=y<=m-B+1 了。下表是 SHIFT 表的两个版本的实现。 SHIFT 表空间不变,时间上多了个 for 循环,总时间是 O(B*|∑|+M) , |∑| 是块集中块的个数。
原始实现:
用 m-B+1 填写 SHIFT 表 ; for each pat { for pat 中每一个块 计算 SHIFT[Bc]; }
| 改进的实现:
用 m 填写 SHIFT 表 ; for (i=1;i<B;i++) { 对所有 Bc ∈ [suffix(Bc,i)=prefix(pat,i)] SHIFT[Bc]=m-i; } for each pat { for pat 中每一个块 计算 SHIFT[Bc]; } |
改进 2 :
WM 算法一旦找准匹配入口点,就开始进行 “ 逐个 ” 的比较,能不能用 “ 好后缀 ” 呢?呵呵,可以。引入 GBSShift[] ,该表记录了每 pat 的末 B 位在所有 patterns 中的所有非后缀出现位置与相应 pat_end 的距离的最小值。这样就可以快速确定滑动距离。
GBSShift[] 与 SHIFT[] 大小相同, |∑| 。 GBSShift[] 和 SHIFT[] 表计算近似,不需额外计算工作,只需复制计算出的 SHIFT[] 表计算结果,所以它所需的额外时间是 O( |∑| ) 。将 WM 伪码中的 // ① 改为 shift_distance=GBSShift[hashval] 即可。
结合改进 1 、 2 ,改进算法在处理大规模数据时比 WM 算法所用时间养活了 8~15% 。
盘点一下:
AC 算法被用于 fgrep1.0 (在 UNIX 中通过 -f 使用)
BM_AC 算法在 gre 中应用,并被 fgrep2.0 收用。
BMH_AC 算法
Wu-Manber 算法在 agrep 中应用,并被 glimpse 收用。
学习自:“A Fast Algorithm For Multi-Pattern Searching”和“一种改进的Wu-Manber多关键词匹配算法”