Trie在ACM中已經十分普及,也是一種非常有效的索引結構,好處就不多說了。
它的本質就是一個確定的有限狀態自動機(DFA),關于它的實現也是有好幾種,ACM中用的最多也是最容易實現的就是多路查找樹。
但是Trie最大的缺點就是占用空間過大,很容易爆內存,當然在ACM里對Trie樹也有相應的優化,如限定高度,對分支較少的節點使用非隨機訪問的結構(減少寬度),但這些都是犧牲部分查找效率換取的。
這里介紹一種實現,Double-Array Trie(雙數組字典樹),其實它就是雙數組,跟樹結構沒啥關系。他能在一定程度上減少內存的浪費。
兩個數組,一個是base[],另一個是check[]。設數組下標為i ,如果base[i], check[i]均為0,表示該位置為空。如果base[i]為負值,表示該狀態為終止態(即詞語)。check[i]表示該狀態的前一狀態。
定義 1. 對于輸入字符 c, 從狀態 s 轉移到狀態 t, 雙數組字典樹滿足如下條件:
check[base[s] + c] = s |
base[s] + c = t |
從定義1中,我們能得到查找算法,對于給定的狀態 s 和輸入字符 c :
t := base[s] + c; |
if check[t] = s then |
next state := t |
else |
fail |
endif |
插入的操作,假設以某字符開頭的 base 值為i,第二個字符的字符序列碼依次為c1, c2, c3…cn,則肯定要滿足base[i+c1], check[i+c1], base[i+c2], check[i+c2], base[i+c3], check[i+c3]…base[i+cn], check[i+cn]均為0。
我們知道雙數組的實現方法是當狀態有新轉移時才分配空間給新狀態,或可以表述為只分配需要轉移的狀態的空間。當遇到無法滿足上述條件時再進行調整,使得其 base 值滿足上述條件,這種調整只影響當前節點下一層節點的重分配,因為所有節點的地址分配是靠 base 數組指定的起始下標所決定的。
我們先得到重分配算法:
Procedure Relocate(s : state; b : base_index) |
{ Move base for state s to a new place beginning at b } |
begin |
foreach input character c for the state s |
{ i.e. foreach c such that check[base[s] + c]] = s } |
begin |
check[b + c] := s; { mark owner } |
base[b + c] := base[base[s] + c]; { copy data } |
{ the node base[s] + c is to be moved to b + c; |
Hence, for any i for which check[i] = base[s] + c, update check[i] to b + c } |
foreach input character d for the node base[s] + c |
begin |
check[base[base[s] + c] + d] := b + c |
end ; |
check[base[s] + c] := none { free the cell } |
end ; |
base[s] := b |
end |
如:有兩個單詞ac和da,先插入單詞ac,這時的 base 數組
插入單詞da的d時,發現該地址已被c占用,我們進行重分配
從上圖可見a和d的位置重新分配了, base 值從0變成了1。
假設,Tire里有n個節點,字符集大小為m,則DATrie的空間大小是n+cm,c是依賴于Trie稀疏程度的一個系數。而多路查找樹的空間大小是nm。
注意,這里的復雜度都是按離線算法(offline algorithm)計算的,即處理時已經得到整個詞庫。在線算法(online algorithm)的空間復雜度還和單詞出現的順序有關,越有序的單詞順序空間占用越小。
查找算法的復雜度和被查找的字符串長度相關的,這個復雜度和多路查找樹是一樣的。
插入算法中,如果出現重分配的情況,我們要附加上掃描子節點的時間復雜度,還有新base值確定的算法復雜度。假如這兒我們都是用暴力算法(for循環掃描),那插入算法時間復雜度是O(nm + cm2)。。
實際編碼過程中,DATrie代碼難度大過多路查找樹,主要是狀態的表示不如樹結構那樣的清晰,下標很容易搞混掉。
有個地方需要注意的是,base值正數表示起始偏移量,負數表示該狀態為終止態,所以在查找新base值時,要保證查到的值是正數。
如:空Trie狀態下,插入d時,因為第一個空地址是1,所以得到base=1-4=-3,這樣base正負的含義就被破壞了。
關于優化:
- 查找空地址優化
- 數組長度的壓縮
- 字符后綴的壓縮
base[i], check[i]均為0,表示該位置為空。我們可以把這部分給利用起來,全為0的標記所包含的信息實在太少了。我們利用base和check數組組成一個雙向鏈表。
定義 2. 設 r1, r2, … ,rcm 為空閑地址有序序列,則我們的雙向鏈表可定義為 :
check[ 0 ] = -r[ 1 ] |
check[r[i]] = -r[i+1] ; 1 <= i <= cm- 1 |
check[r[cm]] = 0 |
base[ 0 ] = -r[cm] |
base[r[ 1 ]] = 0 |
base[r[i+ 1 ]] = -r[i] ; 1 <= i <= cm- 1 |
由于我們把base[0]作為根節點,所以初始化時就可以把base[0]排除在鏈表之外,而check[0]可以被作為鏈表的頭節點。
設節點的狀態轉移集為P = {c1, c2, …, cp},依靠鏈表我們能得到新的空地址查找算法:
{find least free cell s such that s > c[1]} |
s := -check[ 0 ]; |
while s <> 0 and s <= c[ 1 ] do |
s := -check[s] |
end ; |
if s = 0 then return FAIL; {or reserve some additional space} |
{continue searching for the row, given that s matches c[1]} |
while s <> 0 do |
i := 2 ; |
while i <= p and check[s + c[i] - c[ 1 ]] < 0 do |
i := i + 1 |
end ; |
if i = p + 1 then return s - c[ 1 ]; {all cells required are free, so return it} |
s := -check[-s] |
end ; |
return FAIL; {or reserve some additional space} |
優化后的空地址查找算法時間復雜度為O(cm2),而重分配算法的時間復雜度為O(m2),則總的時間復雜度為O(cm2 + m2) = O(cm2)。
重分配或刪除節點后,原先的地址被作廢,可以重新加入鏈表,這樣如果遇到原狀態轉移集的子集時,就可以派上用場了。
其實這部分的優化就是使用了閑置信息域做成了鏈表,所以這部分的插入和刪除優化原理就很容易理解了,時間復雜度為O(cm)。
t := -check[ 0 ]; |
while check[t] <> 0 and t < s do |
t := -check[t] |
end ; |
{t now points to the cell after s' place} |
check[s] := -t; |
check[-base[t]] := -s; |
base[s] := base[t]; |
base[t] := -s; |
當有節點刪除時,我們不僅可以把它加回到鏈表中,還可以重新為最大非空節點的狀態重新確定base值,因為刪除可能導致它的前面有空間容納下它的狀態轉移集。這樣我們可能又得以刪除一些空值狀態,使得數組長度有希望被壓縮。
這個思想借鑒于后綴樹,我們可以將沒有分支的后綴單獨存放,但這個結構肯定獨立于DATrie,所以在這就不詳述了。詳情見[Aoe1989]。
整體而言,在ACM中,DATrie略高的編碼復雜度和過低的插入效率,應用面不會太廣。但現實問題中,詞庫大小一般比較穩定,在離線算法也有很大的優化余地,此時DATrie的空間優勢就會比較明顯。畢竟Trie高效的檢索效率這一優點是值得研究探討的。
這篇日志寫的夠長了,等有空再把DATrie測試報告給整理下吧。唉,發現自己語言組織能力越來越差了,寫的連自己有時都看不下去,要多堅持記下技術日志了~~
以下是只加入空地址查找優化后的DATrie代碼,對于字符集表的映射結構也是個需要持續討論的問題,在這個代碼里只支持英文字母。

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143
