LeetCode 1032. 字符流 - AC自动机“关键步骤”不一样的理解

https://leetcode-cn.com/problems/stream-of-characters/

这题用的AC自动机,之前在工作中用过,再来练练手总结一下,这里记录一下自己在实现过程中比较纠结的几个点。

AC自动机可以实现多模匹配,就是初始化给定一批特定字符串(模式)集合,然后判断输入的字符流中是否包含该集合中的模式串。

1.Trie树

要实现AC自动机需要先理解Trie树。

建立Trie树时会有一个空节点Root作为根,其余每个节点有一个指向父节点的指针Father,一个大小为字符集大小的孩子指针集合Next,Next[i]==Null表示对应的孩子不存在,以及一个标记当前节点是否是某个模式串的结束标记end;
AC自动机针对Trie树做了优化来提高匹配速度,在每个节点中增加了一个后缀指针Tail

2.后缀指针(Tail)

普通Trie树(不涉及各种优化)的构建还是比较简单的,构建完Trie树之后,就得计算每个节点的“后缀指针”。

百度浏览了一下AC自动机的相关介绍,可以知道传统上大家都叫“失配(Fail)指针”,基本每篇文章里面说的都是Fail指针,Fail指针的意思就是当前位置匹配失败后需要跳转过去的地方。这种说法只表达了匹配失败后要跳转,但是没有表达出为什么要跳转,跳转到哪里去。实际本质上是要跳转到当前匹配串的后缀串,所以我觉得称为“后缀(Tail)指针”可能更易于理解一点。

即:对于要查找的字符串D,设从根匹配到当前节点的串为S1,S1继续向下匹配失败,这说明所有与S1有公共前缀的模式串都匹配失败了,这时需要换到另一个与S1没有公共前缀的串进行匹配。

这里切换的时候就需要一定技巧了,类似于KMP算法的思想,就是对于已经比较过的字符串要避免重复比较

即,如果新模式串M的一个前缀S2是当前串S1的后缀,那么切换后不需要从M头部开始比较,因为前缀S2已经在S1的匹配过程中比较过了,所以直接从S2后面继续比较即可。

如图所示,由模式串ABCD,BCD构成的Trie树,假设输入的字符流为ABCDE,则左边匹配到字符C,即串S1后,发现继续匹配G与D不同,匹配失败,这时我们需要换另一个模式进行匹配,但是可以看到BCD的前缀BC是S1的后缀,以及比较过了,所以我们直接跳到C'继续匹配,并命中模式BCD。所以这里,C的Tail指针指向C'。

理解了Tail指针的含义,我们就可以在初始化建立Trie树之后,计算好每个节点的Tail指针,从而可以在匹配的时候减少不必要的重复比较,提高匹配速度。

计算所有节点Tail指针

Root的所有孩子节点的Tail指针指向Root;

对于其余每个值为字符'C'的节点Node,其Tail指针指向父节点的Tail指针对应节点的子节点中值也为'C'的节点,若该节点不存在,则指向【父节点】的【Tail指针对应节点】的【Tail指针对应节点】的子节点中值也为'C'的节点,一起类推,直至Root节点,即:

Node->Tail = Node->Father->Tail->(...Tail->)Next[C]

因为计算每个节点的Tail指针过程需要用到上层节点的Tail信息,所以需要按广度优先的顺序计算。

如图所示,节点A、B’的Tail指针直接指向Root(具体实现的时候将指向Root的都指向NULL,便于判断);

节点C的Tail指针(蓝色)由红色指针Father->Tail->Next[C]确定指向C’;

节点D的Tail指针(黄色), 由绿色指针Father->Tail指向Root,因为root没有值为D的孩子节点,所以D的Tail直接指向Root.

3.匹配

在建立好Trie树,计算完Tail指针之后,就可以进行字符流的匹配了。

匹配过程中使用currentNode指针表示当前在树上的匹配位置,大致有以下几种匹配状态:

(1)currentNode字符与字符流当前字符相等

(1.1)currentNode被标记为end,则匹配到一个模式串,返回成功

(1.2)currentNode不是end,这时就是匹配失败了,需要通过Tail指针去匹配其他模式串。但是这种匹配失败是因为当前模式没有结束而导致的,后续继续输入的字符流可能使得当前模式串可以继续匹配成功,因此不能丢失当前位置,即currentNode保持不动,使用一个新的临时指针tmp,通过Tail去找有没有比当前模式短的后缀子串能够匹配上:

tmp=currentNode->Tail;
while(tmp && tmp->end==0){
    tmp = tmp->tail;
}

 如果通过Tail进行回溯过程中匹配到一个end,则表示有一个较短的模式命中,返回成功

否则直至Root(NULL)也没有命中则表示没有匹配到模式串,返回失败

if(tmp && tmp->end){
    return true;
}
return false;

(2)currentNode字符与字符流当前字符不相等

当前模式匹配失败,并且无法继续匹配,通过Tail指针切换到另一个模式进行匹配:

currentNode = currentNode->tail;

大致原理就是如此,最后奉上具体代码:

#define ALPHABET_SIZE 26
typedef struct _AcNode {
	struct _AcNode *father;
	struct _AcNode *tail;
	char value;
	char end;
    struct _AcNode *nexts[ALPHABET_SIZE];
} AcNode;

typedef struct {
    AcNode *root[ALPHABET_SIZE];
    AcNode *currentNode;
    int nodeCount;
} StreamChecker;

//#define MAX_SIZE 2000000
//AcNode *bfsQueue[MAX_SIZE];
void getTail(StreamChecker* streamChecker){
	AcNode **bfsQueue = (AcNode**)malloc(streamChecker->nodeCount*sizeof(AcNode*));
	int bfsQueueCount = 0;
	int bfsQueueEnd = 0;
	int bfsQueueStart = 0;
	AcNode **nexts = streamChecker->root;
	int i = 0;
    for(i=0; i<ALPHABET_SIZE; i++){
    	if(nexts[i]){
    		bfsQueueCount++;
    		bfsQueue[bfsQueueEnd] = nexts[i];
    		bfsQueueEnd++;
    	}
    }
    while(bfsQueueCount){
    	AcNode * node = bfsQueue[bfsQueueStart];
    	if(node->father==NULL){
    		node->tail = NULL;
    	}else{
            AcNode * father = node->father;
            while(node->tail==NULL && father && father->tail){
    		    node->tail = father->tail->nexts[node->value-'a'];
                father = father->tail;
            }
            if(node->tail==NULL){
                node->tail = streamChecker->root[node->value-'a'];
            }
    	}
    	bfsQueueStart++;
    	bfsQueueCount--;

    	nexts = node->nexts;
    	for(i=0; i<ALPHABET_SIZE; i++){
	    	if(nexts[i]){
	    		bfsQueueCount++;
	    		bfsQueue[bfsQueueEnd] = nexts[i];
	    		bfsQueueEnd++;
	    	}
	    }
    }
}

void trieTree(char ** words, int wordsSize, StreamChecker* streamChecker){
	int i = 0;
    for(i=0; i<ALPHABET_SIZE; i++){
    	streamChecker->root[i] = NULL;
    }
    streamChecker->currentNode = NULL;
    streamChecker->nodeCount = 0;
    for(i=0; i<wordsSize; i++){
    	AcNode *father = NULL;
    	AcNode *tail = NULL;
    	AcNode **nexts = streamChecker->root;
    	char *p = words[i];
    	
    	while(*p){
    		streamChecker->nodeCount++;
    		AcNode **next = &(nexts[(*p)-'a']);
    		if(*next==NULL){
    			*next = (AcNode*)malloc(sizeof(AcNode));
    			(*next)->father = father;
    			(*next)->tail = NULL;
    			(*next)->end = 0;
    			(*next)->value = *p;
    			int j = 0;
    			for(; j<ALPHABET_SIZE; j++){
			    	(*next)->nexts[j] = NULL;
			    }
    		}
    		father = *next;
    		nexts = (*next)->nexts;
    		p++;
    	}
    	father->end = 1;
    }
}

StreamChecker* streamCheckerCreate(char ** words, int wordsSize) {
    StreamChecker* streamChecker = (StreamChecker*)malloc(sizeof(StreamChecker));
    trieTree(words, wordsSize, streamChecker);
    getTail(streamChecker);
    return streamChecker;
}

bool streamCheckerQuery(StreamChecker* obj, char letter) {
    while(obj->currentNode){
    	AcNode **nodes = obj->currentNode->nexts;
        if(nodes[letter-'a']){//equal
            obj->currentNode = nodes[letter-'a'];
            if(nodes[letter-'a']->end){//equal and end
                return true;
            }
            else{//equal but not end
                AcNode *tmp = obj->currentNode->tail;
                while(tmp && tmp->end==0){
                    tmp = tmp->tail;
                }
                if(tmp && tmp->end){
                    return true;
                }
                return false;
            }
        }
        //fail
        obj->currentNode = obj->currentNode->tail;
    }
    AcNode **nodes = obj->root;
    if(nodes[letter-'a']){//equal
        obj->currentNode = nodes[letter-'a'];
        if(nodes[letter-'a']->end){//equal and end
            return true;
        }else{//not end
            return false;
        }
    }
    return false;//no equal
}

void nodeFree(AcNode* node){
	AcNode **nexts = node->nexts;
	int i = 0;
    for(i=0; i<ALPHABET_SIZE; i++){
    	if(nexts[i]){
    		nodeFree(nexts[i]);
    	}
    }
    free(node);
}
void streamCheckerFree(StreamChecker* obj) {
	int i = 0;
    for(i=0; i<ALPHABET_SIZE; i++){
    	if(obj->root[i]){
    		nodeFree(obj->root[i]);
    	}
    }
    free(obj);
}

/**
 * Your StreamChecker struct will be instantiated and called as such:
 * StreamChecker* obj = streamCheckerCreate(words, wordsSize);
 * bool param_1 = streamCheckerQuery(obj, letter);
 
 * streamCheckerFree(obj);
*/

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值