ジェーン(くん)シングル(ナン)私はオープンできるようにする(JUE)心(王)サフィックスオートマトンの家族のバケット(共通接尾、一般化接尾辞配列)が

リファレンス

:、カッコウ毎日貧しい製品に誰がありません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サフィックスオートマトン少ないCHEN李杰ギャング論文?ORZ

ブロガーは自分の書かれたブログの他の人々のビューを保持し、本当に臭い恥知らずです

サフィックスオートマトンは神の事でしょうか?

実際には、我々は我々のニーズ、この時間を満たすことができない、時には接尾辞配列、接尾辞配列を行うことができますが、話題の多くは、サフィックスオートマトンが登場しました。

问题描述 

给出一个字符串S(S<=250000),令F(x)表示S的所有长度为x的子串中,出现次数的最大值。求F(1)..F(Lengh(S)); 

样例输入 

ababa 

样例输出 

3
2
2
1
1 

等価クラスと親ツリーの右のセット

定義

まず第一に、我々はというものを定義する必要があります(右\)\、各サブ文字列を持つことができます\(右\)

\(右(S)\)を表す\(S \)をすべての文字列をサブストリング右終点位置を発生します。

類推:で\(アベバ\)で、\(右(AB&)= {2,4} \)

等価クラスのプロパティ

1

2非同一のサブ文字列を追加します\(右\)セット後、さらに接尾辞がされている必要があり、同じです。

これは非常に良い可能にし、最高の感情的な理解は、知っているマップを描きます。

我々は、すべて同じ入れ\(右\)コレクションが等価クラスと呼ばれています。

示されるように、\(AB&、B \)は、内部等価クラスです。

2

二つの\(右\)セット、またはではない2つの数の同じセットを持っている、またはセットが別のセットのサブセットです。

これも非常に良いライセンスで、矛盾によって、場合\は(右\)等しいがあるコレクションでは、我々はその後、接尾辞を発売することができ、(自分自身の考えではありません)別で、それについて考えていますない部分文字列\(右\)ではないという接尾辞部分文字列として\(右\)のサブセット。

3

\(endposの\) クラスを同値)の長さは、サブストリングが連続的であるべきである含んでいます。

これは、サブストリングの内側ではありません提供、部分文字列の長さが不連続であると仮定\(S \) その後、彼はそれだけの意味で等価クラスのサブストリングよりも短くなっている\(右(複数可) \)桁数未満\(endposの\)部分文字列内の\(右\)の桁数、することができますが、長い-部分文字列は内部にあり、これは例外なくよりもこれらの部分文字列である彼長いサブストリングサフィックス、それが唯一の桁数に等しいことができ、そのよう\(S \)が等価クラス内にある必要があります。

4

点の数は、等価クラスの数は、レベルである\(O(N)\)

当社で見つけることができます\(endposの\)プラス文字の前で最長の部分文字列の内側に、別のです\(endposの\)の内側、そしてこの\(endposの\)内の\(右\)コレクションはする必要があります元\(endposの\)内の\(右\)セットのサブセット、新しい部分は間違いなく、これは古い文字の部分文字列だけでなく、可能性の前に平等に基づいて表示したいので。

そして、我々はその結果、古いストリングの前に別の別の文字を追加します(\ endposの)\をされた(右\)\と、元の新しい部分の\(endposの\)がされ(右\)\互いに素です、どのように同じですが、2はそれを見える部分文字列の長さと同じではなく、その位置があるだろうか?

だから我々は、等価クラスということを知ることができます\(右\)正しい部門を通じて収集し、他のいくつかに分割することができます(\ endposの)\される(右\)\コレクションを。

そうから\(\ {...、N-、2、3、})最ものみ(なぜ、酒を注ぎ、2つのリーフノードが父ノードツリーラインとして生成することができ、分割することができるように、分割し始めそれは、それはまた、ほとんどありません)合併計画次第です\(O(N)\)何も、そのノードの最大数\(O(n)と\)

そして、この独創的な手法とは道を示すツリーを分割するので、私たちは木立てエレガントなポイントの名前与えた:\(親木を\)

同時に、文字列しかない\(親\)一つだけ等価クラスの部門があるので木は、。

見てください\(aababa \)される(親\)を\ツリー
ここに画像を挿入説明
れるポイント(右\)\コレクション、何?\(1 \)なぜドットがなくなっていますか?以来\(右\)する(1 \)を\サブストリングの\(\) 残念ながらちょうどされている\(1,2,4,6-を\)その中に含まれるが、影響はありません。私たちは、限り問題として、後の行にそれに注意を払うようにしたいとき。

5

我々は、サブストリングの最も長い長さである同値クラスのセット\(LEN \)最短であり、\(MINLEN \)次いで、(= + LEN(FA(X))。1 MINLEN(X)\)\、実際には、また、非常に良い証明、息子最短ストリングは、あなたが右、文字を追加するサブストリング最長です。

トライ?TODAY?

私たちは皆、強いトライがあることを知って、私たちはそれに挿入された各サフィックスの文字列を置くことができ、それがこの(aabab)のようになる:
ギャングマップ
彼はすべてのクエリ文字列、サブ、および操作のように数で子供をサポートすることができます。

なぜ?

彼は次の性質を持っているので。

  1. 文字の文字列の一の側端を介して追加表現。
  2. 文字列は、ベース文字列の文字列のような任意のエンドポイントに元から形成されています。
  3. 任意のソースノードサブストリングが形成されてから来て、だけでなく、唯一のサブストリング。
  4. 任意の2つの経路が同一のサブストリングに形成されていない(交差または部分的にオーバーラップ可能)重複していません。

しかし、点の数がある\(O(N ^ 2) \) 、その後、我々はまだこの問題に対処しますか。

だから我々は良いことに会うこのような性質を見つける必要があります。

そして、我々はまた、上記の図ことがわかった\(ABAB \)\(BABの\)ができ、他の\(BABの\)組み合わせ。

言い換えれば、我々は見つけることができます\(DAG \)のアルゴリズムではなく、木を。

もちろん、それはサフィックスオートマトンである、と彼は彼がより強力になって、より多くの文字を持っているので。

もちろん、特に偶然、ノードサフィックスオートマトンはある\(両親\)構築時に、我々は2つの理論を形成して改善している一方で、それは、もはやない、と私たちはサイドに目がくらむさせながら、ツリーは、上記A。

図は与えます:

自動機?自動チキン?

そういえば鶏は助けることが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)\)

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

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

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

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

所以我们就证明出来了。

构造方法

代码

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

所以先放上代码。

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\)就是\(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)的。

广义后缀自动机

思路

私たちは単なる文字列を詰めた後、その後、1 =最後には、その後、プラグは、また、すべての文字列の任意の部分文字列を識別満たすことができると思います。

練習

私たちは、開くために各ポイントを与える\(セット\)以上の場合には、これらの経験則は、合併の外に直接組み込むことができるか、サブストリングの文字列にすべての文字列が含まれています。この時点で預金を(k個\)\、そして、その後、我々は彼のvalを扱う文字列の数は、彼を含まれています。

その後、我々は一つのことを知ることができ、ポイントの追加条件を満たすようにする場合、再度親木の父可能であり、我々は内部のサブツリーの時点ですべての親木を知っている\(NP \)ドット(ではありません含む(NQの\)\をポイント数)がポイントです\(右\)限り、我々は答えについて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;//说明他是np
    for(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;
}

おすすめ

転載: www.cnblogs.com/zhangjianjunab/p/11411543.html