• Trie图


    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; }
  • 相关阅读:
    sizeof,终极无惑(上)
    send,recv,sendto,recvfrom
    【问卷调查】社团对海大学生成长的影响研究(及部分调查结果)
    Opencv cvCircle函数
    墨菲定律、二八法则、马太效应、手表定理、“不值得”定律、彼得原理、零和游戏、华盛顿合作规律、酒与污水定律、水桶定律、蘑菇管理原理、钱的问题、奥卡姆剃刀等13条是左右人生的金科玉律
    C#操作Excel文件(读取Excel,写入Excel)
    秋风秋雨愁煞人
    Java Applet读写client串口——终极篇
    数据库索引的作用和长处缺点
    EasyARM i.mx28学习笔记——开箱试用总结
  • 原文地址:https://www.cnblogs.com/gtarcoder/p/4820560.html
Copyright © 2020-2023  润新知