后缀数组&后缀自动机
SA
后缀排序的中心思想是倍增,为了优化常数会使用一些比较特别的技巧。写法上主要分为两个部分,即预处理部分和倍增部分。首先定义几个数组,
void main(){
int n=200,p=0;
for(int i=1;i<=m;i++)pl[rk[i]=w[i]]++;
for(int i=1;i<=n;i++)pl[i]+=pl[i-1];
for(int i=m;i;i--)sa[pl[rk[i]]--]=i;
for(int w=1;;w<<=1,n=p,p=0){
for(int i=m;i>m-w;i--)id[++p]=i;
for(int i=1;i<=m;i++)sa[i]>w&&(id[++p]=sa[i]-w);
memset(pl,0,sizeof(pl));
for(int i=1;i<=m;i++)pl[rk[id[i]]]++;
for(int i=1;i<=n;i++)pl[i]+=pl[i-1];
for(int i=m;i;i--)sa[pl[rk[id[i]]]--]=id[i];
memcpy(ork,rk,sizeof(int)*(m+1));p=0;
for(int i=1;i<=m;i++)rk[sa[i]]=chk(sa[i-1],sa[i],w)?p:++p;
if(p==m)break;
}
}
for(int i=1,k=0;i<=m;i++){
if(k)k--;
while(w[i+k]==w[sa[rk[i]-1]+k])k++;
h[rk[i]]=k;
}
有许多结论。一个是说后缀
另外就是一个非常常见的柿子,给定一个集合
再有就是练习题时间了。
优秀的拆分:首先枚举贡献点,然后把问题拆分成以一个点开头的
关键点并没有画出来。黑色的串是最长的相同前后缀拼起来形成的串,红色的是一个长度为
CF822E 首先有一个贪心的想法,当确定了用
品酒大会 提出了一个常用的做法。两个后缀的
SAM
以下内容为初学笔记,非常幼稚。
有一个串 aabab
,对它的所有后缀建立字典树,会得到:
点太多了,不是非常优秀,而且复杂度太高了。所以想着能不能用一些什么方法把复杂度和点数降下来,这就是后缀自动机想要做的事。先搞一个定义,一个串的
- 若
,那么 是 的后缀(或者相反)。 - 对于两个串
,有 ,要么 。 - 所有
相同的 称为一个等价类,一个等价类中串的长度连续。
根据第一个和第二个结论,所有等价类可以形成一个树形结构,事实上 SAM 就是把这棵树上的节点重新编号连边后形成的东西。比如串 aababa
的这棵树(即
- 记
是等价类 中最短的字符串长度,而 是最长的,那么有结论: 是 的父亲当且仅当 。
性质就差不多了,还有一些正确性和复杂度上的证明然鹅我并不很想研究。于是就是最重要的构造部分了,放个非常经典的图。
主程序非常无脑,一个一个字符加,构成了最下面那排节点。
for(int i=1;i<=m;i++)insert(w[i]-'a');
首先是新建一个节点(毕竟新串的
int p=lastCnt;int np=lastCnt=++cnt;
t[np].len=t[p].len+1;
然后开始遍历加入该字符之前的串的后缀并检查哪些位置的
for(;p&&!t[p].ch[c];p=t[p].fa)t[p].nxt[c]=np;
if(!p)t[np].fa=1;else{}
如果进到了第二个分支就说明
int q=t[p].ch[c];
if(t[q].len==t[p].len+1)t[np].fa=q;else{}
还有其它情况,若
新建一个叫做
int nq=++cnt;t[nq]=t[q];
t[nq].len=t[p].len+1;
t[q].fa=t[np].fa=nq;
最后就是和上面第二部分一样的套路,更新一路上的出边。
for(;p&&t[p].ch[c]==q;p=t[p].fa)t[p].nxt[c]=nq;
总的就是这样的:
inline void insert(int c){
int p=lastCnt;int np=lastCnt=++cnt;f[np]=1;
t[np].len=t[p].len+1;
for(;p&&t[p].nxt[c]==0;p=t[p].fa)t[p].nxt[c]=np;
if(!p)return t[np].fa=1,void();
int q=t[p].nxt[c];
if(t[q].len==t[p].len+1)return t[np].fa=q,void();
int nq=++cnt;t[nq]=t[q];
t[nq].len=t[p].len+1,t[np].fa=t[q].fa=nq;
for(;p&&t[p].nxt[c]==q;p=t[p].fa)t[p].nxt[c]=nq;
}
用自然语言描述这一过程就是说,进来一个字符,更新一串点的出边;如果到头了就返回,否则检查一下是否有合适的父亲。如果有,回去;如果没有,说明有个集合太霸道,从中扯出来一部分,并钦定它是父亲,然后打扮一下这个节点,走完流程,更新出边。好感性的语言啊。
复杂度什么的,由于 ZC 没有脑子,自然是没有那个能力去看的,暂且略过罢。先看一下应用部分。其实吧后缀自动机大多数的应用都来源于它最底层的性质:从根随机游走,任意合法的路径(结束节点没有任何限制)都是原串的子串,并且不重不漏,这个性质听起来就已经非常美妙啦。然后点呢有一些实际含义,比如每个节点的
- 判断子串
在自动机上顺着边跑,如果跑到
- 不同子串的个数
后缀数组上直接用总串数减去
- 求子串不去重字典序第
大
首先预处理出每个节点
然后用
还有很多很多方法和技巧,以后遇到了再说。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架