AC 自动机——多模式串匹配
网站上的敏感词过滤是怎么实现的呢?
实际上,这些功能最基本的原理就是字符串匹配算法,也就是通过维护一个敏感词的字典,当用户输入一段文字内容后,通过字符串匹配算法来检查用户输入的内容是否包含敏感词。
BF、RK、BM、KMP 算法都是针对只有一个模式串的字符串匹配算法,而要实现一个高性能的敏感词过滤系统,就需要用到多模式匹配算法了。
1. 基于单模式和 Trie 树实现的敏感词过滤
多模式匹配算法,就是在多个模式串和一个主串之间做匹配,也就是在一个主串中查找多个模式串。
敏感词过滤,也可以通过单模式匹配算法来实现,那就是针对每个敏感值都做一遍单模式匹配。但如果敏感词很多,并且主串很长,那我们就需要遍历很多次主串,显然这种方法是非常低效的。
而多模式匹配算法只需要扫描一遍主串,就可以一次性查找多个模式串是否存在,匹配效率就大大提高了。那如何基于 Trie 树实现敏感词过滤功能呢?
我们可以首先对敏感词字典进行预处理,构建成 Trie 树。这个预处理的操作只需要做一次,如果敏感词字典动态更新了,我们只需要在 Trie 树中添加或删除一个字符串即可。
用户输入一个文本内容后,我们把用户输入的内容作为主串,从第一个字符开始在 Trie 树中进行匹配。当匹配到叶子节点或者中途遇到不匹配字符的时候,我们就将主串的匹配位置后移一位,重新进行匹配。
基于 Trie 树的这种处理方法,有点类似单模式匹配的 BF 算法。我们知道 KMP 算法在 BF 算法基础上进行了改进,每次匹配失败时,尽可能地将模式串往后多滑动几位。同样,在这里,我们是否也可以对多模式串 Trie 树进行同样的改进呢?这就要用到 AC 自动机算法了。
2. AC 自动机多模式匹配算法
AC 自动机算法,全称是 Aho-Corasick 算法。AC 自动机实际上就是在 Trie 树之上,加了类似于 KMP 算法的 next 数组,只不过此处的数组是构建在树上罢了。
class ACNode
{
public:
char data;
bool is_ending_char; // 是否结束字符
int length; // 当前节点为结束字符时记录模式串长度
ACNode *fail; // 失败指针
ACNode *children[26]; // 字符集只包含 a-z 这 26 个字符
ACNode(char ch)
{
data = ch;
is_ending_char = false;
length = -1;
fail = NULL;
for (int i = 0; i < 26; i++)
children[i] = NULL;
}
};
AC 自动机的构建包含两个操作:
将多个模式串构建成 Trie 树;
在 Trie 树上构建失败指针,就相当于KMP 算法中的失效函数 next 数组。
构建 Trie 树的过程可以参考 Trie 树——搜索关键词提示,这里只是多了一个模式串的长度而已。假设我们的 4 个模式串分别为 c,bc,bcd,abcd,那么构建好的 Trie 树如下所示。
Trie 树中的每一个节点都有一个失败指针,它的作用和构建过程,和 KMP 算法中 next 数组极其相似。
假设我们沿着 Trie 树走到 p 节点,也就是下图中的紫色节点,那 p 的失败指针也就是从根节点走到当前节点所形成的字符串 abc,和所有模式串前缀匹配的最长可匹配后缀子串,这里就是 bc 模式串。
字符串 abc 的后缀子串有 c 和 bc,我们拿它们和其它模式串进行匹配,如果能够匹配上,那这个后缀就叫作可匹配后缀子串。在一个字符串的所有可匹配后缀子串中,长度最长的那个叫作最长可匹配后缀子串。我们就将一个节点的失败指针指向其最长可匹配后缀子串对应的模式串前缀的最后一个节点。
其实,如果我们把树中相同深度的节点放到同一层,那么某个节点的失败指针只有可能出现在它所在层的上面。因此,我们可以像 KMP 算法那样,利用已经求得的、深度更小的那些节点的失败指针来推导出下面节点的失败指针。
首先,根节点的失败指针指向 NULL,第一层节点的失败指针都指向根节点。然后,继续往下遍历,如果 p 节点的失败指针指向 q,那么我们需要看节点 p 的子节点 pc 对应的字符,是否也可以在节点 q 的子节点 qc 中找到。如果找到了一个子节点 qc 和 pc 的字符相同,则将 pc 的失败指针指向 qc。
如果找不到一个子节点 qc 和 pc 的字符相同,那么我们继续令 q = q->fail,重复上面的查找过程,直到 q 为根节点为止。如果还没有找到,那就将 pc 的失败指针指向根节点。
// 构建失败指针
void build_failure_pointer()
{
queue<ACNode *> AC_queue;
AC_queue.push(root);
while (!AC_queue.empty())
{
ACNode *p = AC_queue.front();
AC_queue.pop();
for (int i = 0; i < 26; i++)
{
ACNode *pc = p->children[i];
if (pc == NULL) continue;
if (p == root) pc->fail = root;
else
{
ACNode *q = p->fail;
while (q != NULL)
{
ACNode *qc = q->children[pc->data - 'a'];
if (qc != NULL)
{
pc->fail = qc;
break;
}
q = q->fail;
}
if (q == NULL) pc->fail = root;
}
AC_queue.push(pc);
}
}
}
通过按层来计算每个节点的子节点的失败指针,例中最后构建完之后的 AC 自动机就是下面这个样子。
接下来,我们看如何在 AC 自动机上匹配子串?首先,主串从 i=0 开始,AC 自动机从指针 p=root 开始,假设模式串是 b,主串是 a。
如果 p 指向的节点有一个等于 a[i] 的子节点 x,我们就更新 p 指向 x,这时候我们还要检查这个子节点的一系列失败指针对应的路径是否为一个完整的模式串,之后我们将 i 增 1,继续重复这两个过程;
如果 p 指向的节点没有等于 a[i] 的子节点,我们就更新 p = p->fial,继续重复这两个过程。
// 在 AC 自动机中匹配字符串
void match_string(const char str[])
{
ACNode *p = root;
for (unsigned int i = 0; i < strlen(str); i++)
{
int index = int(str[i] - 'a');
while (p->children[index] == NULL && p != root)
{
p = p->fail;
}
p = p->children[index];
if (p == NULL) p = root; // 没有可匹配的,从根节点开始重新匹配
ACNode *temp = p;
while (temp != root)
{
if (temp->is_ending_char == true)
{
int pos = i - temp->length + 1;
cout << "Fing a match which begins at position " << pos << ' '
<< "and has a length of " << temp->length << '!'<< endl;
}
temp = temp->fail;
}
}
}
全部代码如下:
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
class ACNode
{
public:
char data;
bool is_ending_char; // 是否结束字符
int length; // 当前节点为结束字符时记录模式串长度
ACNode *fail; // 失败指针
ACNode *children[26]; // 字符集只包含 a-z 这 26 个字符
ACNode(char ch)
{
data = ch;
is_ending_char = false;
length = -1;
fail = NULL;
for (int i = 0; i < 26; i++)
children[i] = NULL;
}
};
class AC
{
private:
ACNode *root;
public:
// 构造函数,根节点存储无意义字符 '/'
AC()
{
root = new ACNode('/');
}
// 向 Trie 树中添加一个字符串
void insert_string(const char str[])
{
ACNode *cur = root;
for (unsigned int i = 0; i < strlen(str); i++)
{
int index = int(str[i] - 'a');
if (cur->children[index] == NULL)
{
ACNode *temp = new ACNode(str[i]);
cur->children[index] = temp;
}
cur = cur->children[index];
}
cur->is_ending_char = true;
cur->length = strlen(str);
}
// 构建失败指针
void build_failure_pointer()
{
queue<ACNode *> AC_queue;
AC_queue.push(root);
while (!AC_queue.empty())
{
ACNode *p = AC_queue.front();
AC_queue.pop();
for (int i = 0; i < 26; i++)
{
ACNode *pc = p->children[i];
if (pc == NULL) continue;
if (p == root) pc->fail = root;
else
{
ACNode *q = p->fail;
while (q != NULL)
{
ACNode *qc = q->children[pc->data - 'a'];
if (qc != NULL)
{
pc->fail = qc;
break;
}
q = q->fail;
}
if (q == NULL) pc->fail = root;
}
AC_queue.push(pc);
}
}
}
// 在 AC 自动机中匹配字符串
void match_string(const char str[])
{
ACNode *p = root;
for (unsigned int i = 0; i < strlen(str); i++)
{
int index = int(str[i] - 'a');
while (p->children[index] == NULL && p != root)
{
p = p->fail;
}
p = p->children[index];
if (p == NULL) p = root; // 没有可匹配的,从根节点开始重新匹配
ACNode *temp = p;
while (temp != root)
{
if (temp->is_ending_char == true)
{
int pos = i - temp->length + 1;
cout << "Fing a match which begins at position " << pos << ' '
<< "and has a length of " << temp->length << '!'<< endl;
}
temp = temp->fail;
}
}
}
};
int main()
{
//char str[][8] = {"how", "he", "her", "hello", "so", "see", "however"};
char str[][5] = {"abce", "bcd", "ce"};
AC test;
for (int i = 0; i < 7; i++)
{
test.insert_string(str[i]);
}
test.build_failure_pointer();
//test.match_string("however, what about her boyfriend?");
test.match_string("abcfabce");
return 0;
}
3. AC 自动机的复杂度分析
首先,构建 Trie 树的时间复杂度为 O(m*len),其中 len 表示敏感词的平均长度,m 表示敏感词的个数。
其次,假设 Trie 树中总共有 k 个节点,每个节点在构建失败指针的时候,最耗时的就是 while 循环部分,这里 q = q->fail,每次节点的深度都在减小,树的最大深度为 len,因此每个节点构建失败指针的时间复杂度为 O(len),整个失败指针构建过程的时间复杂度为 O(k*len)。不过,AC 自动机的构建过程都是预先处理好的,构建好之后并不会频繁更新。
最后,假设主串的长度为 n,匹配的时候每一个 for 循环里面的时间复杂度也为 O(len),总的匹配时间复杂度就为 O(n*len)。因为敏感词不会很长,而且这个时间复杂度只是一个非常宽泛的上限,实际情况下,可能近似于 O(n),所以,AC 自动机匹配的效率非常高。
从时间复杂度上看,AC 自动机匹配的效率和 Trie 树一样,但是一般情况下,大部分节点的失败指针都指向根节点,AC 自动机实际匹配的效率要远高于 O(n*len)。只有在极端情况下,AC 自动机的性能才会退化为和 Trie 树一样。
获取更多精彩,请关注「seniusen」!
AC 自动机——多模式串匹配的更多相关文章
- AC自动机——多模式串匹配的算法思想
标准KMP算法用于单一模式串的匹配,即在母串中寻求一个模式串的匹配,但是现在又存在这样的一个问题,如果同时给出多个模式串,要求找到这一系列模式串在母串存在的匹配个数,我们应该如何处理呢? 基于KMP算 ...
- AC自动机 - 多模式串匹配问题的基本运用 + 模板题 --- HDU 2222
Keywords Search Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)T ...
- AC自动机 - 多模式串的匹配运用 --- HDU 2896
病毒侵袭 Problem's Link:http://acm.hdu.edu.cn/showproblem.php?pid=2896 Mean: 略 analyse: AC自动机的运用,多模式串匹配. ...
- AC自动机 - 多模式串的匹配 --- HDU 3695 Computer Virus on Planet Pandora
Problem's Link Mean: 有n个模式串和一篇文章,统计有多少模式串在文章中出现(正反统计两次). analyse: 好久没写AC自动机了,回顾一下AC自动机的知识. 本题在构造文章的时 ...
- AC自动机 - 多模式串的匹配运用 --- HDU 3065
病毒侵袭持续中 Problem's Link:http://acm.hdu.edu.cn/showproblem.php?pid=3065 Mean: 略 analyse: AC自动机的运用. 这一题 ...
- 数据结构14——AC自动机
一.相关介绍 知识要求 字典树Trie KMP算法 AC自动机 多模式串的字符匹配算法(KMP是单模式串的字符匹配算法) 单模式串问题&多模式串问题 单模就是给你一个模式串,问你这个模式串是否 ...
- 【转载】多模式串匹配之AC自动机
原文地址:https://www.cnblogs.com/codeape/p/3845375.html 目录 [隐藏] 一.概述 二.AC算法思想 三.字典树tire的构造 四.搜索路径的确定 附录: ...
- UVA 11019 Matrix Matcher ( 二维字符串匹配, AC自动机 || 二维Hash )
题目: 传送门 题意: 给你一个 n * m 的文本串 T, 再给你一个 r * c 的模式串 S: 问模式串 S 在文本串 T 中出现了多少次. 解: 法一: AC自动机 (正解) 670ms 把模 ...
- Aho-Corasick 多模式匹配算法、AC自动机详解
Aho-Corasick算法是多模式匹配中的经典算法,目前在实际应用中较多. Aho-Corasick算法对应的数据结构是Aho-Corasick自动机,简称AC自动机. 搞编程的一般都应该知道自动机 ...
随机推荐
- 2017年终总结&展望2018年
转眼就要挥别2017年了,也看到好多人都在叹时间过得好快.对啊,在我的印象中时间过得慢的时期恐怕只有中小学期间了,转眼研究生阶段已经过了一半.如今这个阶段,很多时候忙任务和学东西好不容易觉得自己摸到点 ...
- SQL优化例子
如下SQL的优化: select count(*) from ( select id,name,col1,col2 from t1 where name='xxxx' union select id ...
- Linux基础(05)、Linux进阶命令
目录 一.进阶命令 二.系统命令 三.压缩和归档 3.1.归档 3.2.压缩 3.3.归档并压缩 归档.接档:tar -cf.tar -tvf 压缩.解压:gzip.gunzip 归档并压缩:tar ...
- centos总结linux下svn安装与使用
一.安装篇 centos下yum安装 yum install subversion 查看安装是否成功: svnserve --version 查看安装内容与位置 rpm -ql subversion ...
- js数组去重(多种写法)
最基本的写法 使用indexOf() var arr = [1,1,5,77,32,54,2,4,5,2,2,4,52,2,2,2,2,2] //比较常规的语法使用indexOf来判断是否已经存在 g ...
- 第7章 YARN HA配置
目录 7.1 yarn-site.xm文件配置 7.2 测试YARN自动故障转移 ResourceManager (RM)负责跟踪集群中的资源,以及调度应用程序(例如,MapReduce作业).在Ha ...
- 【NXP开发板应用—智能插排】3.驱动GPIO点亮外接LED
[前言] 首先感谢深圳市米尔科技有限公司举办的这次活动并予以本人参加这次活动的机会,以往接触过嵌入式,但那都是皮毛,最多刷个系统之类的,可以说对于嵌入式系统开发这件事情是相当非常陌生的,这次活动为我提 ...
- 关于C链表的实现
学习了数据结构后,自己学习写了一个链表的程序.初步功能是实现了.但是不知道会不会有一些隐含的问题.所以希望大佬指导指导 /******************/ /*一个小的链表程序*/ /***** ...
- 函数调用方法之__cdecl与_stdcall
在debug VS c工程文件时,碰到cannot convert from 'int (__cdecl *)(char *)' to 'xxx'这个问题,发现问题在于typedef函数指针类型时,将 ...
- SpaceVim 语言模块 elm
原文连接: https://spacevim.org/cn/layers/lang/elm/ 模块简介 功能特性 依赖安装及启用模块 依赖安装 启用模块 快捷键 语言专属快捷键 交互式编程 模块简介 ...