DFA 确定性有限状态自动机
DFA确定性有限状态自动机是一种图结构的数据结构,可以由(Q, q0, A, Sigma, Delta)来描述,其中Q为状态集,q0为初始状态,A为终态集合,Sigma为字母表,Delta为转移函数。它表示从唯一一个起始状态q0开始,经过有限步的Delta转移,转移是根据字母表Sigma中的元素来进行,最终到达终态集合A中的某个状态的状态移动。
如图所示是一个终态集合为{"nano"}的DFA。
DFA只能有一个起点而可以有多个终点。每个节点都有字符集大小数条有向边,并且任一节点,都不会存在相同字符的有向边指向不同的节点。
Trie树
Trie树为单词前缀树,m个模式串中的前缀所组成的集合A与根节点到每一个树中的节点的路径上的字符组成的字符串S所组成的集合B,是满射关系。具体参见:Trie树
例如可以利用Trie树求字符串S的所有不同子串:
假设当前字符串为S,用S的所有后缀作为len(S)个模式串,插入到一棵Trie树中,Trie树中的每个节点对应的字符串就是字符串S中的一个子串,不同子串一定对应不同的节点。
Trie图
1. Trie图结构
Trie图为以Trie树为基础构造出来的一种DFA。对于插入的每个模式串,其插入过程使用的最后一个节点都作为DFA的一个“终止”节点。
如果要求一个母串包含哪些模式串(即该母串的某个子串恰好等于预先给定的某个模式串),以母串作为DFA的输入 ,在DFA上行走,走到“终止”节点就意味着匹配了相应的模式串。
2. 失配处理
在行走的过程中,如果出现母串中的下一个字符在Trie图中当前位置处没有一个子节点与之对应,或者Trie图中的当前位置正好匹配了一个模式串,那么需要调整母串重新匹配的位置。一般,可以调整母串上开始匹配的位置,使之加1,再尝试从Trie图的根节点位置开始匹配。这样显然效率很低。可以参考KMP算法的最长相同前后缀的方法,来避免回溯。
3. 前缀指针
为了避免回溯,参考KMP的next数组,在Trie图中定义“前缀指针”:
从根节点到节点P可以得到一个字符串S,节点P的前缀指针定义为 指向树中出现过的S的最长后缀(不能等于S)
4. 高效的构造前缀指针
根据深度一一求出每一个节点的前缀指针。对于当前节点,设它的父节点与它的边上的字符为ch,如果它的父节点的前缀指针所指向的节点的儿子节点中,有通过ch字符指向的儿子,那么当前节点的前缀指针指向该儿子节点,否则通过当前节点父节点的前缀指针指向节点的前缀指针,继续向上查找,直到到达根为止。
5. 危险节点
(1)“终止”节点是危险节点
(2)如果一个节点的前缀指针指向危险节点,则该节点为危险节点
6. 在建立好的Trie图上遍历
从root出发,按照当前串的下一个字符ch进行在树上的移动。若当前点P不存在通过ch连接的儿子,那么将P的前缀指针所指向的节点Q作为当前节点;
如果还无法找到通过ch连接的儿子节点,再考虑Q的前缀指针指向的节点(将之作为当前节点)....;
直到找到通过ch连接的儿子,再继续遍历;
如果遍历的过程中经过了某个非终止节点的危险节点,则可以断定S包含某个模式串,要找出是哪个模式串,沿着危险节点的前缀指针走,碰到终止节点即可。
7. Trie图的时间复杂度
在Trie图上遍历母串S的时间复杂度为len(S)。
(1)母串每过掉一个字符,不论该字符是匹配上还是没匹配上,在trie图上最多向下走一层
(2)一个节点的前缀指针总是指向更高层次的节点,所以每次沿着前缀指针走一步,节点的层次就会向上一层
(3)母串S最终被过掉了len(S)个字符,所以最多向下走了len(S)次。
(4)最多向下走了len(S)次,那么就不可能向上走超过len(S)次,因此沿着前缀指针走的次数,做多不超过len(S)
前缀指针思想
Trie通过前缀指针来避免母串的回溯,其思想和KMP算法非常相似。
KMP算法是通过确定子串中失配点之前的子串的最长相同前后缀,失配时,母串当前点不回溯,而是直接和最长相同前后缀的前缀处继续进行匹配。
(kmp 避免母串指针回溯)
和KMP类似,Trie图中的每个节点都对应一个模板串(节点为终止节点)或者模板串的子串(节点不是终止节点),记为S。S可以确定len(S)-1个后缀(从S中的第2到第len(S)-1个位置到S的末尾确定),其中有些后缀串Si可能正好对应该Trie图中从root节点出发的到某个节点Pi确定的串。
如上图所示,绿色方块区域为从母串上一个开始匹配点到失配点之前的匹配区域,红色为失配点,该绿色匹配区域中有两个后缀子串sub1[S1,A]区域和sub2[S2,A]区域,分别对应Trie图中从root出发到P1,P2点确定的串。且母串中[S1,E1]和[S2,E2]分别对应一个模式串。
母串不回溯,Trie图上当前点的移动,可以匹配母串中存在的所有的模式串
以上图为例此时,需要确定母串指针之后的移动可以找到[S1,E1]和[S2,E2]两个模式串,策略是先匹配起点靠前的那个串,即[S1,E1]。
case 1 E1 > E2
母串指针不回溯,Trie图的当前点转移到P1(从root到P1对应[S1,A]),然后尝试匹配。匹配成功,到达E1点,此时将Trie图中点从E1移动到E1的前缀指针,由于[S2,E1]为[S1,E1]的最长后缀,即移动到点P,使得root到P为[S2,E1]。因为Trie图中[S2,E2]对应从root出发到某个点Q的串,那么root到Q的路径必然经过点P,此时从点P继续匹配,必然能够到达Q;
这样,就得到了[S1,E1]和[S2,E2]两个模式串。
case 2 E1 < E2
母串指针不回溯,Trie图的当前点转移到P1(从root到P1对应[S1,A]),然后尝试匹配。由于[S1,E1]对应一个模式串,即对应Trie图中的某个终止节点,从P1点开始会一直匹配到达P(从root到P对应[S1,E1])。在匹配的过程中,会碰到某个点危险节点M,M指向节点Q(从root到Q对应[S2,E2])(这是在设置Trie图中各个节点的前缀指针的时候确定的),根据Trie图的遍历规则,会得到[S2,E2]的模式串。这样,就得到了[S1,E1]和[S2,E2]两个模式串。
Trie图实现(c++)
#include<iostream> #include<vector> #include<queue> #include<string> using namespace std; #define LETTERS 26 int gNodeCount = 2; struct Node{ Node* childs[LETTERS]; //子节点 Node* prev; //前缀指针 bool danger_node; //是否危险节点 Node(){ Init(); } void Init(){ memset(childs, 0, sizeof(childs)); danger_node = false; prev = NULL; } }; Node gNodes[2000]; void Insert(Node* root, char* str){ char* p = str; Node* node = root; while (*p != ' '){ int index = *p - 'A'; if (node->childs[index] == 0){ node->childs[index] = gNodes + gNodeCount ++; } node = node->childs[index]; p++; } node->danger_node = true; } //在Trie树上添加前缀指针 void BuildDfa(){ Node* root = gNodes + 1; for (int i = 0; i < LETTERS; i++){ //为虚拟节点 gNodes[0].childs[i] = root; } root->prev = gNodes; gNodes[0].prev = NULL; deque<Node*> Q; Q.push_back(root); while (!Q.empty()){ Node* node = Q.front(); Node* prev = node->prev, *p; Q.pop_front(); for (int i = 0; i < LETTERS; i++){ if (node->childs[i]){ p = prev; while (p && !p->childs[i]){ p = p->prev; } node->childs[i]->prev = p->childs[i]; //这个地方注意,不能写成 p->childs[i]->danger_node = node->childs[i]->danger_node if (p->childs[i]->danger_node) node->childs[i]->danger_node = true; Q.push_back(node->childs[i]); } } } } bool SearchDfa(char* str){ char*p = str; Node* node = gNodes + 1; while (*p != ' '){ int index = *p - 'A'; if (node->danger_node) return true; while (node&& !node->childs[index]){ node = node->prev; }
p++; } return false; }