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; }

 

相关文章:

  • 2021-07-31
  • 2021-07-31
  • 2021-06-17
  • 2021-12-16
  • 2021-12-22
  • 2022-02-05
  • 2021-07-05
  • 2021-08-05
猜你喜欢
  • 2021-12-30
  • 2021-09-22
  • 2022-01-07
  • 2022-02-24
  • 2022-01-06
相关资源
相似解决方案