彻底理解AC多模式匹配算法
(本文尤其适合遍览网上的讲解而仍百思不得姐的同学)
一、原理
AC自动机首先将模式组记录为Trie字典树的形式,以节点表示不同状态,边上标以字母表中的字符,表示状态的转移。根节点状态记为0状态,表示起始状态。当一个状态处有一个模式串终结则标记一下。
目前流传较多的讲解多大同小异,尤其是配图,基本采用的是Aho和Corasiek两位巨巨的文章efficient string matching an aid to bibliographic search里的,窃以为那张示意图存在失配点靠前的特点(什么是失配?往下看),看起来稍稍费劲。
我找了样例画了一套新图,主要目标是通过稍微的夸张(失配点远离)让过程更清晰。
匹配的过程是:从0状态起点开始,以字符流输入,进行适当的状态转移,如果可以抵达某一标记终结的状态,则成功匹配模式,串值为从0到终结点的路径。
按照传统的说法,状态机有三个主要函数支撑:goto(状态正常转移),fail(状态失配转移),output(传回匹配结果),而我认为与其规定是具体的函数,倒不如说是三个功能的模块,有不同于函数的实现形式。
Trie树的建立是简单的,在此基础上我们要完善更多的数据结构,实现goto的功能。
goto是自动机基本的状态转移过程,很好想,就是在建立Trie树时让每个状态维护一组指针(广义的),使得在每一状态对于输入都可以正确转移,没有对应的则报错(现在回答刚才的问题,什么是失配?失配就是一个状态接受了无法转移的字符,记fail)。除了字典树中的树枝以外,还有一个转移就是在开始节点,对于不能流进自动机的字符,不报错而是再一次转到开始节点(如上图示),很好理解,对于待匹配串λthis,λ为不含t,h的任意串,真正的模式匹配是在去除了它以后开始的。(当然还有其他的用意,坑稍后填)
好了,正常的状态流转已经建立好了,现在看失配时我们的状态流何去何从。举一个栗子,如果输入thip这个串,状态的流转应该如下图:
那3状态处报错后应该怎么处理呢?最好想的方法当然是错开一位,再从头开始匹配(这种方法就像一位老人家曾经说过,太年轻太简单,有时还很幼稚),AC二位的办法是——利用图中的关系计算出一套跳转关系——在x点处失配的串不打回开头来过,而是跳到y点——继续匹配当前字符。这套规则叫做失配函数,也就是fail功能模块。要点在于当前字符不向前回溯,想想着很适合字符流的关键字匹配对不对。
好了,告诉大家3状态的失配跳转在6状态,先不用管怎么得到的,想想这个过程:3得到p字符,失配,凭goto无法转移状态,使用失配时通用的fail,状态跳至6,接受p——还是这个字符,成功匹配到终结状态7。单趟遍历目标串,cool。
当然这套规则是需要小心计算的。采用的方法很巧妙,在树形结构中很像广度优先搜索BFS,数学形式又很像动态规划DP。
正式开始之前请认真思考这个情况:已知2状态的失配跳转为5,怎么求3状态的失配跳转?从图中很容易看出,2通过i流向3,而5恰有对i的goto,自然地,3失配时可以跳转至6,哒哒
现在我把图小小地改动一下,把hip变成hop,我们都喜欢hip-hop~:
2的失配跳转仍然是5,然而对于所有使2不失配的字符,5都没有合适的goto——即会在5也失配,此种情况怎么求3的失配跳转?
请仔细读这两句话:
2的失配跳转说明不能采用前缀th
5的失配跳转说明不能采用前缀h(现在不要想2的事情了,单独想5)
——失配跳转实际上是一个逐字符推后匹配模式前缀的过程
那么既然h开头的也不能匹配模式了,那么对于目标串,要从i开始匹配了——下一次状态就是5的失配跳转0!
这是一个向前递归的过程,而前面提到0的大量无匹配字符均指回0自己则巧妙地保证了这个递归会最不济也在会0处停下:这种时候则是放弃之前的全部前缀从当前字符重新开始尝试匹配了,对吧。
我要强调,失配跳转的过程中当前字符是不变的。
至此,我们也完整的构造了fail模块的规则。
output需要做的则是对匹配路径上的每一状态,检查是否为一个模式的终结,如果是,用一种合适的方式传回这个匹配的结果。
Another question!目标串全部模式匹配:在匹配到一个模式后,应当驱动自动机继续无遗漏地匹配下一个出现的模式(这个下一模式也许会和已匹配的部分或全部重叠),我再次重复这句话,失配跳转实际上是一个逐字符推后匹配模式前缀的过程,那么应该很简单了吧,匹配到一个模式后自然一次失配跳转就行了!自动机会把前缀去掉一个字符继续匹配。
二、关于自动机的数据结构表示
我在原理中避开一个一定要解释清楚的问题,就是自动机的数据结构实现。Aho & Corasiek的论文中称为goto/fail/output function,与其理解为函数倒不如说是功能,因为它们的实现不必是有输入输出的函数,而可以是向更直接的数据结构直接查询。
我实践中认为易于实现的写法:goto功能就可以实现在结点结构中,每个状态维护一个转向结点的指针,无效则置空;fail即可以是一张自动机维护的表;output在结点中标记是否终结,如果终结,状态结点存储模式串,检测到终结直接传回。
三、完整代码
#include <cstdlib>
#include <set>
#include <string>
#include <vector>
#include <queue>
#include <iostream> using namespace std; #define ALPHABET_NUMBER 26 struct StateNode
{
bool finish_{ false };
int state_{ };
string pattern_{};
//goto table
vector<StateNode *> transition_table_{ vector<StateNode *>(ALPHABET_NUMBER) };
}; class ACSM
{
private:
StateNode *start_node_;
int state_count_;
vector<StateNode *> corresponding_node_;
vector<StateNode *> fail_;
public:
ACSM() :start_node_{ new StateNode() }, state_count_{ }
{
//state0 is start_node_
corresponding_node_.push_back(start_node_);
}
//read all patterns and produce the goto table
void load_pattern(const vector<string> &_Patterns)
{
int latest_state = ;
for (const auto &pattern : _Patterns)
{
auto *p = start_node_;
for (int i = ; i < pattern.size(); ++i)
{
auto *next_node = p->transition_table_[pattern[i] - 'a'];
if (next_node == nullptr)
{
next_node = new StateNode();
}
if (next_node->state_ == )
{
next_node->state_ = latest_state++;
//update the table
corresponding_node_.push_back(next_node);
}
//the goto table
p->transition_table_[pattern[i] - 'a'] = next_node;
p = next_node;
}
p->finish_ = true;
p->pattern_ = pattern;
}
for (int i = ; i < ALPHABET_NUMBER; ++i)
{
if (start_node_->transition_table_[i] == nullptr)
{
start_node_->transition_table_[i] = start_node_;
}
}
state_count_ = latest_state;
}
//produce fail function
void dispose()
{
queue<StateNode *> q;
fail_ = std::move(vector<StateNode *>(state_count_));
for (const auto nxt : start_node_->transition_table_)
{
//d=1,f=0
if (nxt->state_ != )
{
fail_[nxt->state_] = start_node_;
q.push(nxt);
}
}
//calculate all fail redirection
while (!q.empty())
{
auto known = q.front();
q.pop();
for (int i = ; i < ALPHABET_NUMBER; ++i)
{
auto nxt = known->transition_table_[i];
if (nxt && nxt->state_ != )
{
auto p = fail_[known->state_];
while (!p->transition_table_[i])
{
p = fail_[p->state_];
}
fail_[nxt->state_] = p->transition_table_[i];
q.push(nxt);
}
}
}
}
//search matching
void match(const string &_Str, set<string> &_S)
{
auto p = start_node_;
for (int i = ; i < _Str.size(); ++i)
{
int trans = _Str[i] - 'a';
p =
p->transition_table_[trans]
? p->transition_table_[trans]
: (--i, fail_[p->state_]);
if (p->finish_)
{
_S.insert(p->pattern_);
}
}
}
}; int main()
{
ACSM acsm;
vector<string> patterns{ "his","hers","she","he" };
set<string> matched;
acsm.load_pattern(patterns);
acsm.dispose();
string str{ "hishers" };
acsm.match(str, matched);
for (const auto str : matched)cout << str << endl;
system("pause");
return ;
}
谢谢阅读
彻底理解AC多模式匹配算法的更多相关文章
- AC多模式匹配算法
建议:学习ac算法最好的途径是看论文pdf_Efficient_String_Matching_An_Aid_to_Biblio 一.一般的搜索算法 keyword: { he, she, his, ...
- [转] 字符串模式匹配算法——BM、Horspool、Sunday、KMP、KR、AC算法一网打尽
字符串模式匹配算法——BM.Horspool.Sunday.KMP.KR.AC算法一网打尽 转载自:http://dsqiu.iteye.com/blog/1700312 本文内容框架: §1 Boy ...
- 字符串模式匹配算法——BM、Horspool、Sunday、KMP、KR、AC算法一网打尽
字符串模式匹配算法——BM.Horspool.Sunday.KMP.KR.AC算法一网打尽 本文内容框架: §1 Boyer-Moore算法 §2 Horspool算法 §3 Sunday算法 §4 ...
- 字符串模式匹配算法——BM、Horspool、Sunday、KMP、KR、AC算法
ref : https://dsqiu.iteye.com/blog/1700312 本文内容框架: §1 Boyer-Moore算法 §2 Horspool算法 §3 Sunday算法 §4 KMP ...
- Java数据结构之字符串模式匹配算法---Brute-Force算法
模式匹配 在字符串匹配问题中,我们期待察看源串 " S串 " 中是否含有目标串 " 串T " (也叫模式串).其中 串S被称为主串,串T被称为子串. 1.如果在 ...
- 串的模式匹配算法 ------ KMP算法
//KMP串的模式匹配算法 #include <stdio.h> #include <stdlib.h> #include <string.h> int* get_ ...
- 数据结构(三)串---KMP模式匹配算法
(一)定义 由于BF模式匹配算法的低效(有太多不必要的回溯和匹配),于是某三个前辈发表了一个模式匹配算法,可以大大避免重复遍历的情况,称之为克努特-莫里斯-普拉特算法,简称KMP算法 (二)KMP算法 ...
- 【Java】 大话数据结构(8) 串的模式匹配算法(朴素、KMP、改进算法)
本文根据<大话数据结构>一书,实现了Java版的串的朴素模式匹配算法.KMP模式匹配算法.KMP模式匹配算法的改进算法. 1.朴素的模式匹配算法 为主串和子串分别定义指针i,j. (1)当 ...
- 常用算法3 - 字符串查找/模式匹配算法(BF & KMP算法)
相信我们都有在linux下查找文本内容的经历,比如当我们使用vim查找文本文件中的某个字或者某段话时,Linux很快做出反应并给出相应结果,特别方便快捷! 那么,我们有木有想过linux是如何在浩如烟 ...
随机推荐
- .Net多线程编程—并发集合
并发集合 1 为什么使用并发集合? 原因主要有以下几点: System.Collections和System.Collections.Generic名称空间中所提供的经典列表.集合和数组都不是线程安全 ...
- Chrome出了个小bug:论如何在Chrome下劫持原生只读对象
Chrome出了个小bug:论如何在Chrome下劫持原生只读对象 概述 众所周知,虽然JavaScript是个很灵活的语言,浏览器里很多原生的方法都可以随意覆盖或者重写,比如alert.但是为了保证 ...
- sublime常用快捷键
自己觉得比较实用的sublime快捷键: Ctrl + / ---------------------注释 Ctrl + 滚动 --------------字体变大/缩小 Ctrl + N----- ...
- SASS教程sass超详细教程
SASS安装及使用(sass教程.详细教程) 采用SASS开发CSS,可以提高开发效率. SASS建立在Ruby的基础之上,所以得先安装Ruby. Ruby的安装: 安装 rubyinstaller- ...
- [C#][算法] 用菜鸟的思维学习算法 -- 马桶排序、冒泡排序和快速排序
用菜鸟的思维学习算法 -- 马桶排序.冒泡排序和快速排序 [博主]反骨仔 [来源]http://www.cnblogs.com/liqingwen/p/4994261.html 目录 马桶排序(令人 ...
- 【手把手】JavaWeb 入门级项目实战 -- 文章发布系统 (第十二节)
好的,那么在上一节中呢,评论功能的后台已经写好了,这一节,先把这部分后台代码和前台对接一下. 1.评论功能实现 我们修改一下保存评论按钮的点击事件,用jQuery的方式获取文本框中的值,然后通过aja ...
- 编写高质量代码:改善Java程序的151个建议(第6章:枚举和注解___建议88~92)
建议88:用枚举实现工厂方法模式更简洁 工厂方法模式(Factory Method Pattern)是" 创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其它子类" ...
- Linux主机上使用交叉编译移植u-boot到树莓派
0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...
- 浅谈Java的throw与throws
转载:http://blog.csdn.net/luoweifu/article/details/10721543 我进行了一些加工,不是本人原创但比原博主要更完善~ 浅谈Java异常 以前虽然知道一 ...
- Oracle:一个用户操作多个表空间中表的问题(转)
原文地址:http://blog.csdn.net/shmiloy001/article/details/6287317 首先,授权给指定用户. 一个用户的默认表空间只能有一个,但是你可以试下用下面的 ...