AC自动机&后缀自动机
理解的不够深 故只能以此来加深理解 。我这个人就是蠢没办法 学长讲的题全程蒙蔽。可能我字符串就是菜吧,哦不我这个人就是菜吧。
AC自动机的名字 AC 取自一个大牛 而自动机就比较有讲究了 不是寻常的东西呢。
自动机由5部分组成 1 字符集 2 状态集合 3 初始状态 4 结束状态集合 5 状态转移函数。
字符集 是指自动机字符的集合。 当然以上有点深奥,我们只需要其能识别字符串即可。
显然的是 KMP做单字符串对单字符串的匹配使用 而AC自动机则是多个字符串在一个字符串上的匹配。
构建trie 大家都会 但是如何求fail 指针呢 思考一下我们求的fail指针其实是指向和当前这个串能匹配的最长后缀。前缀是没有必要的因为我是利用答案串进行匹配的,因为我们得到一个最长前缀再继续这样匹配还是一样效果我们既然答案串都匹配到了现在 也就是当前局面已经不可逆了我们只能不断的跳 跳到一个合法地方让其能继续匹配下去 所以跳到一个最长后缀的位置这样如果还失配的话我们还有机会继续跳 如果跳到一个较短的后缀上的话我们把那些长的都扔了这显然是非常不好做法。
那么现在就有了基础思路构造fail指针 然后失配就不断跳 就行了。在构造fail指针的时候我们是先构造了一颗trie树在trie树搞fail指针显然最长后缀<当前匹配的位置 至于那些比当前位置还要深的位置一定不可能出现,那么就是看深度咯 bfs逐层扩展的过程中 沿着父亲的fail指针走即可。
//#include<bits/stdc++.h> #include<iomanip> #include<iostream> #include<cstdio> #include<cstring> #include<string> #include<queue> #include<deque> #include<cmath> #include<ctime> #include<cstdlib> #include<stack> #include<algorithm> #include<vector> #include<cctype> #include<utility> #include<set> #include<bitset> #include<map> #define INF 1000000000 #define ll long long #define min(x,y) ((x)>(y)?(y):(x)) #define max(x,y) ((x)>(y)?(x):(y)) #define RI register ll #define db double #define EPS 1e-8 using namespace std; char buf[1<<15],*fs,*ft; inline char getc() { return (fs==ft&&(ft=(fs=buf))+fread(buf,1,1<<15,stdin),fs==ft)?0:*fs++; } inline int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } const int MAXN=1000010; char a[MAXN]; int n,root,len,cnt,T,h,ans; int t[MAXN][26],e[MAXN]; int q[MAXN],fail[MAXN]; inline void insert(char *c) { int p=root; //cout<<c[1]<<' '<<c[2]<<endl; for(int i=1;i<=len;++i) { int w=c[i]-'a'; if(!t[p][w])t[p][w]=++cnt; p=t[p][w]; } ++e[p]; } inline void get_fail() { T=h=0; for(int i=0;i<=25;++i) { if(t[root][i]) { q[++T]=t[root][i]; fail[t[root][i]]=0; } } while(h++<T) { int tn=q[h]; for(int i=0;i<=25;++i) { if(t[tn][i]) { q[++T]=t[tn][i]; fail[t[tn][i]]=t[fail[tn]][i]; } else t[tn][i]=t[fail[tn]][i]; } } } inline void AC() { int p=root; for(int i=1;i<=len;++i) { int c=a[i]-'a'; int w=t[p][c]; p=t[p][c]; while(w&&e[w]!=-1) { ans+=e[w]; e[w]=-1; w=fail[w]; } } } int main() { //freopen("1.in","r",stdin); n=read(); for(int i=1;i<=n;++i) { scanf("%s",a+1); len=strlen(a+1); insert(a); } get_fail(); scanf("%s",a+1); len=strlen(a+1); AC(); printf("%d",ans); return 0; }
自己的第二份AC自动机代码这次真的是理解了 因为后缀数组后缀自动机把我搞自闭了。
//#include<bits/stdc++.h> #include<iomanip> #include<iostream> #include<cstdio> #include<cstring> #include<string> #include<queue> #include<deque> #include<cmath> #include<ctime> #include<cstdlib> #include<stack> #include<algorithm> #include<vector> #include<cctype> #include<utility> #include<set> #include<bitset> #include<map> #define INF 1000000000 #define ll long long #define min(x,y) ((x)>(y)?(y):(x)) #define max(x,y) ((x)>(y)?(x):(y)) #define RI register ll #define db double #define EPS 1e-8 using namespace std; char buf[1<<15],*fs,*ft; inline char getc() { return (fs==ft&&(ft=(fs=buf))+fread(buf,1,1<<15,stdin),fs==ft)?0:*fs++; } inline int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x*f; } const int MAXN=2000010; char a[MAXN]; int n,root,len,cnt,T,h; int t[MAXN][26],vis[MAXN]; int q[MAXN],fail[MAXN],ans[MAXN]; inline void insert(char *c,int v) { int p=root; //cout<<c[1]<<' '<<c[2]<<endl; for(int i=1;i<=len;++i) { int w=c[i]-'a'; if(!t[p][w])t[p][w]=++cnt; p=t[p][w]; } vis[v]=p; } inline void get_fail() { T=h=0; for(int i=0;i<=25;++i) { if(t[root][i]) { q[++T]=t[root][i]; fail[t[root][i]]=0; } } while(h++<T) { int tn=q[h]; for(int i=0;i<=25;++i) { if(t[tn][i]) { q[++T]=t[tn][i]; fail[t[tn][i]]=t[fail[tn]][i]; } else t[tn][i]=t[fail[tn]][i]; } } } inline void AC() { int p=root; for(int i=1;i<=len;++i) { int c=a[i]-'a'; p=t[p][c];++ans[p]; } for(int i=T;i>=1;--i)ans[fail[q[i]]]+=ans[q[i]]; } int main() { freopen("1.in","r",stdin); n=read(); for(int i=1;i<=n;++i) { scanf("%s",a+1); len=strlen(a+1); insert(a,i); } get_fail(); scanf("%s",a+1); len=strlen(a+1); AC(); for(int i=1;i<=n;++i)printf("%d\n",ans[vis[i]]); return 0; }
发现可以直接AC自动机 然后 树上差分一下即可。
后缀自动机 将所有后缀放到一张图上 满足点的数量级为O(n) 边的数量级为O(n)
关于 点的数量级的证明是从right集处证明的 从right集中我们可以得到一些等价类 而这些等价类可以构成一棵parent的树。
对于一些等价类的节点他们是从一个比自己大的集合之中分裂出来的 故会分裂n-1次每次分裂会的到两个集合 故点最多为2n 数量级为O(n)
考虑这个parent树就已经可以构成一个后缀自动机了,但是连边的方式不太一样。。。所以可以得证 后缀自动机的点的数量级为O(n)
关于边的数量级为O(n)至今还不懂,咕了。
下面考虑AC自动机的构造 这个算法是一个名叫增量算法的东西 一个一个插入字符 这样我们考虑1 只需新建几个节点 2 新建的节点连到什么节点上。
1:第一种是S的第i个前缀·right集合只有i
不知不觉发现咕了???
下面这个 是正常的: