經典算法題每日演練——第八題 AC自動機


 

     上一篇我們說了單模式匹配算法KMP,現在我們有需求了,我要檢查一篇文章中是否有某些敏感詞,這其實就是多模式匹配的問題。

當然你也可以用KMP算法求出,那么它的時間復雜度為O(c*(m+n)),c:為模式串的個數。m:為模式串的長度,n:為正文的長度,那

么這個復雜度就不再是線性了,我們學算法就是希望能把要解決的問題優化到極致,這不,AC自動機就派上用場了。

   其實AC自動機就是Trie樹的一個活用,活用點就是灌輸了kmp的思想,從而再次把時間復雜度優化到線性的O(N),剛好我前面的文

章已經說過了Trie樹和KMP,這里還是默認大家都懂。

一:構建AC自動機

  同樣我也用網上的經典例子,現有say she shr he her 這樣5個模式串,主串為yasherhs,我要做的就是哪些模式串在主串中出現過?

1: 構建trie樹

    如果看過我前面的文章,構建trie樹還是很容易的。

2:失敗指針

    構建失敗指針是AC自動機的核心所在,玩轉了它也就玩轉了AC自動機,失敗指針非常類似於KMP中的next數組,也就是說,

 當我的主串在trie樹中進行匹配的時候,如果當前節點不能再繼續進行匹配,那么我們就會走到當前節點的failNode節點繼續進行

匹配,構建failnode節點也是很流程化的。

①:root節點的子節點的failnode都是指向root。

②:當走到在“she”中的”h“節點時,我們給它的failnode設置什么呢?此時就要走該節點(h)的父節點(s)的失敗指針,一直回溯直

     到找到某個節點的孩子節點也是當初節點同樣的字符(h),沒有找到的話,其失敗指針就指向root。

     比如:h節點的父節點為s,s的failnode節點為root,走到root后繼續尋找子節點為h的節點,恰好我們找到了,(假如還是沒

             有找到,則繼續走該節點的failnode,嘿嘿,是不是很像一種回溯查找),此時就將 ”she"中的“h”節點的fainode"指向

            "her"中的“h”節點,好,原理其實就是這樣。(看看你的想法是不是跟圖一樣)

針對圖中紅線的”h,e“這兩個節點,我們想起了什么呢?對”her“中的”e“來說,e到root距離的n個字符恰好與”she“中的e向上的n

個字符相等,我也非常類似於kmp中next函數,當字符失配時,next數組中記錄着下一次匹配時模式串的起始位置。

 1 #region Trie樹節點
 2         /// <summary>
 3         /// Trie樹節點
 4         /// </summary>
 5         public class TrieNode
 6         {
 7             /// <summary>
 8             /// 26個字符,也就是26叉樹
 9             /// </summary>
10             public TrieNode[] childNodes;
11 
12             /// <summary>
13             /// 詞頻統計
14             /// </summary>
15             public int freq;
16 
17             /// <summary>
18             /// 記錄該節點的字符
19             /// </summary>
20             public char nodeChar;
21 
22             /// <summary>
23             /// 失敗指針
24             /// </summary>
25             public TrieNode faliNode;
26 
27             /// <summary>
28             /// 插入記錄時的編號id
29             /// </summary>
30             public HashSet<int> hashSet = new HashSet<int>();
31 
32             /// <summary>
33             /// 初始化
34             /// </summary>
35             public TrieNode()
36             {
37                 childNodes = new TrieNode[26];
38                 freq = 0;
39             }
40         }
41         #endregion

剛才我也說到了parent和current兩個節點,在給trie中的節點賦failnode的時候,如果采用深度優先的話還是很麻煩的,因為我要實時

記錄當前節點的父節點,相信寫過樹的朋友都清楚,除了深搜,我們還有廣搜。

 1  /// <summary>
 2         /// 構建失敗指針(這里我們采用BFS的做法)
 3         /// </summary>
 4         /// <param name="root"></param>
 5         public void BuildFailNodeBFS(ref TrieNode root)
 6         {
 7             //根節點入隊
 8             queue.Enqueue(root);
 9 
10             while (queue.Count != 0)
11             {
12                 //出隊
13                 var temp = queue.Dequeue();
14 
15                 //失敗節點
16                 TrieNode failNode = null;
17 
18                 //26叉樹
19                 for (int i = 0; i < 26; i++)
20                 {
21                     //代碼技巧:用BFS方式,從當前節點找其孩子節點,此時孩子節點
22                     //         的父親正是當前節點,(避免了parent節點的存在)
23                     if (temp.childNodes[i] == null)
24                         continue;
25 
26                     //如果當前是根節點,則根節點的失敗指針指向root
27                     if (temp == root)
28                     {
29                         temp.childNodes[i].faliNode = root;
30                     }
31                     else
32                     {
33                         //獲取出隊節點的失敗指針
34                         failNode = temp.faliNode;
35 
36                         //沿着它父節點的失敗指針走,一直要找到一個節點,直到它的兒子也包含該節點。
37                         while (failNode != null)
38                         {
39                             //如果不為空,則在父親失敗節點中往子節點中深入。
40                             if (failNode.childNodes[i] != null)
41                             {
42                                 temp.childNodes[i].faliNode = failNode.childNodes[i];
43                                 break;
44                             }
45                             //如果無法深入子節點,則退回到父親失敗節點並向root節點往根部延伸,直到null
46                             //(一個回溯再深入的過程,非常有意思)
47                             failNode = failNode.faliNode;
48                         }
49 
50                         //等於null的話,指向root節點
51                         if (failNode == null)
52                             temp.childNodes[i].faliNode = root;
53                     }
54                     queue.Enqueue(temp.childNodes[i]);
55                 }
56             }
57         }

3:模式匹配

   所有字符在匹配完后都必須要走failnode節點來結束自己的旅途,相當於一個回旋,這樣做的目的防止包含節點被忽略掉。

    比如:我匹配到了"she",必然會匹配到該字符串的后綴”he",要想在程序中匹配到,則必須節點要走失敗指針來結束自己的旅途。

從上圖中我們可以清楚的看到“she”的匹配到字符"e"后,從failnode指針撤退,在撤退途中將其后綴字符“e”收入囊腫,這也就是

為什么像kmp中的next函數。

 1         /// <summary>
 2         /// 根據指定的主串,檢索是否存在模式串
 3         /// </summary>
 4         /// <param name="root"></param>
 5         /// <param name="s"></param>
 6         /// <returns></returns>
 7         public void SearchAC(ref TrieNode root, string s, ref HashSet<int> hashSet)
 8         {
 9             int freq = 0;
10 
11             TrieNode head = root;
12 
13             foreach (var c in s)
14             {
15                 //計算位置
16                 int index = c - 'a';
17 
18                 //如果當前匹配的字符在trie樹中無子節點並且不是root,則要走失敗指針
19                 //回溯的去找它的當前節點的子節點
20                 while ((head.childNodes[index] == null) && (head != root))
21                     head = head.faliNode;
22 
23                 //獲取該叉樹
24                 head = head.childNodes[index];
25 
26                 //如果為空,直接給root,表示該字符已經走完畢了
27                 if (head == null)
28                     head = root;
29 
30                 var temp = head;
31 
32                 //在trie樹中匹配到了字符,標記當前節點為已訪問,並繼續尋找該節點的失敗節點。
33                 //直到root結束,相當於走了一個回旋。(注意:最后我們會出現一個freq=-1的失敗指針鏈)
34                 while (temp != root && temp.freq != -1)
35                 {
36                     freq += temp.freq;
37 
38                     //將找到的id追加到集合中
39                     foreach (var item in temp.hashSet)
40                         hashSet.Add(item);
41 
42                     temp.freq = -1;
43 
44                     temp = temp.faliNode;
45                 }
46             }
47         }

好了,到現在為止,我想大家也比較清楚了,最后上一個總的運行代碼:

View Code
  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Diagnostics;
  6 using System.Threading;
  7 using System.IO;
  8 
  9 namespace ConsoleApplication2
 10 {
 11     public class Program
 12     {
 13         public static void Main()
 14         {
 15             Trie trie = new Trie();
 16 
 17             trie.AddTrieNode("say", 1);
 18             trie.AddTrieNode("she", 2);
 19             trie.AddTrieNode("shr", 3);
 20             trie.AddTrieNode("her", 4);
 21             trie.AddTrieNode("he", 5);
 22 
 23             trie.BuildFailNodeBFS();
 24 
 25             string s = "yasherhs";
 26 
 27             var hashSet = trie.SearchAC(s);
 28 
 29             Console.WriteLine("在主串{0}中存在模式串的編號為:{1}", s, string.Join(",", hashSet));
 30 
 31             Console.Read();
 32         }
 33     }
 34 
 35     public class Trie
 36     {
 37         public TrieNode trieNode = new TrieNode();
 38 
 39         /// <summary>
 40         /// 用光搜的方法來構建失敗指針
 41         /// </summary>
 42         public Queue<TrieNode> queue = new Queue<TrieNode>();
 43 
 44         #region Trie樹節點
 45         /// <summary>
 46         /// Trie樹節點
 47         /// </summary>
 48         public class TrieNode
 49         {
 50             /// <summary>
 51             /// 26個字符,也就是26叉樹
 52             /// </summary>
 53             public TrieNode[] childNodes;
 54 
 55             /// <summary>
 56             /// 詞頻統計
 57             /// </summary>
 58             public int freq;
 59 
 60             /// <summary>
 61             /// 記錄該節點的字符
 62             /// </summary>
 63             public char nodeChar;
 64 
 65             /// <summary>
 66             /// 失敗指針
 67             /// </summary>
 68             public TrieNode faliNode;
 69 
 70             /// <summary>
 71             /// 插入記錄時的編號id
 72             /// </summary>
 73             public HashSet<int> hashSet = new HashSet<int>();
 74 
 75             /// <summary>
 76             /// 初始化
 77             /// </summary>
 78             public TrieNode()
 79             {
 80                 childNodes = new TrieNode[26];
 81                 freq = 0;
 82             }
 83         }
 84         #endregion
 85 
 86         #region 插入操作
 87         /// <summary>
 88         /// 插入操作
 89         /// </summary>
 90         /// <param name="word"></param>
 91         /// <param name="id"></param>
 92         public void AddTrieNode(string word, int id)
 93         {
 94             AddTrieNode(ref trieNode, word, id);
 95         }
 96 
 97         /// <summary>
 98         /// 插入操作
 99         /// </summary>
100         /// <param name="root"></param>
101         /// <param name="s"></param>
102         public void AddTrieNode(ref TrieNode root, string word, int id)
103         {
104             if (word.Length == 0)
105                 return;
106 
107             //求字符地址,方便將該字符放入到26叉樹中的哪一叉中
108             int k = word[0] - 'a';
109 
110             //如果該叉樹為空,則初始化
111             if (root.childNodes[k] == null)
112             {
113                 root.childNodes[k] = new TrieNode();
114 
115                 //記錄下字符
116                 root.childNodes[k].nodeChar = word[0];
117             }
118 
119             var nextWord = word.Substring(1);
120 
121             //說明是最后一個字符,統計該詞出現的次數
122             if (nextWord.Length == 0)
123             {
124                 root.childNodes[k].freq++;
125                 root.childNodes[k].hashSet.Add(id);
126             }
127 
128             AddTrieNode(ref root.childNodes[k], nextWord, id);
129         }
130         #endregion
131 
132         #region 構建失敗指針
133         /// <summary>
134         /// 構建失敗指針(這里我們采用BFS的做法)
135         /// </summary>
136         public void BuildFailNodeBFS()
137         {
138             BuildFailNodeBFS(ref trieNode);
139         }
140 
141         /// <summary>
142         /// 構建失敗指針(這里我們采用BFS的做法)
143         /// </summary>
144         /// <param name="root"></param>
145         public void BuildFailNodeBFS(ref TrieNode root)
146         {
147             //根節點入隊
148             queue.Enqueue(root);
149 
150             while (queue.Count != 0)
151             {
152                 //出隊
153                 var temp = queue.Dequeue();
154 
155                 //失敗節點
156                 TrieNode failNode = null;
157 
158                 //26叉樹
159                 for (int i = 0; i < 26; i++)
160                 {
161                     //代碼技巧:用BFS方式,從當前節點找其孩子節點,此時孩子節點
162                     //         的父親正是當前節點,(避免了parent節點的存在)
163                     if (temp.childNodes[i] == null)
164                         continue;
165 
166                     //如果當前是根節點,則根節點的失敗指針指向root
167                     if (temp == root)
168                     {
169                         temp.childNodes[i].faliNode = root;
170                     }
171                     else
172                     {
173                         //獲取出隊節點的失敗指針
174                         failNode = temp.faliNode;
175 
176                         //沿着它父節點的失敗指針走,一直要找到一個節點,直到它的兒子也包含該節點。
177                         while (failNode != null)
178                         {
179                             //如果不為空,則在父親失敗節點中往子節點中深入。
180                             if (failNode.childNodes[i] != null)
181                             {
182                                 temp.childNodes[i].faliNode = failNode.childNodes[i];
183                                 break;
184                             }
185                             //如果無法深入子節點,則退回到父親失敗節點並向root節點往根部延伸,直到null
186                             //(一個回溯再深入的過程,非常有意思)
187                             failNode = failNode.faliNode;
188                         }
189 
190                         //等於null的話,指向root節點
191                         if (failNode == null)
192                             temp.childNodes[i].faliNode = root;
193                     }
194                     queue.Enqueue(temp.childNodes[i]);
195                 }
196             }
197         }
198         #endregion
199 
200         #region 檢索操作
201         /// <summary>
202         /// 根據指定的主串,檢索是否存在模式串
203         /// </summary>
204         /// <param name="s"></param>
205         /// <returns></returns>
206         public HashSet<int> SearchAC(string s)
207         {
208             HashSet<int> hash = new HashSet<int>();
209 
210             SearchAC(ref trieNode, s, ref hash);
211 
212             return hash;
213         }
214 
215         /// <summary>
216         /// 根據指定的主串,檢索是否存在模式串
217         /// </summary>
218         /// <param name="root"></param>
219         /// <param name="s"></param>
220         /// <returns></returns>
221         public void SearchAC(ref TrieNode root, string s, ref HashSet<int> hashSet)
222         {
223             int freq = 0;
224 
225             TrieNode head = root;
226 
227             foreach (var c in s)
228             {
229                 //計算位置
230                 int index = c - 'a';
231 
232                 //如果當前匹配的字符在trie樹中無子節點並且不是root,則要走失敗指針
233                 //回溯的去找它的當前節點的子節點
234                 while ((head.childNodes[index] == null) && (head != root))
235                     head = head.faliNode;
236 
237                 //獲取該叉樹
238                 head = head.childNodes[index];
239 
240                 //如果為空,直接給root,表示該字符已經走完畢了
241                 if (head == null)
242                     head = root;
243 
244                 var temp = head;
245 
246                 //在trie樹中匹配到了字符,標記當前節點為已訪問,並繼續尋找該節點的失敗節點。
247                 //直到root結束,相當於走了一個回旋。(注意:最后我們會出現一個freq=-1的失敗指針鏈)
248                 while (temp != root && temp.freq != -1)
249                 {
250                     freq += temp.freq;
251 
252                     //將找到的id追加到集合中
253                     foreach (var item in temp.hashSet)
254                         hashSet.Add(item);
255 
256                     temp.freq = -1;
257 
258                     temp = temp.faliNode;
259                 }
260             }
261         }
262         #endregion
263     }
264 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM