AC自动机

2018-07-24  本文已影响0人  An_Account

AC自动机(Aho-Corasick\ automaton),可以解决多模板串匹配的问题。可以理解为可以一次性匹配很多串的KMP。在KMP中,有一个失配函数next,在AC自动机中,也有一个类似的失配函数fail。类似于KMP的转移,每当AC自动机失配时,也会相应地转到当前节点的fail

Trie

为了一次性高效地匹配字符串,我们需要用一个数据结构来维护所有的模板串。这个数据结构就叫Trie。可以把Trie叫做字典树,所以Trie是一棵树。比如下面这个

Trie

这个就是一棵包含ab,abc,bd,dda四个串的Trie。通过观察,我们可以发现

  1. 根节点不包含字符

  2. 从根节点到某个节点简单路径上的字母即是这个节点代表的串

  3. 串的终点有标记

每次匹配,我们从Trie的根节点出发,尝试用文本串去匹配Trie树,由于Trie树将一些字符串压缩到了一起,因此时间复杂度会大大降低。但注意,Trie树是一种用空间换时间的做法

部分代码:

void init() {memset(trie[0],0,sizeof(trie[0])),ncnt=0;}
void insert(char s[],int len,int n) {//插入新串
    int now=0,c;
    for (int i=1;i<=len;i++) {//查找路径(没有则添加)
        c=idx(s[i]);
        if (trie[now][c]==0)
            now=trie[now][c]=++ncnt,memset(trie[now],0,sizeof(trie[now]));
        else now=trie[now][c];
    }
    val[now]=n;//标记终点
}

Trie树的空间复杂度是O(nmt)n是串的总数,m是最长串的长度,t是节点字母的种类

AC自动机实现

我们引入一个新的数组last,代表与当前节点有最长后缀的串在Trie上的编号。很明显,假如当前串匹配成功了,那么它的last也一定可以匹配成功(是它的后缀啊)

last的一个重要性质: p的失败指针指向节点k,则它应该满足这样的性质:从rootk的字符串完全匹配于从p往上相同长度的后缀

注意lastfail的区别:last是指当前节点的某个出现在模板串之中的后缀,而fail则不要求出现在模板串中

如果当前串匹配失败,那么就一直跳fail。我们这样考虑:

假设s[1,i]已经匹配成功了,第i个字符对应Trie上的u号节点,但是s[i+1]匹配失败了,很明显,我们可以从u节点跳到它的fail。因为s[1,i]已经匹配成功了,那么s[1,i]的某个后缀(对应fail)也一定可以匹配成功

int c=idx(s[i]);
while (now&&!trie[now][c]) now=fail[now];

举个例子

我们考虑这样一个自动机

AC自动机

这是一棵倒着的Trie加上一些虚线箭头一个AC自动机的状态图,绿色的点代表有标记。至于点上的数,可以暂时不管。

实线代表Trie上的边,虚线代表fail指向的节点

观察91这个点,它表示的串是SHE,它的fail指针指向76号点,这个点表示的串是HE,可以发现,它是SHE的一个后缀。

我们考虑在这样一棵Trie上匹配串SHERS

1.当前位于根节点,对应字符S,因此走85号点,匹配成功
2.当前位于85号点,对应字符H,因此走90号点,匹配成功
3.当前位于90号点,对应字符E,因此走91号点,匹配成功
4.91号点被标记过,匹配数+1
5.当前位于91号点,对应字符R,这个点没有字符为R的点,匹配失败,跳到91号点的fail指针76号点
6.当前位于76号点,对应字符R,因此走160号点,匹配成功
7.当前位于160号点,对应字符S,因此走86号点,匹配成功
8.至此,串SHERS已匹配完成

代码实现如下

void query(char s[],int len) {
    int now=0;
    for (int i=1;i<=len;i++) {
        int c=idx(s[i]);
        while (now&&!trie[now][c]) now=fail[now];//匹配失败,则一直跳fail指针
        now=trie[now][c];
        if (val[now]) cnt[val[now]]++;//匹配成功,且当前节点是某个模板串的末尾
        int las=last[now];
        while (val[las]) val[las]=0,cnt[val[las]]++,las=last[las];//当前节点匹配成功,它的last也一定匹配成功
    }
}

接下来是AC自动机的关键部分,即预处理fail指针与last指针

由于一个节点的fail的深度一定小于这个点的深度,所以我们考虑用bfs实现

考虑从一个已经求出faillast的节点now转移到他的后继节点son

如果nowfail指针有与son相同的后继节点,那么直接转移到那个后继节点。否则一直跳fail,直到根节点。

for (int i=0;i<128;i++) 
    if (trie[now][i]) {
        int f=fail[now];
        while (f&&!trie[f][i]) f=fail[f];
        int son=trie[now][i];
        fail[son]=trie[f][i];
    }

如果now节点所指向的fail的这个儿子已经被标记过,即是某个模板串的结尾,就直接将其赋到son,否则一直跳last

last[son]=val[fail[son]]?fail[son]:last[fail[son]];

bfs实现如下

void bfs() {
    queue<int> q;
    while (!q.empty()) q.pop();
    fail[0]=last[0]=0;
    for (int i=0;i<26;i++) {
        int now=trie[0][i];
        if (now) fail[now]=last[now]=0,q.push(now);
    }
    while (!q.empty()) {
        int now=q.front();q.pop();
        for (int i=0;i<26;i++) 
            if (trie[now][i]) {
                int f=fail[now];
                while (f&&!trie[f][i]) f=fail[f];
                int son=trie[now][i];
                fail[son]=trie[f][i],last[son]=val[fail[son]]?fail[son]:last[fail[son]];
                q.push(son);
            }
    }
}

最后是AC自动机模板代码

int ncnt=0,trie[100010][26],val[100010];
int idx(char c) {return c-'a';}
void init() {memset(trie[0],0,sizeof(trie[0])),ncnt=0;}
void insert(char s[],int len,int n) {
    int now=0,c;
    for (int i=1;i<=len;i++) {
        c=idx(s[i]);
        if (trie[now][c]==0)
            now=trie[now][c]=++ncnt,memset(trie[now],0,sizeof(trie[now]));
        else now=trie[now][c];
    }
    val[now]=n;//val里存的是串的编号
}
int fail[100010],last[100010];
void bfs() {
    queue<int> q;
    while (!q.empty()) q.pop();
    fail[0]=last[0]=0;
    for (int i=0;i<26;i++) {
        int now=trie[0][i];
        if (now) fail[now]=last[now]=0,q.push(now);
    }
    while (!q.empty()) {
        int now=q.front();q.pop();
        for (int i=0;i<26;i++) 
            if (trie[now][i]) {
                int f=fail[now];
                while (f&&!trie[f][i]) f=fail[f];
                int son=trie[now][i];
                fail[son]=trie[f][i],last[son]=val[fail[son]]?fail[son]:last[fail[son]];
                q.push(son);
            }
    }
}
int cnt[510];
void query(char s[],int len) {
    int now=0;
    for (int i=1;i<=len;i++) {
        int c=idx(s[i]);
        while (now&&!trie[now][c]) now=fail[now];
        now=trie[now][c];
        if (val[now]) cnt[val[now]]++;
        int las=last[now];
        while (val[las]) val[las]=0,cnt[val[las]]++,las=last[las];
    }
}
上一篇下一篇

猜你喜欢

热点阅读