trie树可遍历出所有无重复后缀,通过后缀遍历前缀可得到所有子串;后缀链接把所有后缀相同的状态(以当前节点为endpos的子串)连接起来,便有了类似KMP的next数组的性质。

                                                                                                          ——后缀自动机使用理解

AC自动机和后缀自动机算是算法竞赛中字符串算最难、最抽象的吧。

其中又以后缀自动机为大BOSS。

它用途广泛,性能优越,受到万千算法选手青睐;但是其学习却让人痛苦万分。

我花了三天断断续续学了五六个小时才勉强弄懂它的构造原理,期间看了数十个视频、博客,听大佬长篇大论地证明时,我一度怀疑自己是不是脑子不好使。但是看一眼评论区,大家平均都是好几天才能学会,那我也不算太差吧。(弱校没有学长带的痛)

好吧回到正题。

根据我的学习经验,一切把SAM构造当黑盒用的都是扯淡,看大佬一万字数学证明来学更是扯淡。最好还是看形象的,图形化的讲解为佳。

如:

后缀自动机 suffix-automaton_哔哩哔哩_bilibili后缀自动机多图详解(代码实现) - maomao9173 - 博客园

后缀自动机简单解释:

后缀自动机是复杂的树形结构,是字符串的魔幻压缩方式,其主要包含两种边:trie边表示字符间的连接,总体上你能从根节点到叶子节点的简单路径中找到该字符串的任意后缀。第二种边是后缀连接,用来在树上转移,这也是自动机的精髓(毒瘤)所在。

先感性理解:你要在trie上表示所有后缀,那么重复的后缀肯定就不能出现。那么找重复的后缀就是后缀连接的作用,连接后缀的字符就是trie边的作用。

那么搞清楚后缀连接的原理就成了最痛苦的环节。

我们从整个字符串的构建开始思考。

我们从左到右依次构建。

那么每次向右遇到的字符就是要插入的字符,也是新的后缀。假如前面有个字符,那么添加第个字符就会改变所有个后缀。此时我们要找出所有出现的后缀中以前出现过的,那是我们不需要的冗余数据。

所以此时就要靠后缀连接了。

后缀连接就是把((根节点到该点)形成的(后缀相同的)串的)点连接起来,这样每次更新后缀只需要找最后一次插入的点的后缀连接一路判断上去即可。因为新后缀除去最后一个插入的字符还是上次的后缀,所以我们每次跑到后缀连接上就判断这里有没有连接新插入字符,有的话就说明有重复了。一直保持这个过程就能完成更新。

其中有两个特殊情况就要用数组特判。

一个是:

if(tre[q].len==tre[p].len+1)tre[np].fa=q;

表示以这个点结尾的所有字符串中最长的刚好和没判断的部分后缀接上了,像这样:

另一个即拆点(不能存在两个相同,又不能破坏性质)。

            int nq=++cnt;tre[nq]=tre[q];tre[nq].len=tre[p].len+1;//短的接上tre[q].fa=tre[np].fa=nq;//新后缀和长部分都连上短部分for(;p&&tre[p].ch[c]==q;p=tre[p].fa)tre[p].ch[c]=nq;//继承原来的后缀链接
void ex_sam(int c){int p=las,np=las=++cnt;num[cnt]=1;tre[np].len=tre[p].len+1;for(;p&&!tre[p].ch[c];p=tre[p].fa)tre[p].ch[c]=np;if(p==0)tre[np].fa=1;//到根节点 else{int q=tre[p].ch[c];if(tre[q].len==tre[p].len+1)tre[np].fa=q;//刚好接上 else{int nq=++cnt;tre[nq]=tre[q];tre[nq].len=tre[p].len+1;tre[q].fa=tre[np].fa=nq;for(;p&&tre[p].ch[c]==q;p=tre[p].fa)tre[p].ch[c]=nq;}//分点,复制一份接上 }
}

AC自动机

ac自动机最详细的讲解,让你一次学会ac自动机。_creatorx的博客-CSDN博客_自动机

AC自动机 算法详解(图解)及模板_bestsort的博客-CSDN博客_ac自动机、

                //如果有这个子节点为字母i+'a',则
//让这个节点的失败指针指向(((他父亲节点)的失败指针所指向的那个节点)的下一个节点)//有点绕,为了方便理解特意加了括号

AC自动机也没什么好搞的,主要后缀自动机能力比它更强,所以AC自动机就显得没什么用了。

kmp


//char a[],b[],int f[],next1[],lb=strlen(b+1),la=strlen(a+1);
// b串自我匹配:关键是找出自己的可能循环点:如abcabd:那么两个ab就有可能作为回溯的点,可能从这里作为字符串的开头向后匹配可以得解;不然就不用回溯,因为没有相同的开头,一直向后就好了;
void KMP(){next1[1]=0;for(int i=2,j=0;i<=lb;i++) {while(j!=0&&b[i]!=b[j+1])j=next1[j];if(b[i]==b[j+1])j++;next1[i]=j;}
}
//b串匹配a串:如果到不匹配的地方,按next1中求出的可能循环点回溯,没回溯点说明前面不存在可能的相同开头字串,b从头开始匹配,a接着往后。
void get_f(){for(int i=1,j=0;i<=la;i++){while(j>0&&(a[i]!=b[j+1])) j=next1[j];//退回操作:找j之前中可能的一个节点; if(a[i]==b[j+1])j++;f[i]=j;if(j==lb){j=next1[j];//  }
}
}

毁灭吧,累了。

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
#define ll long long
int tre[maxn][26],t_cnt[maxn],t_fail[maxn];
string s[maxn];
int tot=0;
void t_insert(string a){int now=0,len=a.size();for(int i=1;i<len;i++){int p=a[i]-'a';if(!tre[now][p])tre[now][p]=++tot;now=tre[now][p];}t_cnt[now]++;
}
void get_fail(){queue<int>q;for(int i=0;i<26;i++){if(tre[0][i]){t_fail[tre[0][i]]=0;q.push(tre[0][i]);}}while(!q.empty()){int now=q.front();q.pop();for(int i=0;i<26;i++){if(tre[now][i]){t_fail[tre[now][i]]=tre[t_fail[now]][i];q.push(tre[now][i]);}elsetre[now][i]=tre[t_fail[now]][i];}}
}
int ask(string a){int now=0,ans=0,len=a.size();for(int i=0;i<len;i++){now=tre[now][a[i]-'a'];for(int j=now;j&&t_cnt[j]!=-1;j=t_fail[j]){ans+=t_cnt[j];t_cnt[j]=-1;}}return ans;
}

后缀自动机例题:

查询是否出现:

P1368 【模板】最小表示法 (后缀自动机)_Jack_00_的博客-CSDN博客

不同字串个数(总长度):

【模板】后缀自动机 (SAM) - 洛谷

dp地去搞。因为后缀自动机上每个点都代表一种状态,越往下字串越长,越往上字串越短,而且不重复。我们在跑出来地tre上跑个dp,即可。

总长度:,可以感性地理解为每往上,那么每个不同地串就加1。

/*keep on going and never give up*/
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ll long long
#define db(x) cerr<<(#x)<<" "<<(x)<<" "<<endl;
#define endl "\n"
#define fast std::ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int tot=1,las=1;
struct NODE{int ch[26];int len,fa;NODE(){memset(ch,0,sizeof(ch));len=fa=0;}
}tre[2000010];
struct hha{int t,nxt;
}e[2000010];
int head[2000010],cnt=0;
void ad(int a,int b){ e[++cnt].t=b;e[cnt].nxt=head[a];head[a]=cnt; }
int zhi[2000010];
void add(int c){int p=las,np=las=++tot;zhi[tot]=1;tre[np].len=tre[p].len+1;for(;p&&!tre[p].ch[c];p=tre[p].fa)tre[p].ch[c]=np;if(!p)tre[np].fa=1;else{int q=tre[p].ch[c];if(tre[q].len==tre[p].len+1)tre[np].fa=q;else{int nq=++tot;tre[nq]=tre[q];tre[nq].len=tre[p].len+1;tre[q].fa=tre[np].fa=nq;for(;p&&tre[p].ch[c]==q;p=tre[p].fa)tre[p].ch[c]=nq;}}
}
char s[2000010];
int cd,ans=0;
void dfs(int node){for(int i=head[node];i;i=e[i].nxt){dfs(e[i].t);zhi[node]+=zhi[e[i].t];}if(zhi[node]!=1)ans=max(ans,zhi[node]*tre[node].len);
}
signed main()
{scanf("%s",s);cd=strlen(s);for(int i=0;i<cd;i++)add(s[i]-'a');for(int i=2;i<=tot;i++)ad(tre[i].fa,i);dfs(1);printf("%lld\n",ans);return 0;
}

字典序第k大子串:

Lexicographical Substring Search - SPOJ SUBLEX - Virtual Judge

搞个玄学拓扑排序(即按长度排序后,由长度来算size,最长的为1,类似上面的dp,可以看作上面串的可能拓展串),再统计一下size。然后递归地在tre上跑:如果k>当前size,说明不在这棵子数上,k-size,去下一棵树;不然就进去当前子树,再跑这个过程即可。

/*keep on going and never give up*/
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ll long long
#define db(x) cerr<<(#x)<<" "<<(x)<<" "<<endl;
#define endl "\n"
#define fast std::ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
const int maxn=2e6+10;
struct node{int ch[26];int len,fa;node(){memset(ch,0,sizeof(ch));len=fa=0;}
}tre[maxn];int cnt=1,las=1;
void ex_sam(int c){int p=las,np=las=++cnt;tre[np].len=tre[p].len+1;for(;p&&!tre[p].ch[c];p=tre[p].fa)tre[p].ch[c]=np;if(p==0)tre[np].fa=1; else{int q=tre[p].ch[c];if(tre[q].len==tre[p].len+1)tre[np].fa=q; else{int nq=++cnt;tre[nq]=tre[q];tre[nq].len=tre[p].len+1;tre[q].fa=tre[np].fa=nq;for(;p&&tre[p].ch[c]==q;p=tre[p].fa)tre[p].ch[c]=nq;}}
}
int a[maxn],id[maxn],sz[maxn];
void tops(){for(int i=1;i<=cnt;i++) a[tre[i].len]++;for(int i=1;i<=cnt;i++) a[i]+=a[i-1];for(int i=1;i<=cnt;i++) id[a[tre[i].len]--]=i;for(int i=cnt;i>=1;i--){sz[id[i]]=1;for(int j=0;j<26;j++){int v=tre[id[i]].ch[j];if(!v)continue;sz[id[i]]+=sz[v];}}
}
void query(int k){int x=1;while (k){for (int i=0;i<26;i++){if (tre[x].ch[i]){if (sz[tre[x].ch[i]]>=k){putchar('a'+i);x=tre[x].ch[i];k--;break;}else k-=sz[tre[x].ch[i]];}}}puts("");
}
string s;
signed main(){cin>>s;for(auto c:s) ex_sam(c-'a');tops();int q;cin>>q;while(q--){int k;cin>>k;query(k);}
}

最长公共字串:

​​​​​​​z​​​​​​​Longest Common Substring - SPOJ LCS - Virtual Judge

建s,匹配t,如果存在转移,则len+1,否则转移回父节点。有点像KMP的next转移的感觉。

因为fa的后缀都相同,相当于找更短可能后缀的拓展了。

/*keep on going and never give up*/
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ll long long
#define inf 1e14
#define db(x) cerr<<(#x)<<" "<<(x)<<" "<<endl;
#define endl "\n"
#define fast std::ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
const int maxn=2e6+10;
int cnt=1,tot,las=1;
int num[maxn];
struct node{int ch[26];int len,fa;node(){memset(ch,0,sizeof(ch));len=fa=0;}
}tre[maxn];
void ex_sam(int c){int p=las,np=las=++cnt;num[cnt]=1;tre[np].len=tre[p].len+1;for(;p&&!tre[p].ch[c];p=tre[p].fa)tre[p].ch[c]=np;if(p==0)tre[np].fa=1;//到根节点 else{int q=tre[p].ch[c];if(tre[q].len==tre[p].len+1)tre[np].fa=q;//刚好接上 else{int nq=++cnt;tre[nq]=tre[q];tre[nq].len=tre[p].len+1;tre[q].fa=tre[np].fa=nq;for(;p&&tre[p].ch[c]==q;p=tre[p].fa)tre[p].ch[c]=nq;}//分点,复制一份接上 }
}
string s,t;
int ans;
void cal(int n){int now=1,len=0;for(int i=0;i<n;i++){int c=t[i]-'a';if(tre[now].ch[c])len++,now=tre[now].ch[c];else{while(now&&!tre[now].ch[c]) now=tre[now].fa;if(now)len=tre[now].len+1,now=tre[now].ch[c];else len=0,now=1;}ans=max(ans,len);}
}
signed main(){cin>>s>>t;for(auto c:s)ex_sam(c-'a');cal(t.size());cout<<ans;
}

后缀自动机 AC自动机相关推荐

  1. 后缀自动机/回文自动机/AC自动机/序列自动机----各种自动机(自冻鸡) 题目泛做...

    题目1 BZOJ 3676 APIO2014 回文串 算法讨论: cnt表示回文自动机上每个结点回文串出现的次数.这是回文自动机的定义考查题. 1 #include <cstdlib> 2 ...

  2. AC自动机算法及模板

    AC自动机算法及模板 2016-05-08 18:58 226人阅读 评论(0) 收藏 举报  分类: AC自动机(1)  版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] 关于 ...

  3. 提高篇 第二部分 字符串算法 第4章 AC自动机

    https://blog.csdn.net/wangyh1008/article/details/81428056 [模板]AC自动机(加强版) 洛谷3796 AC自动机_A_loud_name-CS ...

  4. 字符串处理 —— AC 自动机

    [概述] KMP 算法用于解决长文本的单模板匹配问题,字典树用于解决单个单词(短文本)多模板匹配问题,而 AC 自动机用于解决的是长文本的多模板匹配问题,其是以 trie 树的结构为基础,结合 KMP ...

  5. 数据结构与算法之美笔记——基础篇(下):图、字符串匹配算法(BF 算法和 RK 算法、BM 算法和 KMP 算法 、Trie 树和 AC 自动机)

    图 如何存储微博.微信等社交网络中的好友关系?图.实际上,涉及图的算法有很多,也非常复杂,比如图的搜索.最短路径.最小生成树.二分图等等.我们今天聚焦在图存储这一方面,后面会分好几节来依次讲解图相关的 ...

  6. 【算法无用系列】AC自动机敏感词过滤

    简介: 本文是博主自身对AC自动机的原理的一些理解和看法,主要以举例的方式讲解,同时又配以相应的图片.代码实现部分也予以明确的注释,希望给大家不一样的感受.AC自动机主要用于多模式字符串的匹配,本质上 ...

  7. 字符串-AC自动机(详细图解)

    文章目录 AC自动机 原理 模板 例题 HDU-2222Keywords Search HDU-2896病毒侵袭 HDU-3065病毒侵袭持续中 POJ-2778DNA Sequence HDU-22 ...

  8. 【数据结构与算法】字符串匹配 AC自动机

    单模式串匹配 BF 算法和 RK 算法 BM 算法和 KMP 算法 多模式串匹配算法 Trie 树和 AC 自动机 AC 自动机 AC 自动机实际上就是在 Trie 树之上,加了类似 KMP 的 ne ...

  9. 字符串匹配算法(AC自动机 Aho-Corasick)

    文章目录 1. 多模式串匹配 2. 经典多模式串匹配--AC自动机 2.1 AC自动机构建 2.2 在AC自动机上匹配主串 2.3 复杂度分析 3. python包 1. 多模式串匹配 前面学的BF. ...

最新文章

  1. 关于数据挖掘的几篇文章(1)
  2. eclipse自定义快捷键
  3. python3 hasattr getattr setattr delattr 对象属性 反射
  4. 转载:程序员从初级到中级10个秘诀
  5. 可搜索的文件? 是的你可以。 选择AsciiDoc的另一个原因
  6. Tcp_wrapper
  7. 网络安全模型_工业互联网态势感知,看得见的网络安全
  8. 安卓9.0系统新特性
  9. numpy.outer
  10. 开源游戏java引擎_基于Java的开源3D游戏引擎jMonkeyEngine
  11. 用脚本运行Modelsim教程
  12. 异速联(E-SoonLink)标准版
  13. PHP(阿里云短信验证码)
  14. 震网病毒这类“精确制导的网络导弹“与传统的网络攻击相比较,有哪些新的特点?
  15. 二级计算机vf题型,计算机二级VF题型有哪些?
  16. C#源码刷新网页 最小化托盘http get和post请求配置保存版权时间限制定时调用 单实例运行,如果已经运行则激活窗口到最前显示
  17. 速看四川省企业技术中心拟认定名单已发布,共181家
  18. 一个超级棒的 Chrome 翻译插件
  19. [2019 icpc徐州] H.Yuuki and a problem 带修改的主席树(主席树+树状数组)
  20. 无线网络传输问题:隐藏节点和暴露节点

热门文章

  1. HttpListener 跨域访问
  2. 日历组件带农历及事件标记,绑定点击事件
  3. 能源区块链的优势与缺点
  4. DHCP和BOOTP
  5. 淘宝玩具店:http://shop59071596.taobao.com
  6. 赤峰php,赤峰php程序员培训,赤峰php程序员培训中心,赤峰php程序员培训哪家比较好...
  7. linux 互斥量pthread_mutex
  8. 求大于200的最小质数,java
  9. linux4.12 bonding简介
  10. 第二十一章、系统配置工具(网络与打印机)与硬件侦测