AC自动机总结

来源:互联网 发布:linux 查看连接的ip 编辑:程序博客网 时间:2024/06/07 22:03

如果你还不知道KMP是什么,或者不知道Trie是什么,先点下面的链接,学好这两样再来看这篇文章。

KMP入门教程传送门。

Trie入门教程传送门。

好,那么我来讲讲AC自动机吧。

以洛谷P3808模板题为例,

如果我们有n个模式串,和一个文本串,我们需要统计有多少个模式串在文本串中出现了,怎么办?

显然,如果我们直接枚举每个模式串,然后将其与文本串进行匹配,判断是否匹配成功,那么效率就是O(n*length(T)),很不理想。

那么我们是否有什么可以利用的信息呢?

如2个模式串:hers,he,显然如果我们匹配到了一个hers,就一定会有一个he。那么我们有一个新的优化思路,先将n个模式串两两匹配,确定包含关系,然后再在文本串里匹配,显然这个思路的效率最坏情况下可以达到O(n^2+n*length(T)),更差。

那为什么要提它呢?

因为这个包含关系我们可以优化!我们根本无需两两匹配,我们直接把n个模式串建成一个Trie,然后用一个bfs直接计算包含关系就好了!这个bfs的效率是O(sigma_length(S))~=O(length(T))的。

那么剩下的工作就好办了,我们直接枚举文本串的每一位,然后用Trie进行O(1)递推的查找与当前文本串结尾相同的模式串就行了。(这句话可能并太好懂,也可以这样解释:由前几位的字符和当前位的字符,我们可以在Trie中确定唯一一个节点,具体的可以参见代码)

可是有的人也许就会问,这个包含关系有什么用,也许n个模式串根本就两两毫不包含呢?

确实,包含这种说法并不准确。其实我们就是在计算失配函数,

只是这个失配函数非常的特殊,因为它是建在一个Trie上的。

我们重新定义KMP中的失配函数。

设f[u]=v表示从根到u号节点的这个字符串的某个后缀与从根到v号节点的这个字符串是等效的(即完全相等的)!这个后缀必须是尽量地长。

我们匹配到在一个模式串的时候就不断的沿着失配边跳,所有的端点v是标记点的失配边都对应了一个模式串。

但是这个失配关系往往会非常复杂,即我们跳了很多次,但是没有一个端点是标记点,所以我们引入last指针的优化。

即定义last[u]=v表示从u开始不断沿着失配边跳到的第一个是标记点的端点v,那么我们再匹配是就无需沿着f跳,而是沿着last跳,每跳到一个last,它就一定对应一个模式串,所以效率是非常高的。

我们首先考虑f的递推。我们设当前节点为u,其一个孩子ch[u][c]=v,k表示u沿着f边跳一次对应的点,即k=f[u],

那么如果u不是根节点,f[v]=ch[k][c],即沿着k再向下走一个c字符,这时两个字符串还是相等的对不对。

如果u是根节点那就没什么好说的了,f[v]=0。

在此条件下,last就非常好递推了,,那么如果f[v]是标记点,那么last[v]=f[v],

否则last[v]=last[f[v]]。

这个f[v]并不能保证ch[k][c]就一定存在,所以还需要一个while循环一直跳直到找到一个ch[k][c]!=0的端点。可以出现了一个while,既使代码不够优美,又使效率无法保证,所以我们直接把所有ch[k][c]=0的端点的ch[k][c]直接连向ch[f[k]][c],就好像并差集的一个路径压缩,由于Trie是读完所有模式串后建的,所以这个加边并不会影响Trie。

考虑到这是一个Trie树上的递推,所以我们用BFS搞一搞就好了。

效率显而易见O(sigma_length(S)+length(T))

和Trie一样,AC自动机的裸题并不多见,常常是和DP结合坑人。

而AC自动机的裸题,先把模板打好后,基本上改的就是对标记点的处理。

P3808:

#include<cstdio>#include<cstring>#include<algorithm>#include<queue>#define REP(i,a,b) for (int i=(a);i<=(b);i++)#define reset(a) memset((a),0,sizeof((a)))using namespace std;const int N=1e6+10;char P[N];int n;struct Aho_Corasick_Automaton{int ch[N][26],val[N],f[N],last[N],NodeCnt;void clear(){reset(ch);reset(val);reset(f);reset(last);NodeCnt=0;}void insert(char *S){int u=0,n=strlen(S);REP(i,0,n-1){int c=S[i]-'a';if (!ch[u][c])ch[u][c]=++NodeCnt;u=ch[u][c];}val[u]++;}void getFail(){queue<int> Q;Q.push(0);while (!Q.empty()){int u=Q.front(),k=f[u];Q.pop();REP(c,0,25){int v=ch[u][c];if (!v){ch[u][c]=ch[k][c];continue;}Q.push(v);f[v]=u?ch[k][c]:0;last[v]=val[f[v]]?f[v]:last[f[v]];}}}int count(char *T){int u=0,n=strlen(T),res=0;REP(i,0,n-1){int c=T[i]-'a';u=ch[u][c];if (val[u])res+=val[u],val[u]=0;int v=u;while (last[v]){v=last[v];if (val[v])res+=val[v],val[v]=0;}}return res;}}AC;int main(){scanf("%d",&n);AC.clear();REP(i,1,n){scanf("%s",P);AC.insert(P);}AC.getFail();scanf("%s",P);printf("%d\n",AC.count(P));return 0;}



洛谷P3796:

就是对AC自动标记点处理的简单修改,即把匹配到的标记点都计数一下,然后求计数最大的那些标记点就行了。

参考代码:

#include<cstdio>#include<algorithm>#include<cstring>#include<queue>#define reset(a) memset((a),0,sizeof((a)))#define REP(i,a,b) for (int i=(a);i<=(b);i++)using namespace std;const int N=1e6+10;const int M=151*71;char s[151][71];char T[N];int m;struct Aho_Corasick_Automaton{int NodeCnt,f[M],ch[M][26],cnt[M],val[M],last[M];void clear(){reset(f);reset(ch);reset(cnt);reset(val);reset(last);NodeCnt=0;}void insert(char *s,int index){int u=0,n=strlen(s);REP(i,0,n-1){int c=s[i]-'a';if (!ch[u][c])ch[u][c]=++NodeCnt;u=ch[u][c];}val[u]=index;}void getFail(){queue<int> Q;Q.push(0);while (!Q.empty()){int u=Q.front(),k=f[u];Q.pop();REP(c,0,25){int v=ch[u][c];if (!v){ch[u][c]=ch[k][c];continue;}f[v]=u?ch[k][c]:0;last[v]=val[f[v]]?f[v]:last[f[v]];Q.push(v);}}}void query(char *T){int u=0,n=strlen(T),res=0;REP(i,0,n-1){int c=T[i]-'a';u=ch[u][c];if (val[u])cnt[val[u]]++;int v=last[u];while (v){if (val[v])cnt[val[v]]++;v=last[v];}}REP(i,1,m)res=max(res,cnt[i]);printf("%d\n",res);REP(i,1,m)if (cnt[i]==res)printf("%s\n",s[i]);return;}}AC;int main(){while (scanf("%d",&m)==1 && m){AC.clear();REP(i,1,m){scanf("%s",s[i]);AC.insert(s[i],i);}AC.getFail();scanf("%s",T);AC.query(T);}return 0;}