超酷演算法(3):字謎樹

威士忌發表於2015-01-17

我毫不猶豫的把這個演算法稱為“超酷”,雖然我自己發明了它,但我還是覺得它相當的酷,而且它很適合我演算法系列的主題,所以無論如何要把它寫下來。

當涉及到尋找單詞字謎時,用的比較頻繁的方法是字謎字典,簡單得說,對單詞的字母進行排序,以提供一個單詞與所有字謎共同點的唯一索引。另外一種方法是為單詞裡的每個字母生成字母頻率直方圖。(這兩種方法實際上或多或少相同。)這些方法查詢確切的單字字謎字串非常高效 – 如果使用雜湊表,複雜度為O(1)。

然而,如果問題是查詢字謎的子集(包含一個字串裡字母的一個子集的單詞),仍然是相當低效的,通常需要在O(n)時間內暴力搜尋整個字典,或者查詢每個有序字串的子串,複雜度與輸入字串的字母長度有關,為O(2^l)。查詢字謎子集顯然更有趣,因為它能查詢多字字謎,可以應用在拼字遊戲上。

不管怎樣,我們先生成能唯一表示一組字母的直方圖,再努力觀察,我們可以生成一個樹結構來更有效得查詢字謎子集。為了構建這樣的樹,我們按照如下幾個簡單的步驟:

假設我們有如下資訊:

  • 一個詞典或單詞字典來填充樹
  • 詞典中單詞的字母表
  • 一個正在構建的樹
  • 當前節點

詞典裡的每個單詞:
1.為該單詞生成字母頻率直方圖。
2.設當前節點為樹的根節點。
3.每個字母表裡的字元:
獲取當前字元在當前單詞裡的頻率,記為f
設定當前節點為當前節點的第f個子節點,如果它不存在的話就建立
4.將當前單詞新增到當前(葉)節點上的單詞列表

以下是這個簡單過程的結果,它是一棵固定高度的樹,27個節點深,所有單詞都在葉節點中,並且樹的每個層級對應字母表裡的字元。下面是個簡略的例子(譯註:原部落格圖片遺失,從WIKI上找了張替圖):

Image_of_an_anatree1

一旦樹建立好後,我們可以如下方式查詢輸入字串的字謎集合:

假設我們有如下資訊:

由上述流程所構建的樹
上面使用過的字母表
一個邊界集合,初始化為空

1.初始化時邊界集合只包含樹的根節點
2.生成輸入字串的字母頻率直方圖
3.對字母表中的每個字元:
1.獲取當前字元在輸入字串裡的頻率,記為f
2.對邊界集合裡的每個節點,新增標號為0到f的子節點到新的邊界集合中
4.當前邊界集合中包含的葉節點,包含所有輸入字串的字謎子集

至少對我來說,對該演算法進行執行期分析比較困難。直觀的看,它在實踐中比任何一種蠻力演算法要快很多,但我無法量化為大O表示法。作為一個上限,它不可能比O(n)的效率低,最壞也比蠻力演算法少一個常數因子。作為下限值,邊界集合中只有一個節點,那查詢時間就與字母表長度成正比,為O(1)。平均情況下,依賴輸入字串所選擇的字典的子集有多大。以輸出的大小來量化的話,需要O(m)的操作。如果有人知道如何確定執行時更準確的範圍的話,請在評論中讓我知曉。

這個演算法有個缺點就是,需要大量的記憶體開銷。我用python來實現,並匯入/usr/share/dict/words,在本機上這大約是2MB的大小,但需要佔用記憶體300MB。使用Pickle模組序列化到磁碟,輸出檔案的大小超過30MB,使用gzip壓縮後下降到大約7MB。我懷疑記憶體大的部分原因是python字典的最小尺寸。我將使用列表來實現,如果我能夠做到更高效,屆時我會更新這篇文章。

這裡是上述所生成樹的資料,可能你會感興趣:
總單詞數:234,936
葉節點:215,366
內部節點:1,874,748

由此我們可以看出,內部節點的平均基數是非常低的,不會大於1。下面資料有助於澄清:

Tier Number of nodes
0 1
1 7
2 25
3 85
4 203
5 707
6 1145
7 1886
8 3479
9 8156
10 8853
11 10835
12 19632
13 28470
14 47635
15 73424
16 92618
17 94770
18 125018
19 156406
20 182305
21 195484
22 200031
23 203923
24 205649
25 214001

靠近樹的頂部節點的基數非常高,但樹很快變平,最後四層樹只佔總結點的一半。這暗示了一個可能的空間優化:刪除樹的最後幾層,將它們的葉子節點連在一起。當進行查詢時,檢查所選的節點,保證它們是輸入字串的字謎集合。

* 我可能只是重新發現了電腦科學領域30年前就被提及的論文。但驚喜的是,通過搜尋尚未找到誰正在使用該演算法,或者有其它方法比蠻力演算法更有效。

修訂:最初的實現程式碼在這。

修訂:使用列表來重新實現我的python程式碼,幾乎節約了一半記憶體。有機會我會貼出pickled後的樹和原始碼。

修訂:更多更新在這

(譯註:字謎問題可簡化為字串編碼和索引問題,如Tea編碼為A1E1T1,編碼雜湊後,同編碼單詞有Ate,Eat等。文章寫於2007年,文中演算法不是最優解,只是提供了一種使用多路查詢樹的思路,類似資料結構有Trie,DAG,Suffix Tree等等。)

相關文章