Aho

摘要:
例子举完了,下面就该我们如何用计算机实现了:我们可以把上面例子中的字典构造成一颗树即“字典树”,其实字典树就是一颗特殊的树。从根到每一片叶子都是一个单词,字典树中有多少片叶子就有多少个单词。字典树的详细构造过程,这里就不细说了,详情请看我的另一篇文章Trieimplementation字典树构造完成,接下来就是为字典树构造Fail指针,用于如果当前字符与当期模式串匹配不成功时,与下一个模式串匹配的位置;如何找到下一个模式串?

Aho - Corasick string matching algorithm 俗称:多模式匹配算法,它是对 Knuth - Morris - pratt algorithm (单模式匹配算法) 形成多模式匹配算法的一种改进,如果我们用单模式匹配算法实现多模式匹配算法,假如模式串有 M 个 , 则需要重复调用 M 次单模式匹配算法 ;

举个很简单的例子,假如我现在有一本特殊的字典,字典中的词汇就是所有的模式串,然后给你一篇文章(全英文),让你查一下这篇文章中有多少个词汇在字典中可以查得到;(为了使问题简单化,我们可以忽略单词之间的空格及标点符号)

想一想,如何在最短的时间内结束查找过程?

首先,我们需要对这本字典建立索引,这个索引应该怎么建立呢?当然是按照常规英文字典进行建立(这里就不详细介绍了,默认大家高中都用过英文字典吧,但是为了接近机器思维,要求我们每次只能匹配一个字符),但是当查到不匹配的字符该怎么办啊?假如这个字符就是这个模式串的最后一个字符了,如果匹配对了,该多好啊,难道要回溯到与模式串第二个字符进行匹配的字符吗?多可惜啊!前面难道白白匹配了这么多吗?这是要累死我的节奏啊!我可不想这样,肯定有方法可以减少这些重复匹配,我还是看看其他的模式串的前缀有没有和当前模式串的前缀的一部分相同的(假如当前模式串为:ababc), 果然不出所料,找的了一个模式串(abd), 这次总算没白找,那我就从这个模式串往后找吧! 找出模式串与模式串之间的联系,就是这本字典的特殊之处。

例子举完了,下面就该我们如何用计算机实现了:

我们可以把上面例子中的字典构造成一颗树即“字典树”,其实字典树就是一颗特殊的树。从根到每一片叶子都是一个单词,字典树中有多少片叶子就有多少个单词。

字典树的详细构造过程,这里就不细说了,详情请看我的另一篇文章 Trie implementation

字典树构造完成,接下来就是为字典树构造Fail 指针,用于如果当前字符与当期模式串匹配不成功时,与下一个模式串匹配的位置;

如何找到下一个模式串?

如果当前模式串中的字符与当前字符不匹配,然后找到当前模式串中的字符所在结点的父亲结点(若父亲结点为根结点,则让当前模式串的字符所在结点的Fail 指针指向根结点)的Fail指针所指的结点,看其儿子中有没有和当前字符相等的,如果有,则让当前模式串的字符所在结点的Fail 指针指向其儿子结点,否则再找其Fail 指针所指的位置,直到找到根结点的Fail指针(NULL)为止!(根结点的Fail指针为NULL)

下面给出相应的伪代码:

root -> fail NULL

q root (q为队列,root入队)

while qempty do

rq (头元素出队)

pr

for i 0 to 26 do //每个结点的儿子结点都需要找到

if r -> next[i] NULL do

if r == root do //如果其父亲结点为根结点,其Fail 指针指向根结点

r -> next[i] -> fail = root

q r ->next[i]

continue

else  do

while r -> fail -> next[i] == NULL and r -> fail root  do //找到父亲结点的 Fail 指针指向结点的儿子结点中的字符是否与当前结点中的字符相同

r = r -> fail

end while

if(p -> next[i] == r -> fail -> next[i]) // 如果相同当前结点指向其儿子结点

p -> next[i] -> fail = r -> fail -> next[i]

else

p -> next[i] -> fail = root //否则指向根结点

end if

end if

q p -> next[i] //将当前结点入队

end if

end for

end while

这样就完成了每个结点中的 Fail 指针的指向 ;

下面需要进行的是,自动匹配任务了:

先介绍一下思想,其实很简单,一句话:如果匹配,继续往下找,如果不匹配,从当前结点的 Fail 指针所指的结点的儿子相同时开始往下找;

给出相应的伪代码:

len Length[ s ]

p root

count 0

for i 0 to len do

rNULL

if root -> next[s[i] - 'a'] NULL do    // 如果能够匹配,r 指向当前字符所在的结点,如果当前字符是一个单词的结尾,r 再指向当前结点的 Fail 指针所指      root = root -> next[s[i] - 'a' ] // 的结点,若 r所指的结点恰好又是另外一个单词的结尾,r 继续进行下去,直到 r 找到根结点为止或找到的结点不是

r root              // 单词的结尾 ,例如(ababd 与 abd , 若当前字符为 d , 则有两个单词可以查到)

while r -> is_over == true && r != root    // 因为可以与当前字符匹配,所以应该接着字符串ababd往下查,而不是字符串 abd ,

count count + 1

r -> is_over = false

r = r -> fail

`    end while

continue

else 

while root -> next[s[i] - 'a'] == NULL and root ≠ p do         // 如果不能匹配,转到 Fail 指针指导的结点进行匹配

root = root -> fail

end while

if root -> next[s[i] - 'a']NULL              //如果找到匹配对象,就从匹配对象开始往下找

root = root -> next[s[i] - 'a']

else                          //如果找不到匹配对象,则从根结点开始往下找

root = p

end if

r root

while r -> is_over == true and r p do // 如果找到模式串的结尾,计数加1,并且 is_over 赋值 false 避免重复查找

count++

r -> is_overfalse

r = r -> fail              //当前指针指向其 Fail所指的结点

end while

end for

return count

以上是对于Aho-Corasick Algorithm 的思想描述,及相关伪代码的实现,最最要的还是要会用这样的思想进行问题的解决,在HDUOJ中有这样一道题,大致描述一下题意: 有N个单词,和一段英文,用相关程序解决这段英文中,有这N个单词中的几个;

解题思路:

第一步肯定是用这N个单词建立字典树

第二步为这个字典树完成 Fail 指针

第三步在字典树中进行查找

下面给出相应的代码: (坑了我一两天才找到其中的一个bug)

#include<iostream>
#include<string.h>
#include<string>
#include<queue>
using namespace std ;

#define K 26

struct Node     {
        Node *fail ;
        Node *next[K] ;
        bool is_over ;
} ;

Node *new_Node()        {
        Node *root = new Node ;
        root -> fail = NULL ;
        for(int i = 0 ; i < K ; i++)
                root ->next[i] = NULL ;
        root -> is_over = false ;
        return root ;
}

void Construct_Trie( Node *root , char *s )     {
        int len = strlen(s) ;
        for( int i = 0 ; i < len ; i++ )        {
                if( root -> next[s[i] - 'a'] == NULL )
                        root -> next[s[i] - 'a'] = new_Node() ;
                root = root -> next[s[i] -'a'] ;
        }
        root -> is_over = true ;
}

void Construct_Fail( Node *root )       {
        root -> fail = NULL ;
        queue< Node* > q ;
        q.push(root) ;
        while(!q.empty())       {
                Node *p = q.front() ;
                Node *r = p ;
          q.pop() ;
                for(int i = 0 ; i < K ; i++)    {
                   if(p -> next[i] != NULL  )   {
                        if(p == root)   {
                                p ->next[i] -> fail = root ;
                                q.push(p ->next[i]) ;
                                continue ;
                        }
                        while(p->fail != root && p ->fail ->next[i] == NULL )    // 找到根结点为止
                                p = p -> fail ;
                        if( p -> fail -> next[i] != NULL  )
                                r ->next[i] -> fail = p -> fail -> next[i] ;
                        else
                                r -> next[i] -> fail = root ;
                        q.push(r -> next[i]) ;
                    }
                }
        }
}

int Aho_Croasick( Node *root , char *s )        {
        int len = strlen(s) ;
        Node *p = root ;
        int count = 0 ;
        for(int i = 0 ; i < len ; i++)  {
                Node *r = NULL ;
                if(p ->next[s[i] - 'a'] != NULL)        {
                        p = p -> next[s[i] - 'a'] ;
                        r = p ;
                        while(r -> is_over && r != root )       {  // 找到根结点为止
                                count++ ;
                                r ->is_over = false ;
                                r = r ->fail ;
                        }
                        continue ;
                }
          else    {
                        while( p -> next[s[i] - 'a'] == NULL && p != root )   // 找到根结点为止
                                p = p -> fail ;
                        if( p -> next[s[i] - 'a'] != NULL )
                                p = p -> next[s[i] - 'a'] ;
                        else
                                p = root ;
                        r = p ;
                        while( r-> is_over && r != root )       {    // 找到根结点为止
                                count++ ;
                                r -> is_over = false ;
                                r = r -> fail ;
                        }
                }
        }
        return count ;
}

int main()      {
        int n ;
        cin >> n ;
        while(n--)      {
                int m ;
                cin >> m ;
                Node *root = new_Node() ;
                while(m--)      {
                        char s[10005];
                        cin >> s ;
                        Construct_Trie(root, s);
                }
                char ss[100005] ;
                cin >> ss ;
                Construct_Fail(root) ;
                cout << Aho_Croasick(root,ss) << endl ;
        }
        return 0 ;
}
// 所有查找过程最多不能超过根结点

整整写了一天啊!有木有……看来我也只适合做程序员了……

免责声明:文章转载自《Aho》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇StatefulSetyum 程序包管理简介下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

B树和B+树

1.磁盘原理   当磁盘需要读取某个地址数据的时候,首先会判断数据在哪个盘片上,确定好盘片之后呢,就开始选道,选道的过程就是通过伸展机械臂到数据对应的磁道(也就是圆环),再通过磁盘的旋转,找到对应的扇区,最后用磁头读取这几个扇区的数据到内存中去,因此可以看到读取磁盘数据是非常的耗时耗力的,尤其这里是机械操作。 因此如果用BST的方式在磁盘上存取数据,就会存...

windows 系统 MySQL_5.6.21安装教程

  1、双击安装文件 mysql_installer_community_V5.6.21.1_setup.1418020972.msi,等待安装界面出现,见下图:   2、勾选:I accept thelicense terms,点击Next,见下图:   3、选择Custom,点击Next,见下图   4、 4.1打开支线,根据服务器操作系统类型(32位...

X awk 两个文件中记录的对比

Linux中awk抽取包含某字段的整行 awk '{if($ 0~“listAuths”)print}' xxx.log 发现 需要的是将一个文件中的内容与另一个文件中的进行匹配 并输出属于A,同时也属于B文件,则将B文件下该行内容打印出来 方法1: awk 'NR==FNR{a[$1];next}{if($2 in a)print }‘ file...

生成CPIO格式的initrd

因为initrd.img只是系统启动的一个虚拟磁盘而已,系统启动完成后就没有用处了,因此,我决定用busybox来完成一些必要的启动工作(用的是busybox-1.5.1,配置文件如下) cd /tmp mkdir initrd cd initrd mkdir dev proc sys lib mnt mkdir -p lib/modules/kernel...

ubuntu root 登录

1、首先用安装ubuntu时的用户登入UBUNTU后,在终端之中输入:sudo passwd root,接着输入密码和root密码,重复密码。这样就有了可用的root用户。2、重新启动UBUNTU,在登陆界面之中选其它,输入ROOT的用户名,及密码。  ubuntu root是默认禁用了,不允许用root登陆,所以先要设置root密码。   执行:sudo...

如何在centos下卸载干净nginx

比如为了测试,我们使用yum新装了nginx,那么如何卸载的时候更干净一些呢? 我们先使用history来查看刚刚执行过的命令 yum history 然后会出现如下所示 [root@localhost ~]# yum history 已加载插件:fastestmirror ID | 登录用户 | 日期和时间...