原文地址:https://www.cnblogs.com/jily/p/6250716.html

一、原理

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_{ 0 };
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_{ 0 }
{
//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 = 1;
for (const auto &pattern : _Patterns)
{
auto *p = start_node_;
for (int i = 0; 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_ == 0)
{
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 = 0; 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_ != 0)
{
fail_[nxt->state_] = start_node_;
q.push(nxt);
}
}
//calculate all fail redirection
while (!q.empty())
{
auto known = q.front();
q.pop();
for (int i = 0; i < ALPHABET_NUMBER; ++i)
{
auto nxt = known->transition_table_[i];
if (nxt && nxt->state_ != 0)
{
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 = 0; 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 0;
}

【初识算法】- AC算法的更多相关文章

  1. AC算法学习笔记

    1.算法流程图 (1)    void Init() 此函数是初始化函数,用来给fail数组和goto数组初始化值. (2)    void GotoFunction(string x) 这个函数的作 ...

  2. AC算法 及python实现

    零 导言 软件安全课上,老师讲了AC算法,写个博客,记一下吧. 那么AC算法是干啥的呢? ——是为了解决多模式匹配问题.换句话说,就是在大字符串S中,看看小字符串s1, s2,...有没有出现. AC ...

  3. [转] 字符串模式匹配算法——BM、Horspool、Sunday、KMP、KR、AC算法一网打尽

    字符串模式匹配算法——BM.Horspool.Sunday.KMP.KR.AC算法一网打尽 转载自:http://dsqiu.iteye.com/blog/1700312 本文内容框架: §1 Boy ...

  4. 字符串模式匹配算法——BM、Horspool、Sunday、KMP、KR、AC算法一网打尽

    字符串模式匹配算法——BM.Horspool.Sunday.KMP.KR.AC算法一网打尽 本文内容框架: §1 Boyer-Moore算法 §2 Horspool算法 §3 Sunday算法 §4 ...

  5. 【转】AC算法详解

    原文转自:http://blog.csdn.net/joylnwang/article/details/6793192 AC算法是Alfred V.Aho(<编译原理>(龙书)的作者),和 ...

  6. 字符串模式匹配算法2 - AC算法

    上篇文章(http://www.cnblogs.com/zzqcn/p/3508442.html)里提到的BF和KMP算法都是单模式串匹配算法,也就是说,模式串只有一个.当需要在字符串中搜索多个关键字 ...

  7. 字符串模式匹配算法——BM、Horspool、Sunday、KMP、KR、AC算法

    ref : https://dsqiu.iteye.com/blog/1700312 本文内容框架: §1 Boyer-Moore算法 §2 Horspool算法 §3 Sunday算法 §4 KMP ...

  8. 强化学习中REIINFORCE算法和AC算法在算法理论和实际代码设计中的区别

    背景就不介绍了,REINFORCE算法和AC算法是强化学习中基于策略这类的基础算法,这两个算法的算法描述(伪代码)参见Sutton的reinforcement introduction(2nd). A ...

  9. GMM算法k-means算法的比较

    1.EM算法 GMM算法是EM算法族的一个具体例子. EM算法解决的问题是:要对数据进行聚类,假定数据服从杂合的几个概率分布,分布的具体参数未知,涉及到的随机变量有两组,其中一组可观测另一组不可观测. ...

  10. 简单易学的机器学习算法——EM算法

    简单易学的机器学习算法——EM算法 一.机器学习中的参数估计问题 在前面的博文中,如“简单易学的机器学习算法——Logistic回归”中,采用了极大似然函数对其模型中的参数进行估计,简单来讲即对于一系 ...

随机推荐

  1. Salt-Formulas的使用

    Saltstack自0.17.x版本开始引进Formulas的概念,旨在通过简化State和集成数据来实现State的友好管理.根据SALT FORMULAS的官方文档,在完成手动添加formula目 ...

  2. git 相关流程和报错解决

    git 相关流程: 预先配置: gitroot $ git config user.name XXXgitroot $ git config user.email XXX@XXX.com git re ...

  3. 如何设置github的ssh key

    目录 Generate SSH key in putty add key to github git setting How to add SSH key to github Generate SSH ...

  4. OS X 恢复模式重置 Mac 用户登录密码

    关闭你的 Mac.按住 Command + R(⌘R) 组合键,并点按开机按钮,直到出现  标志,进入恢复模式(Recovery Mode)(当然,你也可以先按开机键,在听到启动声后,立即按住 ⌘R ...

  5. 华硕主板 Vmware虚拟机 二进制转换与此平台上的长模式不兼容

    出现情况如下: 大概遇到过两次这个问题,第一次是在笔记本VM上装虚拟机,第二次是在台式机VM上装虚拟机. 原因是因为虚拟化(Intel Virtualization Technology)技术,在主板 ...

  6. 【452】pandas筛选出表中满足另一个表所有条件的数据

    参考:pandas筛选出表中满足另一个表所有条件的数据 参考:pandas:匹配两个dataframe 使用 pd.merge 来实现 on 表示查询的 columns,如果都有 id,那么这是很好的 ...

  7. 路径规划: PRM 路径规划算法 (Probabilistic Roadmaps 随机路标图)

    随机路标图-Probabilistic Roadmaps (路径规划算法) 路径规划作为机器人完成各种任务的基础,一直是研究的热点.研究人员提出了许多规划方法如: 1. A* 2. Djstar 3. ...

  8. C语言设计模式

    一 .C语言和设计模式(继承.封装.多态) C++有三个最重要的特点,即继承.封装.多态.我发现其实C语言也是可以面向对象的,也是可以应用设计模式的,关键就在于如何实现面向对象语言的三个重要属性. ( ...

  9. Flask自动刷新前端页面(方便调试)livereload

    是不是每次调整模板文件,就要停止flask服务器,重启flask服务器,再去浏览器刷新页面? 有没有办法自动完成这3步呢? 安装livereload即可, 仅仅把app.run() 改为下面的例子就可 ...

  10. git worktree 目录修复

    三种方式挨个尝试,1不行用2 2不行用3 1.拉取阶段失败 git worktree add -f -B xxx_branch ./xxx_branch origin/xxx_branch 强制拉取 ...