「SAM」你的名字

  第二次做了

  回过头来看自己第一次写的代码简直一派胡言

  重新看的题解+骚扰Lrefrain解释代码,耗时一整天

  然后终于明白了

  纪念之

  

  给定$S[]$,多次给定$T[],L,R$,询问$T$有多少本质不同的子串是在$S[L...R]$出现过的

  $|S|<=5e5 |T|<=1e6$

  

  首先考虑$L==1,R==n$的情况怎么做,

  一个直观的想法是拿$T$在$S$的$SAM$上跑

  然后沿路打上标记。然而$SAM$上的一个节点代表长度连续的多个串,可能并不能全部跑到

  所以要维护一个变量$plen$表示已匹配的长度,每个节点标记上有$min(plen,x->len)-x->f->len$串被跑到了

  然后再遍历所有跑过的节点累加就行了?

  既然已经注意到每个节点代表的多个子串中,跑到其中一个必定能跑到它的所有后缀

  不应该忽略$fail$树上此节点父链上所有的子串都是其后缀,都可以跑到

  所以每次都暴力跳父链打上$min(plen,x->len)-x->f->len$的标记,喜提$O(n^2)$

 

  发现跳父链的时候打上的标记全都是$x->len-x->f->len$,即全都包含

  则如果发现当前的x已经是这个标记了就不用再跳了,喜提$O(n\sqrt{n})$

  

  为什么这么不优秀,因为被打标记的节点数的上限是和$|S|$有关的,这不好

  不如只用$SAM(S)$维护$plen$,同时在$SAM(T)$上跑并打标记,喜提$O(n)$

  (其实全世界只有我一开始在$S$上打标记,所有人都想到了同时在$T$上跑。)

  

  然后若$L,R$任意,需要得到$SAM(S[L...R])$,但是不好做

  由于$SAM(S)$可以从$root$跑出$S$的所有子串,所以可以仍然依托$SAM(S)$的整体结构

  而用$endpos$集合来检查是否符合$[L,R]$的限制

  考虑怎么用$endpos$集合来达到等效目的,观察用$SAM(S)$做了什么

  1.寻找目前节点$x$有无字符$c$的出边,有就转移过去

    设目前完成匹配的子串为$C[1...plen]$,此时的$endpos$为$p$,

    其实更应该表述为匹配完成前$plen$时有没有出边,因为通过减小$plen$可能使得原本没有出边现在有出边

    若$C[1...plen]+c$仍在$S[L,R]$中,必满足

    $p-plen+1\in[L,R]$,$p\in[L,R]$

    即$L+plen-1<=p<=R$

    检查$x->son[c]$有无在区间$[L+plen,R]$中的$endpos$即可

    (由于此时$x=x->son[c],++plen$这行代码还未执行,$plen$还是未加字符$c$前的$plen_0$,不等式中的$plen=plen_0+1$)

  2.跳$x$的$parent$

    不论是$SAM(S[L...R])$还是整个串的$parent$树

    $x$的父链都包含了所有的$x->len$个后缀

    而整个串的$SAM$的可能更长一些,但是总的跳父亲次数怎么也不会超过$|T|$

    所以完全可以直接使用,不会遗漏任何出现过的最长后缀就完事了

  3.查询$x$的$len$

    首先思考真的需要查询$x$在$SAM(S[L,R])$的len吗?

    答案是否,只想要$T$在$S[L,R]$能维持最长多长的匹配,只是当$L=1,R=n$时,恰好等于$x->len$而已

    所以接下来求出来的并不是真正的$len$,而是最长匹配长度。

    

    首先在$[L,R]$中得有$endpos$

    考虑其中最大的$endpos$,设此节点所代表的 以这个位置为结尾的子串中 最长的一个为$skyh$

    如果$skyh$的左端点$>L$,则在$[L,R]$中的最长长度就等于整个串的$SAM$中这个节点的$len$了

    (可以理解为这个$endpos$完整地呈现了$x->len$)

    如果左端点$<=L$,则最长长度为$maxendpos-L$,

    (所以所谓“真正的len”和最长匹配长度也只有在$skyh$越过左端点时有一点区别,就当我前边在说垃圾话吧)

 

  所以做法就是先建出整个串的$SAM$,然后可持久化线段树合并得到$endpos$集合

  检查当前节点(当前长度),如果有出边,转移过去

  否则$--plen$,如果$plen<=x->f->len$,$x=x->f$

  继续检查有无出边

 

  另一种做法,不枚举最长匹配长度,而是先枚举此长度位于哪个节点。

  检查方法是,将$x->son[c]->f->len+1$代入$plen$,检查$x->son[c]$里有无合法的$endpos$

  如果没有,那最长匹配的串不在这个节点上,跳$parent$(同时将$plen$对$x->f->len$取min)

  否则在这个节点上,$plen=min(x->len,maxendpos-L)$

  

1     while(!eg(l,R,x,ch)&&x!=root) x=x->f;
2     j=min(j,getlen(l,R,x));
3     x=x->c[ch]; ++j;
4     j=min(j,getlen(l,R,x));
很显然的是

 

1     while(!eg(l,R,x,ch)&&x!=root) x=x->f,j=min(j,getlen(l,R,x));
2     x=x->c[ch]; ++j;
3     j=min(j,getlen(l,R,x));
两种都是正确的

 

  关于第二种写法

  为什么$x=x->son[c]$后需要重新将$j$对$getlen()$取$min$呢,难道$getlen()$不会同步增加至少1吗

  $getlen=min(x->len,maxendpos-L)$,$x->len$的确至少增加1了,但是$endpos$极有可能缩小,

  而$eg()$是带入了$x->f->len$不可能关注到这一点,只知道$x$节点里有一个$endpos.$但是不可知道是不是原有的最大的$endpos.$

 

posted @ 2020-06-09 20:00  Yxsplayxs  阅读(186)  评论(0)    收藏  举报