目录

  • 参考文献
  • 后缀自动机是什么神仙玩意?
    • 例题
    • right集合、等价类以及Parent树
      • 定义
      • 等价类性质
    • Trie?DAG?
    • 自动机?自动鸡?
      • 自动机的基本性质
      • 一定存在后缀自动机吗?
      • 后缀自动机是唯一的吗?
      • 后缀自动机的几个性质
      • 边的数量级
    • 构造方法
      • 代码
      • 部分1
      • 部分2
      • 部分3
      • 部分4
      • 部分5
    • 思路
    • 代码
    • 时间复杂度
  • 广义后缀自动机
    • 思路
    • 练习
  • 子序列自动机

参考文献

咕咕日报上的,就没有一个是差品:https://www.luogu.org/blog/Kesdiael3/hou-zhui-zi-dong-ji-yang-xie,同时,带luogu水印的图也是一律采用这个博客的,因为我太弱了,不会画图QAQ。

时间复杂度的证明https://blog.csdn.net/qq_35649707/article/details/66473069

广义后缀数组就是在这学的:https://blog.csdn.net/litble/article/details/78997914

字符集的相关内容:https://oi.men.ci/suffix-automaton-notes/

套B-树:https://www.cnblogs.com/hyghb/p/8445112.html

stO 后缀自动机怎么能少了陈立杰大佬的论文了呢? Orz

博主是真的臭不要脸,拿着别人的图写着自己的博客

后缀自动机是什么神仙玩意?

其实许多题目,我们都可以用后缀数组做,但是后缀数组有时候远远不能满足我们的需求,这个时候,后缀自动机就出现了。

例题

问题描述 给出一个字符串S(S<=250000),令F(x)表示S的所有长度为x的子串中,出现次数的最大值。求F(1)..F(Lengh(S)); 样例输入 ababa 样例输出 3
2
2
1
1 

right集合、等价类以及Parent树

定义

首先,我们要定义一个东西,叫\(right\),字符串的每个子串都可以有个\(right\)。

\(right(s)\)表示的是\(s\)子串在母串所有出现位置的右端点。

打个比方:在\(ababa\)中,\(right(ab)={2,4}\)

等价类性质

1

加入有两个不相同的子串的\(right\)集合是相等的,那么必然一个是另外一个的后缀。

这个挺好证的,感性理解最好,画个图就知道了。

我们把所有相同的\(right\)集合叫做一个等价类。

如图,\(ab,b\)就是在一个等价类里面。

2

两个\(right\)集合,要么两个集合没有相同的数字,要么一个集合是另外一个集合的子集。

这个也挺好证的,用反证法,如果\(right\)集合中有一个相等的,那么我们就可以进而推出一个是另外一个的后缀(自己想想看是不是),那么在想想看是不是一个子串的\(right\)是不是就是那个作为后缀子串的\(right\)的子集。

3

对于每个\(endpos\)(等价类)而言,里面所包含的子串的长度应当是连续的。

假设子串的长度是不连续的,那么设这个不在里面的子串是\(s\),那么比他短的子串在这个等价类里面,也就是说只有这个\(right(s)\)数字的个数小于\(endpos\)里面子串\(right\)的数字个数,才可以,但是比他长的子串也在里面,而这个子串无一例外是这些比他长的子串的后缀,所以只能数字个数只能等于,所以这个\(s\)应当在等价类里面。

4

等价类点的个数的数量级是\(O(n)\)的。

我们可以发现,在一个\(endpos\)里面最长的子串前面加一个字符,就是另外一个\(endpos\)里面的,而这个\(endpos\)里面的\(right\)集合应该是原来\(endpos\)里面的\(right\)集合的子集,因为这个新子串要出现肯定是要这个旧子串的基础上还有个字符相等才可以的。

而我们在旧子串前面添加另外一个不一样的字符,得到的\(endpos\)的\(right\)和原来的新子串\(endpos\)的\(right\)是不相交的,怎么可能有个位置使得两个长度相同但不一样的子串都出现呢?

所以我们可以知道,一个等价类的\(right\)集合通过正确的分割,可以分割成几个其他\(endpos\)的\(right\)集合。

那么从\({1,2,3,...,n}\),开始分割,最多也只能像线段树那样分(为什么,倒着想,两个叶子结点就可以产生一个父亲节点,这是最多的合并方案了),也是最多就\(O(n)\)个罢了,所以节点数量最多\(O(n)\)。

而这个巧妙的分裂方案又呈现出一个树的样子,那么我们就给这棵树起个文雅点的名字:\(Parent Tree\)。

同时一个字符串只有唯一的\(Parent\)树,因为等价类的划分只有一个。

LOOK一下\(aababa\)的\(Parent\)树

点里面的就是\(right\)集合,什么?\(1\)的点为什么不见了?因为\(right\)为\(1\)的子串为\(a\),很不巧的是刚好被\(1,2,4,6\)给包含了进去,但是并不会影响我们,只要以后想题的时候注意一下就行了。

5

我们设等价类里面最长的子串长度是\(len\),最短的是\(minlen\),那么\(len(fa(x))+1=minlen(x)\),其实这个也挺好证明的,儿子最短的子串就是自己最长的子串加一个字符吗,对吧。

Trie?DAG?

我们都知道,有个强大的Trie,我们可以把一个字符串的每个后缀插入进去,然后就成了这样(aabab):

他可以支持查询任意一个子串,子串个数等等操作。

为什么?

因为他有以下的性质:

  1. 走过一条边表示在字符串后面添加字符。
  2. 从源点到任意终点形成的字符串为母串的子串。
  3. 从源点走到任意节点形成的是子串,同时也只能是子串。
  4. 任意两条不重叠路径(允许相交或部分重合)形成的子串不相同。

但是点的个数是\(O(n^2)\)的,那么我们要如何处理这个问题呢。

那么我们就得找到一个符合这个性质的一个好东西。

而且我们也发现上图中的\(abab\)中的\(bab\)可以和其他的\(bab\)结合的。

也就是说我们可以找到一个在\(DAG\)上的一种算法,而不是一棵树。

当然也就是后缀自动机,同时因为他有更多的性质,使得他更加的强大。

当然,特别巧的是,后缀自动机的节点就是\(Parents\)树上面的,边就不是了,边就让我们和眼花缭乱了,而在构造的时候,我们也是用两种理论互相完善形成的。

给出一个图:

自动机?自动鸡?

谈到鸡就不禁想到了jntm,自动jntm?

自动机的基本性质

首先,自动机一般有这么五个东西:初始状态,状态集合,结束状态,转移函数,以及字符集。

那么字符集我们知道,初始状态和结束状态呢?初始状态我们设为空串,也就是\(right\)集合\({1,2,3,4...,n}\),而结束状态则是集合内一个个的后缀,也就是\(Parent\)树中\(right\)集合为\(n\)的以及他的父亲与祖先,除了根节点。

而转移呢,从一个节点\(x\)到另外一个节点\(y\)连一条为\(c\)的边出来,表示的是\(x\)包含的所有子串在后面加上\(c\)后都是\(y\)中所包含的子串(但\(y\)中的子串去掉最后一个字符不一定是\(x\)中的子串,只有可能是\(x\)儿子所包含的子串)。

字符集不就是那个母串吗。

大佬:还以为有多术语,没想到还是口头语。

那么一个成型的后缀自动机大概长这样:

橙点是结束状态,每个点旁边的红色字符串表示的是最长的子串。

一定存在后缀自动机吗?

我们回想起来后缀自动机必须遵循的几个性质:

  1. 走过一条边表示在字符串后面添加字符。
  2. 从源点到任意终点形成的字符串为母串的子串。
  3. 从源点走到任意节点形成的是子串,同时也只能是子串。
  4. 任意两条不重合路径形成的子串不相同。

前面三个都可以手动满足,但是在满足前三个性质后,第四个性质能不能满足?

我们可以发现每个\(endpos\)所含的子串都是不同的,但是同时所有\(endpos\)所含的子串累加起来就是母串的所有不重复子串数量的和。

所以转移我们都是在一部分子串的后面加上一个字符到达的,不可能有两个点含有相同的子串,而且到达一个点形成的子串只能是这个点\(endpos\)所含有的,这不就证出来了?

后缀自动机是唯一的吗?

我不敢保证绝对唯一,只能证在\(Parent\)树上的后缀自动机是绝对唯一的,首先\(Parent\)树是唯一的,而能指向一个\(endpos\)的\(endpos\)数量也是固定的,所以后缀自动机是唯一的。(好草率呀。)

后缀自动机的几个性质

这里总结一下性质,方便下个证明(Trie的性质就不搬了,反正后缀自动机也是满足的),也方便做题:

  1. 到达一个点的话,形成的字符串必须是这个点所包含的子串。(后缀自动机必须满足的性质)
  2. 一个点可能被多个点指到。
  3. 到达\(x\)所形成的的字符串一定都比\(fa(x)\)的长,这个可以用性质\(1\)证。
  4. 通过上诉的结论,我们可以发现后缀自动机是个DAG(有向无环图)。

边的数量级

后缀自动机边的数量级是\(O(n)\)的,为什么,我们对一个后缀自动机求一个生成树出来,删掉其他的边。

然后从每个终点往源点跑自己所包含的子串,往回跑的意思是找到跑到这个终点能形成这个子串的路径,然后往会跑,允许找不到的路可以往回走的时候,可以添加一条边回来,那么我们就可以再次走完一个子串,但是可能不是这个子串,但是我们可以先把走完的子串划掉,然后再跑现在的子串,可以证明,跑完这个终点所有的子串(其实就是母串的后缀),添加回来的新边不会超过这个终点所包含的子串个数。

大家弄不懂为什么添加了一条边就又可以跑出一个子串出来?我们看一下,在生成树上多连一条边出来,那么是不是就是多了一条路径可以到终点了?根据前面的性质我们知道肯定是个不同的子串,所以就可以跑出一个新的子串,所有的终点跑完后,添加的边数不超过\(n\)条(其实这个利用性质的证法我认为更简单,也是我的证法)。

所以数量级为\(O(n)\)。

这里草率的放个形象生动的生成树例子在这,因为我觉得我的证法好像和他的不是很一样。


橙点是终点,神奇颜色的点是源点,黑边是生成树的边,蓝边是要添加回来的边,绿边是删掉未添加回来的边,箭头指向的就是目前要跑的终点,注意是反着跑。

图可能画的不是很标准,只是为了呈现出一个加边多路径的一个情况出来。

我们会发现,加了一条边,就多出现了一条路径,也就多了个子串可以走回去。

所以我们就证明出来了。

构造方法

代码

终于到了最后的时刻,通过看大佬的博客,我发现代码放前面更容易帮助人理解构造的过程。

所以先放上代码。

struct  node
{int  a[30],len,fa;
}tr[N];int  tot=1,last=1;
void  add(int  c)
{int  p=last;int  np=last=++tot;tr[np].len=tr[p].len+1;for(;p  &&  !tr[p].a[c];p=tr[p].fa)tr[p].a[c]=np;if(!p)tr[np].fa=1;else{int  q=tr[p].a[c];if(tr[q].len==tr[p].len+1)tr[np].fa=q;else{int  nq=++tot;tr[nq].fa=tr[q].fa;tr[q].fa=nq;tr[nq].len=tr[p].len+1;memcpy(tr[nq].a,tr[q].a,sizeof(tr[q].a));tr[np].fa=nq;for(;p  &&  tr[p].a[c]==q;p=tr[p].fa)tr[p].a[c]=nq;}}
}

后缀自动机的构建是在线的,也就是把字符串一个个字符丢进去,进行构建。

我们跟着KesdiaelKen大佬一起,分成一个个部分来进行传教讲课。

部分1

int  p=last;int  np=last=++tot;
tr[np].len=tr[p].len+1;

我们知道,扔进去一个字符,那么会改变的肯定就是原字符串的后缀,那么我们就用\(las\)记录包含有原字符串母串的点是哪个点,然后新点\(np\)的最长子串的长度就是新串的长度就是旧串的长度+1。

部分2

for(;p  &&  !tr[p].a[c];p=tr[p].fa/*fa表示的是Parent树中的*/)tr[p].a[c]=np;

我们通过跳\(p\)的父亲,使得每一个原本的终点(含有后缀的点)都有一条指向\(np\)的边,也就是使所有原本的后缀都可以跳到现在的后缀集合。

\(!tr[p].a[c]\)这句话是什么意思呢?如果有个终点也有一条边是指向\(c\)的,首先可以说明的是这个终点包含的后缀在原串中出现了至少两次,否则在旧串中一个后缀,是不可能再在后面加点的,而且也说明了,新串的后缀有一部分已经出现过了,就要进行一些新的判断了。

部分3

if(!p)tr[np].fa=1;
else

很简单,说明原串从未有\(c\)这个数字出现(有的话\(1\)号点会指过去),所以这个新串的所有后缀都在一个\(endpos\)里面,也就是说新串只有一个终点。

当然,跳进了那个\(else\)的话,就代表了新串也不只有一个终点了。

部分4

int  q=tr[p].a[c];
if(tr[q].len==tr[p].len+1)tr[np].fa=q;

首先,我们知道\(p\)所代表的子串都是旧串的后缀,如果\(q\)的最长长度是\(p\)的\(+1\),那么说明\(q\)所代表的的子串都是旧串的后缀\(+c\),自然\(np\)就可以认他做父亲。

这是你又会问了,但是\(q\)只能代表\(np\)的一些后缀呀,比如新串是\(dbcabcabc\),然后假设\(q\)表示的是\({bc}\)这个后缀,但是他有个儿子(\(Parent\)树的儿子)表示的又是\({abc}\),那么根据后缀自动机\(np\)不应该认这个儿子吗?

设这个儿子为\(q'\),我们知道后缀自动机的话层数越深,所代表的子串长度越长,那么指向\(q'\)的一个后缀应该是\(ab\),那么理论上将找到\(ab\)会比找到\(b\)更先,所以认的原本就是最儿子的那个。(:雾

但是你认完了以后也没有继续去上面更新又是怎么一回事,因为上面的后缀肯定都是小于\(tr[p].len\)的了,也就是说以后如果要找新串后缀的话,如果长度小于等于\(tr[q].len\)的话,那么找到的自然就是\(q\)了,那么我们还何必屁颠屁颠的再去加一种方式呢?破坏性质还变慢,赔了夫人又折兵。这么亏的勾当我们才不做呢,认完就完事了。

部分5

int  nq=++tot;
tr[nq].fa=tr[q].fa;tr[q].fa=nq;
tr[nq].len=tr[p].len+1;memcpy(tr[nq].a,tr[q].a,sizeof(tr[q].a));
tr[np].fa=nq;
for(;p  &&  tr[p].a[c]==q;p=tr[p].fa)tr[p].a[c]=nq;

那么如果\(tr[q].len≠tr[p].len+1\),我们知道,这个时候只有\(tr[q].len>tr[p].len+1\)的情况了。

但是同时这个长的串也不是新串后缀,为什么?因为如果是的话,去掉最后一个字母比\(tr[p].len\)还大,所以应该先被跳到才对,为什么现在还没有被跳到呢?因为这个长的串由最后\(tr[p].len+1\)个字母组成的后缀是新串后缀,但是这个长的串不是。

那么我们就涉及到了一个问题了,这个点现在出现了锅了,我们需要把他拆成两个点了,因此申请了一个\(nq\),然后\(nq\)表示的就是这个点的\(tr[p].len+1\)的后缀以及更短的后缀,因为这些子串在后面又出现了一次,而\(q\)因为少了这一次,所以他的\(right\)是\(nq\)的子集,理所应当成为了他的儿子,而根据分割要求,所以\(nq\)的\(right\)还有个数字没有分出去,就是现在新串的长度,刚好,我们的\(np\)的父亲也可以认到\(nq\)身上,这不就解决了吗。

同时虽然分开了,但是能在后面添加的数字还是可以添加的,于是我们可以把\(q\)的\(a\)数组直接拷贝到\(nq\)上面去。

不对,还有个问题,\(p\)以及\(p\)的父亲祖先的\(c\)都指向了\(q\),那么因为\(q\)以前包含了\(tr[p].len+1\)这个长度的后缀,但是现在没有了,跑到\(nq\)上去了,所以我们自然需要用到for然后去更新一下父代。

那么这不就好起来了吗?QMQ

思路

而例题,则十分的明显,就是叫我们把每个点的\(right\)集合处理出来,然后拿集合大小去更新\(ans[tr[i].len]\),然后再把\(ans\)从上到下更新一遍。

我也忘记了我到底想到了一个什么SB的思路,反正忘记了,就不管了吧。

正解就是每次在创\(np\)是,对\(np\)的\(right\)集合的个数++,我们可以知道,一个非叶子结点,他的\(right\)集合的大小就是所有儿子的\(right\)集合的大小,加上自己本身的\(right\)集合的大小。

为什么要算上自己的,自己的不是没有嘛,因为存在掉叶子结点的情况,上文我们有提到\(1\)的叶子结点被包括在了其他节点里面,这就是我所说的考虑这种情况,这种情况一般不会对结果造成影响,但是过程可能要有点变动。

代码

#include<cstdio>
#include<cstring>
#define  N  510000
using  namespace  std;
struct  node
{int  a[30],len,fa;
}tr[N];int  tot=1,last=1;
char  st[N];int  n;
int  dp[N],r[N];
void  add(int  c)
{int  p=last;int  np=last=++tot;r[np]++;tr[np].len=tr[p].len+1;for(;p  &&  !tr[p].a[c];p=tr[p].fa)tr[p].a[c]=np;if(!p)tr[np].fa=1;else{int  q=tr[p].a[c];if(tr[q].len==tr[p].len+1)tr[np].fa=q;else{int  nq=++tot;tr[nq].fa=tr[q].fa;tr[q].fa=nq;tr[nq].len=tr[p].len+1;memcpy(tr[nq].a,tr[q].a,sizeof(tr[q].a));tr[np].fa=nq;for(;p  &&  tr[p].a[c]==q;p=tr[p].fa)tr[p].a[c]=nq;}}
}//后缀自动机
inline  int  mymax(int  x,int  y){return  x>y?x:y;}
inline  int  mymin(int  x,int  y){return  x<y?x:y;}
int  cs[N],sa[N];
int  main()
{scanf("%s",st+1);n=strlen(st+1);for(int  i=1;i<=n;i++)add(st[i]-'a');for(int  i=2;i<=tot;i++)cs[tr[i].len]++;for(int  i=1;i<=n;i++)cs[i]+=cs[i-1];for(int  i=2;i<=tot;i++)sa[cs[tr[i].len]--]=i;int  p=1;for(int  i=tot;i>=1;i--)r[tr[sa[i]].fa]+=r[sa[i]];//以上按深度排序的部分其实是可以直接一发DFS暴力解决的,但是这样打也可以。for(int  i=2;i<=tot;i++)dp[tr[i].len]=mymax(dp[tr[i].len],r[i]);for(int  i=1;i<=n;i++)printf("%d\n",dp[i]);return  0;
}

时间复杂度

你问我时间复杂度?我们可以发现能影响时间复杂度的就这两句话:

for(;p  &&  !tr[p].a[c];p=tr[p].fa)tr[p].a[c]=np;for(;p  &&  tr[p].a[c]==q;p=tr[p].fa)tr[p].a[c]=nq;

第一句因为是加边,所以不会总体不会大于\(O(n)\)。

更严谨的证明,跳\(for\)循环\(last\)的层数会减,而二操作最多层数\(+1\),所以是\(O(n)\)


第二句话我们考虑\(short(x)\)表示的是\(x\)这个点的最短子串的长度,\(fa(x)\)为\(tr[x].fa\),那么我们接着考虑一下\(short(fa(last))\)会有怎样的变化。

我们知道原循环有两个\(if\),两个\(else\),三种情况。

如果跳到第一种情况,那么\(short(fa(last))\)就会等于0.

如果在第二种情况,我们考虑一下\(p\)在哪,在加字符前,我们会发现\(short(p)<=short(fa(last))\),为什么会相等,因为\(p\)有可能就是\(fa(last)\),然后跳到了\(q\),因为只加了一个字符,所以\(short(q)<=short(p)+1\),所以在我们可以知道\(short(nq)<=short(fa(last))\)(没加新字符的\(last\)),所以我们就可以知道,在第二种情况下,我们的\(short(fa(last))\)会\(+1\)或者更小。

第三种情况下,我们同样可以知道\(short(q)<=short(p)+1\),然后把\(q\)拆成了\(q\)和\(nq\),同时我们不是要往上跳吗,我们知道中父亲的short肯定是比儿子要少\(1\)的(不管是Parent Tree还是后缀自动机),然后我们也知道这个for循环的重定向是把原来指向\(q\)的指向与\(nq\)了。

而我们的for会跳几层几层呢?仔细想想最多不就是\(short(p)+1-short(q)+1\)吗,因为你从\(p\)开始跳的话,\(short(p)\)会不断减少,如果\(short(p)+1\)还小于\(short(q)\)的话,那么不就指不到\(q\)了吗,所以只会跳\(short(p)-short(q)+2\),而最坏情况\(short(p)=short(fa(last))\),而我们新的父亲就是\(nq\),仔细想想发现跳的次数\(-1\)就是\(short(fa(last))\)减少的个数。

所以总结下来,\(n\)次调用,最多出现\(n\)个\(1\),所以减少的也是最多\(n\)次,可以得出为\(O(n)\)。


中间不是有个拷贝吗?但是那是字符集的大小,也就是说字符集固定的话复杂度是线性的。

不固定呢?我们可以给每个节点用map呀,当然也有大佬用B-树的。

这里用上https://www.cnblogs.com/hyghb/p/8445112.html大佬的话

首先,我们曾经说过要保证字母表的大小是常数。否则,那么线性时间就不再成立:从一个顶点出发的转移被储存在B-树中,它支持按值的快速查找和添加操作。因此,如果我们记字母表的大小是k,算法的渐进复杂度将是O(n*logk),空间复杂度O(n)。但是,如果字母表足够小,就有可能牺牲部分空间,不采用平衡树,而对每个节点用一个长度为k的数组(支持按值的快速查找)和一个动态链表(支持快速遍历所有存在的键值)储存转移。这样O(n)的算法就能够运行,但需要消耗O(nk)的空间。因此,我们假设字母表的大小是常数,即,每个按字符查询转移的操作、添加转移、寻找下一个转移——所有这些操作我们都认为是O(1)的。

广义后缀自动机

思路

我们只要每次塞入一个字符串之后,然后把last=1,然后再塞,仔细想想也满足能都识别任意一个字符串的子串。

练习

例题

我们给每个点开个\(set\)存一下以这个点包含的所有串为子串的字符串有哪些,这些可以直接用启发式合并合并出来,如果大于等于\(k\)的话,那么我们就处理他的val为他所包含的字串个数。

然后我们可以知道一件事情,就是加入一个点是可以的话,那么再Parent树的父亲也满足条件,而且我们知道Parent树中一个点的子树里面所有由\(np\)组成的点(不包括\(nq\)的点)的个数就是这个点\(right\)集合的个数,所以我们只要DFS遍历一下统计答案。

#include<cstdio>
#include<cstring>
#include<set>
#include<algorithm>
#define  N  210000
using  namespace  std;
struct  node
{int  y,next;
}a[N];int  len,last[N];
void  ins(int  x,int  y){len++;a[len].y=y;a[len].next=last[x];last[x]=len;}
struct  SAM
{int  a[30],len,fa,id;
}tr[N];int  tot=1,las;
set<int>ss[N];
void  add(int  id,int  c)
{int  p=las;int  np=las=++tot;tr[np].len=tr[p].len+1;ss[np].insert(id);tr[np].id=id;for(;p  &&  !tr[p].a[c];p=tr[p].fa)tr[p].a[c]=np;if(!p)tr[np].fa=1;else{int  q=tr[p].a[c];if(tr[q].len==tr[p].len+1)tr[np].fa=q;else{int  nq=++tot;tr[nq].fa=tr[q].fa;tr[q].fa=nq;tr[nq].len=tr[p].len+1;memcpy(tr[nq].a,tr[q].a,sizeof(tr[nq].a));tr[np].fa=nq;for(;p  &&  tr[p].a[c]==q;p=tr[p].fa)tr[p].a[c]=nq;}}
}
set<int>::iterator  ii;
long  long  val[N];int  n,m;
void  dfs1(int  x)
{for(int  k=last[x];k;k=a[k].next){int  y=a[k].y;dfs1(y);if(ss[y].size()>ss[x].size())swap(ss[x],ss[y]);//减少常数for(ii=ss[y].begin();ii!=ss[y].end();ii++)ss[x].insert(*ii);ss[y].clear();//顺便清空}if(ss[x].size()>=m)val[x]=tr[x].len-tr[tr[x].fa].len;
}
long  long  ans[N];
void  dfs2(int  x,long  long  zz)
{zz+=val[x];if(tr[x].id)ans[tr[x].id]+=zz;//说明他是npfor(int  k=last[x];k;k=a[k].next)dfs2(a[k].y,zz);
}
char  st[N];
int  main()
{scanf("%d%d",&n,&m);for(int  i=1;i<=n;i++){scanf("%s",st+1);int  slen=strlen(st+1);las=1;//别忘了for(int  j=1;j<=slen;j++)add(i,st[j]-'a');}for(int  i=2;i<=tot;i++)ins(tr[i].fa,i);dfs1(1);dfs2(1,0);for(int  i=1;i<n;i++)printf("%lld ",ans[i]);printf("%lld\n",ans[n]);return  0;
}

子序列自动机

这里说说,子序列按顺序,但是不一定是连续的,懂吧。

放上https://blog.csdn.net/litble/article/details/78997914的介绍,写的很好


后缀自动机的一条路径是原串的一个子串,那么序列自动机上的一条路径就是原串的一个子序列
序列自动机很好写,就是每次查看最后出现过的一些表示字母x的节点,如果它们没有当前插入的字符y的儿子,那么就将它们的y儿子赋为当前节点,显然这样可以表示出原串的所有子串。

void ins(int x) {++SZ,pre[SZ]=last[x];for(RI i=0;i<26;++i) {int now=last[i];while(!ch[now][x]) ch[now][x]=SZ,now=pre[now];}last[x]=SZ;
}

转载于:https://www.cnblogs.com/zhangjianjunab/p/11411543.html

简(kun)单(nan)到让我开(jue)心(wang)的后缀自动机全家桶(普通后缀、广义后缀、子序列)...相关推荐

  1. 首批6款5G旗舰手机开卖 最低售价过万!全家桶套餐更酸爽...

    [TechWeb]昨日晚间,中国联通宣布5G先锋计划正式推出,将在北京.上海.广州.深圳率先施行,同时,联通也公布了5G先锋计划首批六款机型的售价,最低10800元起. 据了解,这六款5G机型名称和售 ...

  2. 数据分析入门极简书单

    本文授权转载自麻瓜编程 如果你找一个熟悉的朋友给你推荐书单,他会倾向于越短越好,因为他想把他知道的最好的推荐给你,让你少花时间在不重要的事情上. 但如果你在网上看到一个书单,往往会发现都很长长长长长, ...

  3. 双显示屏切单显时打不开关掉的显示屏上打开的软件的问题

    解决双显示屏切单显时打不开关掉的显示屏上打开的软件的问题 1.打开ps软件,按组合键ctrl+n调出新建窗口,此时在笔记本的显示器上面看不到该窗口. 2.按alt+空格,组合键,松开后按下m键. 3. ...

  4. Vue全家桶 + webpack 构建单页应用初体验

    文章指南 主题   承接这上一篇Vue + Webpack 构建模块化开发框架详解,我们知道了如何使用webpack对vue进行打包,从而开始我们的前端模块化开发之路,这一篇在上一篇的基础上讲解 Vu ...

  5. [模板]多项式全家桶小记(求逆,开根,ln,exp)

    前言 这里的全家桶目前只包括了ln,exp,sqrtln,exp,sqrtln,exp,sqrt.还有一些类似于带余数模,快速幂之类用的比较少的有时间再更,NTTNTTNTT这种前置知识这里不多说. ...

  6. 『抽丝剥茧』手把手快速上带你开启文心大模型的奇妙冒险

    『抽丝剥茧』手把手快速上带你开启文心大模型的奇妙冒险 文心大模型简介 文心大模型ERNIE 3.0系列&&ERNIE-ViLG文生图系列 使用文心大模型 网页快速体验 代码调用之ERN ...

  7. phpcms v9手机站wap后台绑定任意单页面(简单二开)

    默认的phpcms v9手机门户后台管理,添加栏目的时候无法绑定单页面,虽然有人可以直接通过传递单页面id的方式调用,但是毕竟不是很方便,简单二开,让你实现后台栏目绑定任意的单页面 一.前端代码: ( ...

  8. php 表单提交2次,微信开发之php表单微信中自动提交两次问题解决办法

    前言: 最近做微信开发,在微信中访问PHP页面,页面中有个表单.测试发现偶尔会出现表单被自动提交两次的情况,导致流程出错. 问题原因 暂时未找到原因,不过怀疑跟微信本身的机制有关. 解决方法 用ses ...

  9. 如何在html中制作个人简历表单

    这里我给大家介绍一下在html中如何去建一个表格 1.比如以下这个个人信息简历表格 2.在html中用代码做一个表格,首先我们要给他简历一个整体框架,比如这个表格是几行几列,弄好框架之后再去弄里面的合 ...

最新文章

  1. iOS使用shell脚本批量修改属性
  2. 4路外线(NAT+PBR真实案例)
  3. 1.4 异常处理机制及异常处理的基本结构
  4. windows下 sqlplus / as sysdba 报ora-12560的终极解决方法
  5. 语义分割 | segnet 制作自己的数据,如何训练,如何测试,如何评价
  6. ClickHouse 分布式原理:Distributed引擎
  7. 服务器的共享文件夹怎么隐藏,Server200服务器隐藏共享文件夹 隐藏共享文件的方法...
  8. explorer.exe rundll32.exe病毒解决方案
  9. 201706问题记录
  10. java 压缩jar 仓库,java服务安装(一):使用java service wrapper及maven打zip包
  11. HTML中表单能嵌套吗,你可以嵌套的HTML表单?
  12. 【白皮书分享】2021国有企业数字化转型指数与方法路径白皮书.pdf(附下载链接)...
  13. POJ1029 UVA158 Calendar题解
  14. 中国移动创新系列丛书《OPhone应用开发权威指南》读者交流活动圆满结束
  15. C++ boost::upgrade_lock upgrade_to_unique_lock 升级锁 是什么 怎么用
  16. 源码目录变了,修改Eclipse配置:工作空间,JDK
  17. CISSP-OSG-要点总结梳理
  18. 红警3 联网对战平台
  19. 统一身份认证(CAS)中文文档 请多指教
  20. 流程驱动管理vs 数据驱动管理

热门文章

  1. 爬取校园新闻首页的新闻的详情,使用正则表达式,函数抽离
  2. 在CentOS7上配置Open vSwitch和VXLAN
  3. Django使用缓存笔记
  4. Navicat Premium 数据库设计器和报表有什么作用
  5. 【转】Monkey测试6-Monkey Test Log
  6. Android 动态刷新listview中的数据
  7. 《Effective Java读书笔记》--序列化
  8. linux下常用软件下载
  9. 成功搞定了在Windows7中安装SAP了
  10. ansible-playbook剧本使用配置