后缀自动机(SAM)学习笔记

· · 个人记录

后缀自动机(SAM)学习笔记

更好的阅读体验1

更好的阅读体验2

注:文中所有字符串位置从1开始

注:若无特别标出,文中所有子串均为可空子串

注:空串在文中用下划线_表示

注:初始状态与空串代表的状态意义相同

注:文中状态,结点与endpos等价类是同一概念。

注:图中实线表示DAWG边,虚线表示后缀link。

SAM基础知识

引入

SP1811 LCS - Longest Common Substring 题意:求两个字符串的最长公共子串长度。 数据范围:1\leqslant |s|\leqslant 250000

前两个算法不可以通过本题,二分哈希虽然能通过,但数据加强就也无能为力了。

我们需要一个更优秀的算法——最好有线性复杂度,它就是后缀自动机,又称SAM(Suffix AutoMaton)。

SAM的定义

SAM是一个能接受字符串所有后缀的最小DFA(确定性有限自动机),同时它其实也可以表示这个字符串的所有子串。

即可以理解为:SAM是一张DAG(其实这个定义不是特别准确,后面会讲),从SAM的起点开始走,可以通过一些边(相当于字符)进行转移,只有字符串的后缀才能到终止集合(注意,终点不止一个,但起点只有一个),且SAM的结点数是最少的。

学SAM需要知道的

结束位置集合endpos

我们定义endpos(t)为子串ts出现的所有结束位置的集合。(注意,我们定义空串的endpos为所有位置组成的集合)

举个例子,对于字符串s=aabaabbendpos(a)=\{1,2,4,5\}endpos(ab)=\{3,6\}endpos(aab)=\{3,6\}

endpos等价类

我们定义endpos等价类为所有endpos相同的子串构成的集合。

还是上面这个例子:对于字符串s=aabaabbendpos等价类\{3,6\}=\{ab,aab\}

后缀link

对于一个endpos等价类,它在从长到短排序后一定满足后面的字符串是前面的字符串的子串(这个在后面会进行证明),因此我们定义后缀link为两个endpos等价类之间的有向边,且u能向v连后缀link当且仅当u\subseteq vv是具有这个性质的v中集合大小最小的。

这里再举个例子吧,对于字符串ababab,它的所有endpos等价类为:\{1,2,3,4,5,6\}对应\{\_\}\{1,3,5\}对应\{a\}\{2,4,6\}对应\{b,ab\}\{3,5\}对应\{ba,aba\}\{4,6\}对应\{bab,abab\}\{5\}对应\{baba,ababa\}\{6\}对应\{babab,ababab\},因此它对应的后缀link会连成这样的形式:

\{1,3,5\}\rightarrow\{1,2,3,4,5,6\} \{2,4,6\}\rightarrow\{1,2,3,4,5,6\} \{3,5\}\rightarrow\{1,3,5\} \{4,6\}\rightarrow\{2,4,6\} \{5\}\rightarrow\{3,5\} \{6\}\rightarrow\{4,6\}

终止集合&终止链

对于每一个后缀形成的endpos等价类,形成的是终止集合,它们的后缀link一定会形成一条链,我们称这条链为终止链。

Parent Tree

根据后缀link将所有endpos等价类连成的结构叫做Parent Tree。因为所有非空子串代表的集合都会连向大小严格大于它的集合,而空串代表的集合又是最大的,因此这是一颗以空串代表的集合为根结点的树,这棵树有一些优秀的性质,我们之后再讲。

比如,根据字符串ababab,我们构建出来的Parent Tree为:

DAWG(Direcged Acyclic Word Graph)

定义DAWG为能表示字符串所有后缀的图,其中每一个结点都是一个endpos等价类,可以完成SAM的基本操作。

形成DAWG的DAWG边带权,表示某个endpos等价类在后面添加一个字符可以转移到另一个endpos等价类。

DAWG的构造会在后面讲到,这里用上面的字符串ababab举个例子吧:

其实DAWG和Parent Tree是共用一套结点的,只是边不一样而已。

连续的转移

len_xx代表的endpos等价类最长串的长度,那么如果一条从u连向v的DAWG边满足len_u+1=len_v,我们就称这条DAWG的转移是连续的,否则就称是不连续的。

SAM的性质

性质1:对于两个非空子串a,b|a|\leqslant|b|),a\in suffix(b)\Leftrightarrow endpos(b)\subseteq endpos(a)a\notin suffix(b)\Leftrightarrow endpos(a)\cap endpos(b)=\varnothing

这里证的有些问题,之后再补上。

性质2:将一个endpos等价类的所有字符串按长度从大到小排序后,每一个字符串都是其前面的字符串的后缀,且这些后缀是连续的(即长度严格递增)

考虑endpos等价类中两个字符串ab|a|\leqslant|b|),endpos(a)=endpos(b),由引理1得ba的后缀。

然后我们设endpos等价类中最长的子串为ls,最短的子串为sssls的一个后缀且|ls|\geqslant|s|\geqslant|ss|,那么肯定有s\in suffix(ls),ss\in suffix(s),由引理1得endpos(ls)\subseteq endpos(s)\subseteq endpos(ss),又因为ls,ss属于一个endpos等价类,所以endpos(ls)=endpos(ss),故endpos(ls)=endpos(s)=endpos(ss),即s属于这个endpos等价类。

因此,我们证明了一个endpos等价类中最长的子串的所有长度大于等于这个等价类中最短子串的后缀都属于这个endpos等价类,即里面的后缀是连续的。

性质3:endpos等价类的级别为O(n)

考虑一颗表示长度为n的字符串的Parent Tree,由引理1和后缀link的定义可以知道任意父结点的所有儿子一定都是这个父结点互不相交的子集,且每个子节点的大小必定小于父亲结点的大小,因此可以考虑分割关系,根结点的子集大小为n,最多进行n-1次分割,即产生n-1对父子关系,因此endpos等价类的级别是O(n)

这个不懂没关系,在证明复杂度的时候会再次提及(以另一种方式)。

性质4:若存在u连向v的后缀link,那么minlen(u)=maxlen(v)+1

上面讲过,后缀link可以看做分割关系,即被连向的状态分割成所有连向它的状态(注意,某些endpos可能会丢失,这个后面会讲到),因此,我们可以考虑在某一个Parent Tree中非叶子节点代表的endpos等价类中最长串前面添加一个字母,使得它的endpos不属于这个结点的endpos等价类,因此这是一个新的endpos等价类,而根据后缀link的定义,它的后缀link应该连向原来的结点。某些结点可能是串的前缀,在前面是无法添加字母的,因此可能会丢失一些endpos

有了这个性质,我们可以在构造SAM的时候少保存一些信息。

性质5:从初始状态到任意状态,有且仅有一条完全由连续转移组成的路径到达

这个很显然,因为跳连续转移意味着在后面添加字符,如果有两条完全由连续转移组成的路径,那么这两条路径一定是重合的。

SAM的构造

SAM一般指Parent Tree与DAWG一整套结构,下面我们对这两个结构的构造进行讲解:

DAWG

DAWG可以用一种叫做增量法的方式进行在线构造:

首先定义变量:len_iendpos等价类i中最长的子串,link_ii的后缀link,nxt_{i,c}i可以通过字符c转移到什么状态。

由于是在线构造,因此我们考虑每次都加入字符c,当前位置为p(当前位置为当前后缀自动机表示的串所处的endpos等价类),并让新字符串中所有的后缀被表示出来。

我们考虑哪些点需要更新,由于要让所有的后缀被表示出来,因此只需要之前的最长后缀代表的endpos等价类last到空串代表的endpos等价类(即新的终止链)都可以转移到表示现在所有后缀的状态就可以了。

那么我们首先新建一个结点now,代表这些新后缀代表的endpos等价类,很显然len_{now}=len_{last}+1(即新的串为旧的串后面添加一个字符),我们只需要将所有上述状态用一条字符为c的边连向now就可以完成构建了。

看一种情况:

我们需要将终止链上的状态连向now,然后考虑now的后缀link怎么连,因为是now这个状态代表的endpos\{p\},而没有其他的状态代表的endpos中含有位置p,所以now的后缀link需要连向root(即空串表示的状态)。

但是远远没有这么简单,在下图中,v3已经有一条字符为c的边了,此时该怎么处理呢?

首先还是新建状态now,并连接v1v2连向now的边。

此时,我们需要分类讨论一下:

如果len_{v3}+1=len_d,即v3最长的子串后面添一个字符c就是状态d的最长子串,那么d一定代表的最长串为t+c,其中tv3中最长的串,也是v1中最长的串s的子串。而now代表的最长串为s+ct+cs+c的后缀,故now需要向d连后缀link。(不过此时x结点不存在,可以思考一下)

否则,len_{v3}+1<len_d(因为不可能大于,如果大于那么肯定存在更优的属于d状态的串t+c使len_d=len_{v3}+1),这就有一个严重的问题:d中所有的子串,有些在v3更新后会多一个地方结束,导致d不属于一个endpos等价类、

此时我们需要克隆出一个新状态r存储v3更新后会影响的子串,因为这些子串可以通过原先d的DAWG边进行转移,所以我们要先让它接受所有dnxt信息(即连出的DAWG边)。

考虑如何更新各个经过修改的结点的后缀link:now的后缀link,d的后缀link,r的后缀link。

因为存在v1v3的后缀link组成的路径,所以d最长串t+ctv3中某一个串)为now最长串s+csv1最长串)的后缀,所以now的后缀link可以连向r

同理,因为rv3某些串加上字符c构成的endpos等价类,所以r最长串可以用t'+c表示(t'v3中某一个串)。考虑tt'的关系,因为加上字符ct结束的地方不会改变而t'出现的地方会改变,所以肯定t'长度小于t,又因为tt'属于一个endpos等价类,所以t'+ct+c的后缀,t+cendpos一定是t'+cendpos的子集,即d的后缀link需要连向r

同时,由于d原本的后缀link连向y,由引理4可以知道y中所有的串都是d中所有的串的后缀,在克隆之后r会替代d将后缀link连向y

最后,我们遍历剩余的终止链,由于v3后加c可以转移到状态d,所以v4以及之后的状态加c都可以转移到状态d,因此这些状态都会有一条字符为c的DAWG边连向状态d,更新之后可以通过这些状态加c转移到的子串的endpos一定会改变,因此它们的字符c边需要连向r

当然,这条终止链上可能还有其他的结点,它们如果不指向状态d,那么一定会有一条同样字符的边指向其他的状态(d以前的后缀link一定会连向这些状态),这时候我们就不用管了。

代码(原则:理解性默写):

int extend(int last,int x){
    int now=++tot,pos=last,tmp,cln;
    len[now]=len[last]+1;
    while(pos!=0&&nxt[pos][x]==0)
        nxt[pos][x]=now,pos=link[pos];
    if(pos==0){
        link[now]=1;
        return now;
    }
    tmp=nxt[pos][x];
    if(len[tmp]==len[pos]+1){
        link[now]=tmp;
        return now;
    }
    cln=++tot;
    for(int i=1;i<=26;i++)
        nxt[cln][i]=nxt[tmp][i];
    len[cln]=len[pos]+1;
    link[cln]=link[tmp],link[tmp]=link[now]=cln;
    while(pos!=0&&nxt[pos][x]==tmp)
        nxt[pos][x]=cln,pos=link[pos];
    return now;
}

主函数内,维护一下last就可以了:

cin>>s;
n=s.size(),s=" "+s;
last=++tot;
for(i=1;i<=n;i++)
    last=insert(s[i]-96);

Parent Tree

Parent Tree很容易建,只需要把所有后缀link连边就好了。(注意,为了让空串代表的状态为根结点,因此需要反着建边)

代码:

for(i=2;i<=tot;i++)
    add(link[i],i);

DAWG和Parent Tree的差别与联系

SAM的正确性证明

因为每一次添加字符一定会让当前的终止链中所有结点(包括空串代表的状态)存在一条边权为这个字符的DAWG边,所以SAM一定可以表示出字符串的所有后缀,进一步,也可以表示字符串的所有子串。(因为走后缀的时候一定会走过后缀的前缀,而子串可以理解为后缀的前缀)

SAM的复杂度证明

这里假定字符集大小是常数,那么SAM的复杂度为O(n),否则SAM的复杂度为O(n|\Sigma|)n为字符串长度,|\Sigma|为字符集大小)

SAM的状态数是O(n)

按照建SAM的过程,有一个初始状态(空串),第一次添加状态和第二次添加状态的时候不可能进行克隆,后面每一次都有可能进行克隆,因此总状态数最多为1+1+1+2\cdot (n-2)=2n-1,是O(n)的。

实际上,存在状态数达到上限的情况:abb\cdots bb

因为SAM中所有结点是这个串所有的endpos等价类,所以自然endpos等价类的级别也是O(n)的。

SAM的转移数是O(n)

我们构造以空串代表的状态为根的最长路径生成树,那么这个生成树上的转移一定是连续的转移(否则一定找得出一条更长的路径来代替树上不连续的转移)。我们称在最长路径生成树上的转移为树边,其他的转移为非树边。因此有且仅有一条路径可以通过树边从根走到任意结点。

我们考虑每一条非树边a\rightarrow b(边权为c),我们可以用字符串u+c+w来表示从空串代表的状态到a状态的最长路径u(那么u一定在最长路径生成树上),a\rightarrow b的边权c,状态b到终止状态的最长路径v。那么,如果非树边的a不同,其他的相同,那么在最长路径生成树上的u一定不同;如果转移的b不同,a相同,那么一定c不相同(由DAWG边的定义可得);如果c不相同,那么这条路径也一定不相同。因此,我们可以发现每一条非树边都只能映射到一条这样的路径,即非树边和这些路径是单射的。

得到了非树边和路径是单射的后,我们考虑这些路径的含义:因为这些路径都是从空串代表的状态到终止状态的路径,因此每一条路径都代表字符串的一个后缀,且完整串代表的后缀一定不在这些路径上(因为完整串代表的路径上的转移一定都是连续的,而由SAM的性质“初始状态到某一状态连续转移组成的路径一定只有一条”得完整串代表的路径一定在最长路径生成树上),所以非树边的个数为真后缀(即非完整串的后缀)代表的路径的个数,因此非树边最多有n-1条。

最长路径生成树的边数(即转移数)为点数减一,点数最多为2n-1,因此树边总数最多为2n-2。加上n-1条非树边,SAM的转移数最多为3n-3,为O(n)的。

实际上,SAM的转移数无法到达上界,因为如果状态数达到了上界,那么串一定是abb\cdots bb的形式,而它的转移数却无法到达3n-3(如果状态数无法到达上界,那么转移数也无法到达上界)。所以SAM的转移数真正的上界是3n-4,且存在情况使转移数到达上界:abb\cdots bbc

构建SAM的时间复杂度是O(n)

我们看SAM的构建代码:

int extend(int last,int x){
    int now=++tot,pos=last,tmp,cln;
    len[now]=len[last]+1;
    while(pos!=0&&nxt[pos][x]==0)
        nxt[pos][x]=now,pos=link[pos];
    if(pos==0){
        link[now]=1;
        return now;
    }
    tmp=nxt[pos][x];
    if(len[tmp]==len[pos]+1){
        link[now]=tmp;
        return now;
    }
    cln=++tot;
    for(int i=1;i<=26;i++)
        nxt[cln][i]=nxt[tmp][i];
    len[cln]=len[pos]+1;
    link[cln]=link[tmp],link[tmp]=link[now]=cln;
    while(pos!=0&&nxt[pos][x]==tmp)
        nxt[pos][x]=cln,pos=link[pos];
    return now;
}

首先,里面非循环的复杂度是O(1),而extend的次数是n次,所以非循环的总复杂度是O(n)的。

然后,我们看向3个循环:

故,我们证明了一个很美妙的性质——SAM的时间复杂度是O(n)的!

SAM的复杂度优化

上面,我们证明了SAM的复杂度是O(n)的,但是,如果字符集过大,那么字符集大小|\Sigma|也会对复杂度进行影响,我们需要更优的复杂度!

代码很简单(离散化不给出):

map<int,int>nxt[maxn];

int extend(int last,int x){
    int now=++tot,pos=last,tmp,cln;
    len[now]=len[last]+1;
    while(pos!=0&&nxt[pos][x]==0)
        nxt[pos][x]=now,pos=link[pos];
    if(pos==0){
        link[now]=1;
        return now;
    }
    tmp=nxt[pos][x];
    if(len[tmp]==len[pos]+1){
        link[now]=tmp;
        return now;
    }
    cln=++tot;
    nxt[cln]=nxt[tmp]; 
    len[cln]=len[pos]+1;
    link[cln]=link[tmp],link[tmp]=link[now]=cln;
    while(pos!=0&&nxt[pos][x]==tmp)
        nxt[pos][x]=cln,pos=link[pos];
    return now;
}

SAM的应用

作者只做过一些SAM的题,因此SAM的应用总结不全,就这样看看吧(

1.处理各种字符串信息

SAM可以很简单地处理各种关于子串长度,子串出现次数等信息。

子串出现次数:对于某个状态,它的出现次数可以在Parent Tree上记一下数(具体地,对于每一次extendnow,记录tag_{now}=1,然后在Parent Tree上求这个子树的tag之和)

void dfs(int x){
    size[x]=tag[x];
    for(int i=start[x];i;i=then[i]){
        int y=to[i];
        dfs(y);
        size[x]+=size[y];
    }
}

(其中size就是出现次数)

子串长度:某个状态x的包含的所有子串的长度恰好为区间[len_{link_x}+1,len_x],且不重不漏。

似乎没有什么要放的代码,凑个数QAQ

子串长度出现次数:对于某个状态,区间修改一下子串长度区间,一般可以差分做。

cnt[len[link[x]]+1]++,cnt[len[x]+1]--;

for(i=1;i<=n;i++)
    cnt[i]+=cnt[i-1];

P3804 【模板】后缀自动机 (SAM):求所有出现次数不为1的子串出现次数乘该子串长度的最大值,直接统计就可以了。

P5341 [TJOI2019]甲苯先生和大中锋的字符串:求恰好出现k次的子串中出现次数最多的长度,在Parent Tree上求一下各种信息,恰好出现k次的状态都区间修改一下长度的出现次数,可以差分一下,然后前缀和统计答案。

CF802I Fake News (hard):求所有子串的出现次数平方和,直接统计就可以了。

CF123D String:求所有子串的出现次数乘(出现次数+1)之和,直接统计就可以了。

SP8222 NSUBSTR - Substrings:给定长度为n的串s,求长度为1\cdots n的子串在s中最大出现次数。

2.求本质不同子串个数

方法1

void dfs(int x){
    size[x]=len[x]-len[link[x]];
    for(int i=start[x];i;i=then[i]){
        int y=to[i];
        dfs(y);
        size[x]+=size[y];
    }
}

方法2

void dfs(int x){
    if(ans[x])
        return ;
    ans[x]=1;
    for(int i=1;i<=26;i++)
        if(nxt[x][i]){
            dfs(nxt[x][i]);
            ans[x]+=ans[nxt[x][i]];
        }
}

方法3

for(i=1;i<=n;i++){
    last=extend(last,s[i]);
    ans+=(len[last]-len[link[last]]);
}

SP694 DISUBSTR - Distinct Substrings求本质不同子串个数。

SP705 SUBST1 - New Distinct :求本质不同子串个数,是上一题加强版。

P2408 不同子串个数:求本质不同子串个数。

P4070 [SDOI2016]生成魔咒:求本质不同子串个数,离散化+map优化。

SP32951 ADASTRNG - Ada and Substring:求某个字母开头本质不同子串个数。

#6071. 「2017 山东一轮集训 Day5」字符串:求多个串可空子串拼接后不同子串个数,对每个串建SAM,然后用子序列自动机把DAWG拼接起来,形成的新DAG有个美妙的性质:它上面的路径对应着所有新字符串的子串。这样,我们就只需要在上面计算路径数量就可以了。

P6139 【模板】广义后缀自动机(广义 SAM):求多串本质不同子串,建广义SAM后直接算

3.匹配子串/匹配循环同构

我们要在s上匹配一个串,可以怎么做呢?

可以考虑不断让这个串进行两个操作:向前增加字符来进行匹配,将无法继续匹配的字符丢弃(以匹配后面的字符)。

我们在学习笔记辨析过Parent Tree和DAWG的差异与联系,在这里派上了用场:

如何在后面增加字符:跳一个为这个字符的DAWG边,一定让你恰好增加了一个字符。

如何在前面丢弃字符:每一次丢弃字符都暴力跳后缀link,直到到根结点或者存在当前匹配字符的DAWG边。由后缀link的定义可以知道跳后缀link是让你的endpos发生改变的最小代价(即存在机会让你继续匹配),因此如果可以继续匹配,暴力跳后缀link一定可以跳到能匹配的状态。

因为每个状态最多遍历一遍,所以复杂度是O(n)的。

int now=1,l=0;
for(i=1;i<=n;i++){
    while(now!=0&&nxt[now][s[i]]==0)
        now=link[now],l=len[now];
    if(nxt[now][s[i]])
        now=nxt[now][s[i]],l++;
    ......
}

循环同构:s的循环同构包括s_1\cdots s_ns_2\cdots s_ns_1s_3\cdots s_n s_1 s_2\cdotss_ns_1\cdots s_{n-1}(比如说,aab的循环同构有aabbaaaba)。

循环同构的匹配和普通串的匹配差不多,你只需要破环为链就可以了。(不过有一点要注意,如果匹配的长度大于该字符串的长度,那么需要强制失配)

破环为链就不需要代码了吧QAQ

CF235C Cyclical Quest:给定s和若干个询问串t,求每个t的所有本质不同循环同构在s的出现次数之和,循环同构破环为链就好了,到了长度需要强制失配,本质不同只需要打个标记就可以了。

4.最小表示法

最小表示法:求字符串字典序最小的循环同构

很简单,只需要倍长一下(因为要破环为链),建好SAM,然后在DAWG上贪心走字典序更小的边就可以了(也可以用mapnxt,然后用begin()跳,这样快一些,而且在字符集过大之时有很好的优化作用)。

放的是第二种做法。

int now=1;
for(int i=1;i<=n;i++){
    int x=nxt[now].begin()->first;
    printf("%d ",x);
    i=nxt[i][x];
}

P1368 工艺 /【模板】最小表示法:板子题

5.求k大子串

本质不同k小子串:

void getcnt(int x){
    if(cnt[x])
        return ;
    cnt[x]=x==1? 0:1;
    for(int i=1;i<=26;i++){
        int y=nxt[x][i];
        if(y==0)
            continue;
        getcnt(y);
        cnt[x]+=cnt[y];
    }
}
void query(int x,int k){
    if(k<=0)
        return ;
    for(int i=1;i<=26;i++){
        int y=nxt[x][i];
        if(y==0)
            continue;
        if(k>cnt[y]){
            k-=cnt[y];
            continue;
        }
        printf("%c",i+96);
        query(y,k-1);
        return ;
    }
}

位置不同k小子串:

void getsize(int x){
    size[x]=tag[x];
    for(int i=start[x];i;i=then[i]){
        int y=to[i];
        getsize(y);
        size[x]+=size[y];
    }
}
void getcnt(int x){
    if(cnt[x])
        return ;
    cnt[x]=x==1? 0:size[x];
    for(int i=1;i<=26;i++){
        int y=nxt[x][i];
        if(y==0)
            continue;
        getcnt(y);
        cnt[x]+=cnt[y];
    }
}
void query(int x,int k){
    if(k<=0)
        return ;
    for(int i=1;i<=26;i++){
        int y=nxt[x][i];
        if(y==0)
            continue;
        if(k>cnt[y]){
            k-=cnt[y];
            continue;
        }
        printf("%c",i+96);
        query(y,k-size[y]);
        return ;
    }
}

P3975 [TJOI2015]弦论:求本质不同/位置不同的k小子串

SP7258 SUBLEX - Lexicographical Substring Search:求本质不同k小子串

6.求公共串

我们定义end_{x,y}代表状态x是否在串y中出现,这样我们每一次插入可以记录end数组。然后在Parent Tree上合并一下,可以用bitset优化到O(\frac{nm}{\omega})n为SAM状态数,m为串数),实际上也可以用线段树合并(见下面的"维护endpos")。

bitset<maxn>end[maxn];
int extend(int last,int x){
    ......
    end[now][t]=1;
    ......
}
void dfs(int x){
    for(int i=start[x];i;i=then[i]){
        int y=to[i];
        dfs(y);
        end[x]|=end[y];
    }
    for(int i=1;i<=n;i++)
        if(end[x][i]==0)
            return ;
    ans=max(ans,len[x]);
}

SP1811 LCS - Longest Common Substring:求两串公共串,也可以用第二个串在第一个串上跑匹配。

SP1812 LCS2 - Longest Common Substring II:求多串公共串

SP10570 LONGCS - Longest Common Substring:多组数据,求多串公共串

P5546 [POI2000]公共串:求多串公共串

P2463 [SDOI2008]Sandy的卡片:求所有串的相同段(其中相同段的定义为这一段统一加上某个数就会变成另一端),差分一下就是求公共串的裸题

7.求后缀的lcp/前缀的lcs

有一个性质,反串SAM的Parent Tree是正串的后缀树,然后我们就可以利用SAM建后缀树了。 然后还有一个性质,两个串的$lcs$是它们在后缀树上对应的结点的$lca$(很显然,感性理解一下就好了),这样我们就可以用SAM求一些神奇的操作了。 有时候会和虚树dp搭配在一起,SAM(细节少,不直观)+虚树dp(细节多,直观)=SAM上虚树dp(细节多,不直观)QAQ。 [P4248 [AHOI2013]差异](https://www.luogu.com.cn/problem/P4248):求后缀的lcp,SAM辅助建后缀树,也可以考虑式子的意义,然后用点分治的思想给路径计数。 **[P1117 [NOI2016]优秀的拆分](https://www.luogu.com.cn/problem/P1117)**:SAM+部分结论+插点统计贡献(调和级数) **[CF1073G Yet Another LCP Problem](https://www.luogu.com.cn/problem/CF1073G)**:SAM上虚树dp求$lcp$。 **[SP687 REPEATS - Repeats](https://www.luogu.com.cn/problem/SP687)**:论文题。 ## 8.维护$endpos

有些题目需要求SAM中endpos,我们可以用几种方法来维护。(推荐使用方法3,偷懒可以用方法2,但不保证不会TLE)

注:SAM上线段树合并,经常和树上倍增搭配在一起,就是屑上加屑。

CF1037H Security:给定sm组询问,每组询问给定l,r,t,求字典序最小的s'满足s's的子串且s'的字典序大于t(即求s_{l\cdots r}t的前缀),我们可以用线段树合并维护出endpos等价类,然后寻找替代t每个字符的最小代价。具体地,我们边执行子串匹配(注意,这里的匹配不可以丢弃前面的字符,如果无法匹配下去就直接退出匹配),边从t_i后面的字符到z字符不断找,如果后面接上这个字符还在存在endpos在限定的区间[l+len,r]内(其中len为已匹配的长度),那么这个字符是最小的能代替当前字符的字符。直到我们匹配完整个串,我们就贪心从后往前扫,扫到第一个可以替代的字符进行替代,然后输出。(有一个细节,匹配的长度需要到t的长度+1,比如说s=aaaaa,t=a,l=1,r=3,此时答案为aa

P4094 [HEOI2016/TJOI2016]字符串:SAM+线段树合并+二分答案

CF666E Forensic Examination,广义SAM+线段树合并+子串匹配+树上倍增