DFA 确定性有限状态自动机
DFA只能有一个起点而可以有多个终点。每个节点都有字符集大小数条有向边,并且任一节点,都不会存在相同字符的有向边指向不同的节点。
Trie树
假设当前字符串为S,用S的所有后缀作为len(S)个模式串,插入到一棵Trie树中,Trie树中的每个节点对应的字符串就是字符串S中的一个子串,不同子串一定对应不同的节点。
Trie图
如果要求一个母串包含哪些模式串(即该母串的某个子串恰好等于预先给定的某个模式串),以母串作为DFA的输入 ,在DFA上行走,走到“终止”节点就意味着匹配了相应的模式串。
在行走的过程中,如果出现母串中的下一个字符在Trie图中当前位置处没有一个子节点与之对应,或者Trie图中的当前位置正好匹配了一个模式串,那么需要调整母串重新匹配的位置。一般,可以调整母串上开始匹配的位置,使之加1,再尝试从Trie图的根节点位置开始匹配。这样显然效率很低。可以参考KMP算法的最长相同前后缀的方法,来避免回溯。
为了避免回溯,参考KMP的next数组,在Trie图中定义“前缀指针”:
从根节点到节点P可以得到一个字符串S,节点P的前缀指针定义为 指向树中出现过的S的最长后缀(不能等于S)
(2)如果一个节点的前缀指针指向危险节点,则该节点为危险节点
如果遍历的过程中经过了某个非终止节点的危险节点,则可以断定S包含某个模式串,要找出是哪个模式串,沿着危险节点的前缀指针走,碰到终止节点即可。
7. Trie图的时间复杂度
在Trie图上遍历母串S的时间复杂度为len(S)。
(1)母串每过掉一个字符,不论该字符是匹配上还是没匹配上,在trie图上最多向下走一层
(2)一个节点的前缀指针总是指向更高层次的节点,所以每次沿着前缀指针走一步,节点的层次就会向上一层
(3)母串S最终被过掉了len(S)个字符,所以最多向下走了len(S)次。
(4)最多向下走了len(S)次,那么就不可能向上走超过len(S)次,因此沿着前缀指针走的次数,做多不超过len(S)
前缀指针思想
(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]分别对应一个模式串。
这样,就得到了[S1,E1]和[S2,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 != '\0'){
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 != '\0'){
int index = *p - 'A';
if (node->danger_node)
return true;
while (node&& !node->childs[index]){
node = node->prev;
}
p++;
}
return false;
}