AC自動機中,轉移的最小單位是一個字符。也就是說,匹配後只能移動一個字符,複雜度是線性的O(n)
。然而線性並非最快,Boyer-Moore算法在匹配後可以跳過多個字符,比線性還快。據說在實踐中,利用Boyer-Moore優化的AC自動機總是更快。
來熟悉一下Boyer-Moore算法的基本思路。假設模式串的長度爲m
,母文本爲t。算法不是去母文本中找模式串,而是在模式串中從右到左找文本的第 m個字符tm。如果沒找到,那麼就可以在母文本中跳過m個字符,繼續搜索t2m。如果找到了,比如說是模式串的第2個字符,那就可以跳過m−2個字符,繼續搜索t2m−2,以此類推。ti恰好與模式串尾部匹配的時候,再比較剩下的ti−1⋯tt−m,直到這m個字符都匹配上。該算法可利用下圖演示(二進制串匹配,白色代表0,綠色代表1
):
上例在匹配下標5
後直接快進了3
個字符。
Wu Manber利用了Boyer-Moore的思路,將該算法拓展到多模式匹配。
預處理
第一步要算出所有模式串上的最小長度m
,然後先考慮每個模式串的前m個字符。如此所有模式串長度都一樣了。注意如果最短模式串非常短,比如長度爲1,則算法不可能跳過2
及以上個字符,效率變低。
如果每次比較不侷限於1
個字符,而是比較B個字符,則比較次數可以減小到1B。同時每次在模式串位置i匹配上了之後可以跳過的字符數減小到m−i−B+1,都不匹配時i=0
。
SHIFT表的構造
用一個SHIFT表儲存匹配後最大可以跳過的字符數,將每個長B
的“子串”哈希到一個整數,對應SHIFT表中的下標。那麼SHIFT表的大小理論上是∥Σ∥B,其中Σ
是字符表。
SHIFT表還可以理解爲,後綴作爲子串在模式串中離尾部的最短距離(上圖爲3
)。
記正在掃描的B
個字符爲X=x1⋯xB,並且X被哈希到i,在所有模式串從右到左尋找X
,則會發生兩種情況:
所有模式串都不含X
此時可以跳過m−B+1
,將其存入SHIFT[i]中。
存在包含X
的模式串
找到X
在這些模式串中的下標中的最大值(也即最右位置)
q,將
m−q存入SHIFT[i]。
爲了得到這樣的SHIFT表,只需枚舉所有模式串(的前m
個字符)中長
B的子串
aj−B+1⋯aj,將
m−B+1(子串位於模式串首部之外,即不含該子串)和
m−j(子串位於模式串的下標
j處)中的較小者存入SHIFT表即可。這個值代表最少需要移動多少個字符來“對齊”這個子串,大於這個值的話會遺漏某些模式串,小於等於這個值則是安全的。
SHIFT表的壓縮
考慮到SHIFT表可能很大,現在看看如何壓縮。SHIFT表的定義是匹配子串時最大可以跳過的字符數,如果這個值比精確值大,算法會出錯;然而小一點則不會出錯,只會降低效率。於是可以將一些子串放到同一個下標中,只需將SHIFT值設爲它們的SHIFT值的最小值。實踐中在模式串很少的時候,使用B=2
、精確形式;在模式串很多的時候使用
B=3、壓縮形式。
HASH表的構造
在SHIFT[h]=0
的時候(尾部
B個字符匹配成功,不應該跳過,也就是說匹配到了公共後綴)需要找到那些以該子串結尾的模式串,複用SHIFT表的哈希函數,製作另一張HASH表,值爲以該子串結尾的所有模式串。HASH表比SHIFT表稀疏(因爲只儲存後綴),可以考慮只利用哈希值最後幾個比特得到更緊湊的結構。
記h
爲哈希函數的輸出值,將所有模式串按後綴的哈希排序,那麼必然有一些連續區域的哈希值是相同的,也就是說這些區域共享長
B的後綴。將排序後的模式串記錄爲鏈表,其中的指針記爲
p。那麼HASH表格就是以索引連續區域(子鏈表)爲目標構造的結構。
SHIFT[h]=0
時,此時
HASH[h]指向一個子鏈表的首部
p,不斷遞增
p直到
p+1等於
HASH[h+1]時即可得到子鏈表的尾部元素。
PREFIX表的構造
由於自然語言中的單詞經常共享後綴,比如ion或ing。這會導致HASH表中索引的模式串分佈非常不均勻,產生大量衝突。極端情況下可能所有模式串都被映射到同一條目中。此時必須對所有模式串逐一比對,降低了效率。爲了解決這個問題,引入了另一個PREFIX表。
在上一節中HASH表維護的是長B
的後綴,類似地PREFIX維護的長
B′的前綴。HASH表除了索引模式串本身外,還索引了模式串長
B′的前綴的哈希值。在母文本與模式串的後綴匹配的情況下,先用HASH表得到所有後綴相同的模式串,然後用母文本長
m的窗口移動
m−B′得到前綴,去PREFIX表得到哈希值,用這個哈希值過濾一下,剩下的就是需要逐一匹配的模式串。
事實上PREFIX表不是算法必須的,特別是在一些公共後綴不多的情況下。PREFIX表其實也不是一張Key-Value表,只是一個哈希函數而已,記作PREFIX(x)。
匹配
其實匹配的過程在預處理環節已經提到不少,正是因爲匹配時要用到,所以才需要這3個表格的預處理。歸納起來,匹配過程的主循環可以描述爲如下4步:
計算母文本中當前長B
。
檢查SHIFT[h]:如果>0
則跳過SHIFT[h]個字符並轉到1;否則,轉到3。
計算當前位置往左m
的長
B′的前綴的哈希值,記爲text_prefix_hash
檢查HASH[h]≤p<HASH[h+1]
區間內的p是否有PREFIX(p)=text_prefix_hash,當兩者相等時,進一步直接比較模式串與這段來自母文本的子串。當它們完全匹配的時候,就找到了一個模式串。無論找到與否,都將當前位置右移1個字符,並跳轉1。
Oh et al. (2014)舉了個例子:
這裏m=5,B=B′=2
,當前正在匹配的後綴是nb,前綴是um。由於在所有模式串中,nb離尾部的距離最小爲
2,所以SHIFT[nb]=
2,跳過
2個字符;此時後綴變爲er,前綴變爲an。後綴er的SHIFT值爲0,檢查一下HASH表中具有公共前綴的模式串{anber, ander, ancert},發現anber完全匹配。輸出anber後,當前位置移動
1個字符,跳轉
1。
事實上,在後綴匹配成功後,總是隻能跳1
個單位,依然不夠快。另外,HASH表已經夠費內存的了,額外再加一個PREFIX表,雙倍內存。
複雜度
P
個模式串,
M=mP是所有模式串的總長度,最多
P=M/m個子串對應同一個
SHIFT值i(極端情況下所有模式串在位置i的子串都相等)。長B的子串最少有M個(極端情況下B=1,所有模式串長度都爲1。這是我的理解,與論文給出的2M不同,我舉出的反例如上所述)。所以隨機挑一個子串,它的SHIFT值爲某個特定值i的概率小於兩者之比1/m。
哈希函數的複雜度是O(B)
,母文本長度N,不跳轉的情況下(i=0)複雜度爲三者乘積O(BN/m);跳轉的情況下,平均SHIFT值爲1+⋯+m−B+1m=O(m2),複雜度爲哈希複雜度乘以文本長度除以平均SHIFT值,也是O(BN/m)。所以Wu Manber是總體複雜度就是O(BN/m)。
變種
爲了解決SHIFT[i]=0
時無法跳過更多字符的問題,
Oh et al. (2014)提出對所有這樣的後綴額外記錄一個SHIFT值,代表該後綴第2小的SHIFT值,稱爲auxiliary shift(ASHIFT)。匹配成功後按ASHIFT快進。
爲什麼可以跳過這麼多呢?因爲SHIFT[i]
的定義保證了所有模式串在跳過的區間內不會含有該後綴i。當SHIFT[i]=0時,最短距離爲0已經考慮了,接着跳過所有模式串中i離尾部的第二短距離,當然是安全的。
ASHIFT表的構造
在構造SHIFT表的過程中,當新的SHIFT[i]=0
時,如果ASHIFT[i]≠NULL,用舊的SHIFT[i]和ASHIFT[i]中的較小者更新ASHIFT[i]。如果說SHIFT[i]儲存的是後綴作爲模式串的子串離尾部的最短距離的話,ASHIFT[i]儲存的就是第二短的距離。這個過程可以用下面兩張圖描述:
m=5,B=1
,一共兩個模式串。
此時第二短的距離是3
,來自第一個模式串。
此時第二短的距離是1
,來自第二個模式串(最短距離來自相同的模式串)。
回過頭來看對上一個例子的加速,由於所有模式串的前5
個字符都以
er結尾,所以
ASHIFT[er]=m−B+1=4:
在匹配了er之後不再只跳過1
,而是可以安全地跳過
4個字符:
Early Decision Method
在步驟4檢查HASH[h]≤p<HASH[h+1]
區間內的p是否有PREFIX(p)=text_prefix_hash時,可以通過預先排序PREFIX(p)。檢索有序列表比無序列表快。即使後綴和前綴都匹配上了,剩下的片段也可以預先排序。有序列表上的順序檢索可以early stopping。
總結
Wu Manber算法理論上複雜度爲O(BN/m)
(1≤B≤m=min(strlen(M))),與AC自動機的O(N)相比,只有在特定條件下(B≪m)才能體現出優勢。這個特定條件很苛刻,要求模式串不能太短。而在自然語言處理的場景下,經常有單字作爲模式串的情況,此時Wu Manber無法跳過多個字符,沒有優勢。另外,漢語最常見的詞語長度爲2,也限制了該算法的使用。
另一方面,Wu Manber所依賴的哈希表則帶來了很大的內存負擔,如果哈希函數複雜度本身很高,更加得不償失。
題外話,算法研究沒有止境,再簡單的問題,也有一條歷史悠久的進化路線與錯綜複雜的變種。