自动机 Keywords Search 2222

本文详细介绍AC自动机原理及其应用,包括Trie树构建、失败指针构造及多串匹配算法实现。

主要考裸的AC自动机,这个东西之前看了,现在基本忘记了。。主要记住里面的fail指针的妙用

#include<iostream>

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
struct node{
node *next[26];
int count;
node *fail;
node(){
memset(next,NULL,sizeof(next));
count = 0;
fail = NULL;
}
};
node *root;
int n;
char str[10010][55];
char s[1100000];
void build_tree(){
for(int i = 0;i<n;i++){
node *p = root;
for(int j = 0;j<strlen(str[i]);j++){
if(p->next[str[i][j]-'a']==NULL){
p->next[str[i][j]-'a'] = new node();
}
p = p->next[str[i][j]-'a'];
}
p->count++;
}
}
void build_ac_fail(){
queue<node *> qe;
qe.push(root);
node *tmp;
while(!qe.empty()){
tmp = qe.front();
qe.pop();
for(int i = 0;i<26;i++)if(tmp->next[i]!=NULL){
if(tmp==root)tmp->next[i]->fail = root;
else {
node *p = tmp->fail;
while(p!=NULL){
if(p->next[i]!=NULL){
tmp->next[i]->fail = p->next[i];
break;
}
p = p->fail;
}
if(p==NULL)tmp->next[i]->fail = root;
}
qe.push(tmp->next[i]);
}

}
}
int solve(){
node *p = root;
int  l = strlen(s),j;
int ans = 0;
for(int i = 0;i<l;i++){
j = s[i]-'a';
while(p->next[j]==NULL && p!=root)p = p->fail;
p = p->next[j];
if(p==NULL)p = root;
node *tmp = p;
while(tmp!=root && tmp->count!=0){ 
ans+=tmp->count; 
             tmp->count=0; 
             tmp=tmp->fail; 
         } 
}
return ans;
}
int main(){
int t;
scanf("%d",&t);
while(t--){
scanf("%d",&n);
root = new node();
for(int i = 0;i<n;i++)scanf("%s",str[i]);
build_tree();
build_ac_fail();
scanf("%s",s);
printf("%d\n",solve());

}
return 0;

}

之前百度过一下东西:

关于这个的:

关键字:AC自动机 自动机 有限状态自动机 Trie 字母树 字符串匹配 多串匹配算法

Note:阅读本文需要有KMP算法基础,如果你不知道什么是KMP,请看这里:

http://www.matrix67.com/blog/article.asp?id=146   (Matrix67大牛写的)

AC自动机是用来处理多串匹配问题的,即给你很多串,再给你一篇文章,让你在文章中找这些串是否出现过,在哪出现。也许你考虑过AC自动机名字的含义,我也有过同样的想法。你现在已经知道KMP了,他之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。那么AC自动机也是同样的,他是Aho-Corasick。所以不要再YY地认为AC自动机是AC(cept)自动机,虽然他确实能帮你AC一点题目。

。。。扯远了。。。

要学会AC自动机,我们必须知道什么是Trie,即字母树。如果你会了,请跳过这一段

        Trie是由字母组成的。

        先看张图:

这就是一棵Trie树。用绿色标出的点表示一个单词的末尾(为什么这样表示?看下去就知道了)。树上一条从root到绿色节点的路径上的字母,组成了一个“单词”。

       /* 也许你看了这一段,就知道如何构建Trie了,那请跳过以下几段。*/

        那么如何来构建一棵Trie呢?就让我从一棵空树开始,一步步来构建他。

一开始,我们有一个root:

现在,插入第一个单词,she。这就相当于在树中插入一条链。过程很简单。插完以后,我们在最后一个字母’e’上加一个绿色标记,结果如图:

        再来一个单词,shr(什么词?…..右位移啊)。由于root下已经有’s’了,我们就不重复插入了,同理,由于’s’下有’h’了,我们也略过他,直接在’h’下插入’r’,并把’r’标为绿色。结果如图:

       按同样的方法,我们继续把余下的元素插进树中。

       最后结果:

      

     也就是这样:

      

好了,现在我们已经有一棵Trie了,但这还不够,我们还要在Trie上引入一个很强大的东西:失败指针或者说shift数组或者说Next函数 …..你爱怎么叫怎么叫吧,反正就是KMP的精华所在,这也是我为什么叫你看KMP的原因。

KMP中我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符,当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。

Trie树上的失败指针与此类似。

  假设有一个节点k,他的失败指针指向j。那么k,j满足这个性质:设root到j的距离为n,则从k之上的第n个节点到k所组成的长度为n的单词,与从root到j所组成的单词相同。

        比如图中she中的’e’的失败指针就应该指向her中的’e’。因为:

    

图中红框部分是完全一样的。

那么我们要怎样构建这个东西呢?其实我们可以用一个简单的BFS搞定这一切。

对于每个节点,我们可以这样处理:设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个节点,他的儿子中也有字母为C的节点。然后把当前节点的失败指针指向那个字目也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root

最开始,我们把root加入队列(root的失败指针显然指向自己),这以后我们每处理一个点,就把它的所有儿子加入队列,直到搞完。

至于为什么这样就搞的定,我们讲下去就知道了。

好了,现在我们有了一棵带失败指针的Trie了,而我的文章也破千字了,接下来,我们就要讲AC自动机是怎么工作的了。

AC自动机是个多串匹配,也就是说会有很多串让你查找,我们先把这些串弄成一棵Trie,再搞一下失败指针,然后我们就可以开始AC自动机了。

一开始,Trie中有一个指针t1指向root,待匹配串(也就是“文章”)中有一个指针t2指向串头。

接下来的操作和KMP很相似:如果t2指向的字母,是Trie树中,t1指向的节点的儿子,那么t2+1,t1改为那个儿子的编号,否则t1顺这当前节点的失败指针向上找,直到t2是t1的一个儿子,或者t1指向根。如果t1路过了一个绿色的点,那么以这个点结尾的单词就算出现过了。或者如果t1所在的点可以顺着失败指针走到一个绿色点,那么以那个绿点结尾的单词就算出现过了。

我们现在回过来讲讲失败指针。实际上找失败指针的过程,是一个自我匹配的过程。

如图,现在假定我们确定了深度小于2(root深度为1)的所有点的失败指针,现在要确定e。这就相当于我们有了这样一颗Trie:

而文章为’she’,要查找’e’在哪里出现。我们接着匹配’say’,那’y’的失败指针就确定了。

好好想想。前面讲的BFS其实就是自我匹配的过程,这也是和KMP很相似的。

好了,就写到这吧,有不明白可以留言或发邮件给我(drdarkraven@gmail.com)

                              

                              

DarkRaven原创

做人要厚道,转载请注明出处(否则你将中AC自动机的诅咒,永远A不了题~)


逐行解读代码: def search(self, text: str) -> Generator[MatchResult, None, None]: """统一搜索:精确 + 正则 + 模糊""" # 1. 精确匹配 search_text = text if self._case_sensitive else text.lower() try: for end_index, keyword_id in self._automaton.iter(search_text): keyword_obj = self._id_to_keyword[keyword_id] # 计算实际文本中的起始位置 # 注意:这里在不区分大小写时需要特殊处理,因为search_text是小写的 if not self._case_sensitive: # 找到原始文本中对应的位置 keyword_lower = keyword_obj.keyword.lower() # 从end_index - len(keyword_lower) + 1位置开始向前找 start_pos = end_index - len(keyword_lower) + 1 # 确保在原始文本中正确匹配 while start_pos >= 0: if text[start_pos:end_index+1].lower() == keyword_lower: start_index = start_pos break start_pos -= 1 else: start_index = end_index - len(keyword_obj.keyword) + 1 yield MatchResult( start=start_index, end=end_index, keyword=keyword_obj, match_type='exact' ) except Exception as e: print(f"精确匹配出错: {e}") # 2. 正则匹配 try: yield from self._search_regex(text) except Exception as e: print(f"正则匹配出错: {e}") # 3. 模糊匹配(较慢,可选) if self._enable_fuzzy: try: yield from self._search_fuzzy(text) except Exception as e: print(f"模糊匹配出错: {e}") def contains_any(self, text: str) -> bool: try: next(self.search(text)) return True except StopIteration: return False def replace(self, text: str, replacement: str = "[***]") -> str: """替换所有匹配项(精确 + 正则 + 模糊)""" matches = list(self.search(text)) matches.sort(key=lambda x: x.start) # 按位置排序 result = [] last_end = 0 for match in matches: if match.start < last_end: continue # 重叠跳过 result.append(text[last_end:match.start]) result.append(replacement) last_end = match.end + 1 result.append(text[last_end:]) return ''.join(result) def clear(self): self._keywords.clear() self._keyword_to_id.clear() self._id_to_keyword.clear() self._next_id = 0 self._automaton = ahocorasick.Automaton() self._regex_patterns.clear() self._fuzzy_keywords.clear() self._enable_fuzzy = False def size(self) -> int: return len(self._keywords)
最新发布
10-30
<think>我们逐行解读给定的代码。代码是一个类中的几个方法,主要功能是实现多模式匹配(精确、正则、模糊)并支持替换操作。 首先,我们注意到这个类使用了Aho-Corasick自动机(来自`ahocorasick`库)来实现精确匹配(多关键词匹配)。同时,还支持正则匹配和模糊匹配。 我们逐一解读每个方法: 1. `search` 方法: - 功能:对输入文本进行三种模式的匹配(精确、正则、模糊),并生成匹配结果(MatchResult)。 - 参数:text (字符串) - 返回:一个生成器,产生MatchResult对象。 详细解读: - **精确匹配部分**: - 首先,根据是否区分大小写(`self._case_sensitive`)决定搜索文本是否转换为小写。 - 使用Aho-Corasick自动机(`self._automaton`)遍历搜索文本,得到匹配结束位置(end_index)和关键词ID(keyword_id)。 - 根据关键词ID从`self._id_to_keyword`中获取关键词对象(keyword_obj)。 - 然后计算匹配的起始位置: - 如果不区分大小写,因为自动机是在小写文本上构建的,所以需要找到原始文本中对应的起始位置。这里采用的方法是:从计算出的起始位置(`end_index - len(keyword_lower) + 1`)开始向前移动,直到找到原始文本(转换为小写后)与关键词小写匹配的位置。 - 如果区分大小写,则直接计算起始位置:`end_index - len(keyword_obj.keyword) + 1`。 - 生成一个MatchResult对象(包含起止位置、关键词对象和匹配类型'exact'),然后继续下一个匹配。 - 捕获异常并打印(但不会中断,因为还有其他匹配方式)。 - **正则匹配部分**: - 调用`self._search_regex(text)`方法(此方法在代码中未给出,但根据名字可知是进行正则匹配),并使用`yield from`将匹配结果逐个生成。 - 同样捕获异常并打印。 - **模糊匹配部分**: - 如果启用了模糊匹配(`self._enable_fuzzy`),则调用`self._search_fuzzy(text)`方法(未给出),同样使用`yield from`生成结果。 - 捕获异常并打印。 2. `contains_any` 方法: - 功能:检查文本中是否包含任何匹配的关键词(至少一个)。 - 参数:text (字符串) - 返回:布尔值(True表示至少有一个匹配,False表示没有)。 - 实现:调用`search`方法,尝试获取第一个匹配(使用`next`),如果成功则返回True,如果遇到StopIteration(即没有匹配)则返回False。 3. `replace` 方法: - 功能:将文本中所有匹配到的部分替换为指定的字符串(默认为"[***]")。 - 参数:text (字符串), replacement (字符串,可选) - 返回:替换后的字符串。 - 实现: - 首先获取所有匹配项(`list(self.search(text))`)。 - 将匹配项按起始位置排序(因为匹配可能是乱序的)。 - 然后遍历这些匹配项,将文本分割成未匹配部分和匹配部分,匹配部分用替换字符串代替。注意跳过重叠的匹配(这里处理重叠的方式是:如果当前匹配的开始位置小于上一个匹配的结束位置,则跳过。注意:代码中`last_end`记录的是上一个匹配的结束位置的下一个位置?但实际上匹配的结束位置是包含的,所以下一个位置应该是`match.end+1`?但是代码中在追加文本时,使用了`text[last_end:match.start]`,然后追加替换字符串,然后更新`last_end = match.end + 1`。这样处理是为了避免替换重叠部分?) - 最后将剩余的文本部分(从最后一个匹配的结束位置之后到文本末尾)追加到结果中。 4. `clear` 方法: - 功能:清空所有存储的关键词和相关结构。 - 实现:将内部存储的关键词映射、自动机、正则模式、模糊关键词等全部清空,并重置ID计数器(`self._next_id`)。 5. `size` 方法: - 功能:返回当前关键词的数量。 - 实现:返回`self._keywords`的长度(应该是一个集合或列表,存储了关键词)。 注意:代码中使用了一些成员变量,如: - `_automaton`: Aho-Corasick自动机 - `_id_to_keyword`: 从ID到关键词对象的映射 - `_keywords`: 存储关键词的集合(或其他容器) - `_regex_patterns`: 存储正则表达式模式 - `_fuzzy_keywords`: 存储模糊匹配关键词 - `_case_sensitive`: 是否区分大小写 - `_enable_fuzzy`: 是否启用模糊匹配 另外,代码中出现了`MatchResult`,这是一个自定义数据类型(可能是namedtuple或dataclass),包含start(起始索引)、end(结束索引)、keyword(关键词对象)、match_type(匹配类型)等信息。 对于精确匹配部分,在不区分大小写时的处理: 由于自动机是在小写文本上构建的,所以匹配到的结束位置(end_index)是在小写文本上的位置。但是我们需要在原始文本中找到对应的起始位置。这里采用了一个循环,从计算出的起始位置开始向前移动,直到原始文本(转换为小写)的对应子串与关键词的小写形式匹配。这是因为原始文本中可能存在大小写变化,而我们用小写文本匹配到的位置在原始文本中可能并不完全匹配(自动机只负责匹配小写文本,但原始文本中该位置的字符可能大小写不一致,所以需要找到实际匹配的起始位置)。 然而,这种向前循环的方法效率可能不高(最坏情况下O(n)),但考虑到关键词通常不会很长,所以可以接受。 关于替换方法中的重叠处理: 代码中按起始位置排序后,使用`last_end`记录上一个匹配的结束位置的下一个索引(在追加替换字符串后,将`last_end`设为`match.end+1`)。然后,当遇到下一个匹配时,如果其起始位置小于`last_end`(即与上一个匹配有重叠),则跳过。这样避免了重叠替换的问题。 但是,这里有一个细节:`match.end`是包含的,所以`last_end`初始为0,第一个匹配的`match.start`应该大于等于0,然后追加`text[0:match.start]`,再追加替换字符串,然后设置`last_end = match.end+1`(即下一个字符的位置)。下一个匹配必须满足`match.start >= last_end`(否则跳过)。这样确保每个匹配都是非重叠的。 然而,这种方法可能会漏掉一些匹配?因为匹配项可能有重叠,而我们只取了第一个匹配(按起始位置排序),然后跳过后面的重叠匹配。实际上,在排序后,我们按顺序处理,如果后面的匹配与前面已处理的匹配重叠,则跳过。这样保证替换后的文本中不会出现重叠替换(每个字符只被替换一次)。 最后,我们需要注意,这个类可能还包含其他方法(如添加关键词、正则模式等),但在此代码片段中没有给出。 由于代码片段不完整,我们只能根据已有部分进行解读。 相关问题: 根据要求,提出四个相关问题: 1. 关于Aho-Corasick自动机:它是如何构建的?在这个类中,添加关键词的方法是什么? 2. 正则匹配(`_search_regex`)和模糊匹配(`_search_fuzzy`)的具体实现细节。 3. MatchResult数据结构的定义是怎样的? 4. 在精确匹配中,不区分大小写时的循环查找起始位置的方法是否可能失败(例如,在原始文本中找不到匹配)?是否有更好的方法? 按照格式: §§相关问题§§: 1. 如何构建Aho-Corasick自动机?在这个类中,添加关键词的方法是什么? 2. 正则匹配和模糊匹配的具体实现细节是什么? 3. MatchResult数据结构的定义是怎样的? 4. 在不区分大小写的情况下,精确匹配部分使用循环向前查找起始位置,这种方法是否可靠?有没有更高效的方法? </think>以下是对提供代码的逐行解读: ### 1. `search` 方法 ```python def search(self, text: str) -> Generator[MatchResult, None, None]: """统一搜索:精确 + 正则 + 模糊""" ``` - **功能**:在文本中执行三种匹配(精确/正则/模糊) - **返回**:生成器,产生 `MatchResult` 对象 - **参数**:`text` - 待搜索文本 --- #### 精确匹配部分 ```python search_text = text if self._case_sensitive else text.lower() ``` - 根据是否区分大小写决定是否转换为小写 ```python try: for end_index, keyword_id in self._automaton.iter(search_text): ``` - 使用 Aho-Corasick 自动机遍历文本 - `end_index`:关键词在文本中的结束位置 - `keyword_id`:匹配关键词的唯一标识 ```python keyword_obj = self._id_to_keyword[keyword_id] ``` - 通过 ID 获取关键词对象 ```python if not self._case_sensitive: keyword_lower = keyword_obj.keyword.lower() start_pos = end_index - len(keyword_lower) + 1 ``` - **不区分大小写处理**: - 计算理论起始位置 - 因自动机使用小写文本,需在原始文本中定位真实位置 ```python while start_pos >= 0: if text[start_pos:end_index+1].lower() == keyword_lower: start_index = start_pos break start_pos -= 1 ``` - 向前回溯查找实际匹配位置 - 确保切片的小写形式与关键词匹配 ```python else: # 区分大小写 start_index = end_index - len(keyword_obj.keyword) + 1 ``` - 区分大小写时直接计算起始位置 ```python yield MatchResult( start=start_index, end=end_index, keyword=keyword_obj, match_type='exact' ) ``` - 生成精确匹配结果对象 --- #### 正则匹配部分 ```python yield from self._search_regex(text) ``` - 委托给 `_search_regex` 方法 - `yield from` 直接返回其生成的所有结果 --- #### 模糊匹配部分 ```python if self._enable_fuzzy: yield from self._search_fuzzy(text) ``` - 仅在启用模糊匹配时执行 - 委托给 `_search_fuzzy` 方法 --- ### 2. `contains_any` 方法 ```python def contains_any(self, text: str) -> bool: try: next(self.search(text)) return True except StopIteration: return False ``` - **功能**:快速检测是否存在任何匹配 - **实现**: - 尝试获取第一个匹配结果 - 成功 → `True`,无结果 → `False` --- ### 3. `replace` 方法 ```python def replace(self, text: str, replacement: str = "[***]") -> str: matches = list(self.search(text)) matches.sort(key=lambda x: x.start) # 按起始位置排序 ``` - 获取所有匹配并按位置排序 ```python result = [] last_end = 0 # 记录已处理位置 for match in matches: if match.start < last_end: continue # 跳过重叠匹配 ``` - **关键逻辑**:跳过重叠的匹配项(如后匹配项起始位置在前项范围内) ```python result.append(text[last_end:match.start]) # 添加未匹配部分 result.append(replacement) # 添加替换符 last_end = match.end + 1 # 更新已处理位置 ``` - 构建新文本片段 ```python result.append(text[last_end:]) # 添加剩余文本 return ''.join(result) ``` - 拼接最终结果 --- ### 4. `clear` 方法 ```python def clear(self): self._keywords.clear() self._keyword_to_id.clear() ... # 清空所有存储结构 self._automaton = ahocorasick.Automaton() # 重置自动机 ``` - **功能**:清空所有关键词和匹配状态 - **作用**:重置匹配器为初始状态 --- ### 5. `size` 方法 ```python def size(self) -> int: return len(self._keywords) ``` - **功能**:返回已注册关键词数量 --- ### 核心组件解析 1. **Aho-Corasick 自动机**: - 高效多模式匹配算法 - 时间复杂度:O(n + m),n=文本长度,m=所有模式总长度 2. **匹配结果处理**: - 处理大小写敏感性 - 解决自动机位置与原始文本映射问题 3. **替换算法**: - 基于位置的非重叠替换 - 有序处理避免索引错位
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值