[关闭]
@xiaoziyao 2021-01-31T13:48:41.000000Z 字数 7111 阅读 3098

后缀自动机(SAM)/广义后缀自动机(广义SAM)应用与做题记录

字符串 学习笔记


上一篇:后缀自动机(SAM)学习笔记

做了一些SAM和广义SAM的题,总结出一些套路,把它们记下来以供参考。

SAM板子:

  1. int extend(int last,int x){
  2. int now=++tot,pos=last,tmp,cln;
  3. len[now]=len[pos]+1;
  4. tag[now]=1;
  5. while(pos!=0&&nxt[pos][x]==0)
  6. nxt[pos][x]=now,pos=link[pos];
  7. if(pos==0){
  8. link[now]=1;
  9. return now;
  10. }
  11. tmp=nxt[pos][x];
  12. if(len[tmp]==len[pos]+1){
  13. link[now]=tmp;
  14. return now;
  15. }
  16. cln=++tot;
  17. len[cln]=len[pos]+1;
  18. for(int i=1;i<=26;i++)
  19. nxt[cln][i]=nxt[tmp][i];
  20. link[cln]=link[tmp],link[tmp]=link[now]=cln;
  21. while(pos!=0&&nxt[pos][x]==tmp)
  22. nxt[pos][x]=cln,pos=link[pos];
  23. return now;
  24. }
  1. cin>>s;
  2. n=s.size(),s=" "+s;
  3. last=++tot;
  4. for(i=1;i<=n;i++)
  5. last=extend(s[i]-96);

广义SAM板子:

  1. int extend(int last,int x){
  2. if(nxt[last][x]){
  3. int pos=last,tmp=nxt[pos][x],cln;
  4. if(len[tmp]==len[pos]+1)
  5. return tmp;
  6. cln=++tot;
  7. for(int i=1;i<=26;i++)
  8. nxt[cln][i]=nxt[tmp][i];
  9. len[cln]=len[pos]+1;
  10. link[cln]=link[tmp],link[tmp]=cln;
  11. while(pos!=0&&nxt[pos][x]==tmp)
  12. nxt[pos][x]=cln,pos=link[pos];
  13. return cln;
  14. }
  15. int now=++tot,pos=last,tmp,cln;
  16. len[now]=len[pos]+1;
  17. while(pos!=0&&nxt[pos][x]==0)
  18. nxt[pos][x]=now,pos=link[pos];
  19. if(pos==0){
  20. link[now]=1;
  21. return now;
  22. }
  23. tmp=nxt[pos][x];
  24. if(len[tmp]==len[pos]+1){
  25. link[now]=tmp;
  26. return now;
  27. }
  28. cln=++tot;
  29. len[cln]=len[pos]+1;
  30. for(int i=1;i<=26;i++)
  31. nxt[cln][i]=nxt[tmp][i];
  32. link[cln]=link[tmp],link[tmp]=link[now]=cln;
  33. while(pos!=0&&nxt[pos][x]==tmp)
  34. nxt[pos][x]=cln,pos=link[pos];
  35. return now;
  36. }

1.处理各种字符串信息

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

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

  1. void dfs(int x){
  2. size[x]=tag[x];
  3. for(int i=start[x];i;i=then[i]){
  4. int y=to[i];
  5. dfs(y);
  6. size[x]+=size[y];
  7. }
  8. }

(其中就是出现次数)

子串长度:某个状态的包含的所有子串的长度恰好为区间,且不重不漏。

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

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

  1. cnt[len[link[x]]+1]++,cnt[len[x]+1]--;
  2. for(i=1;i<=n;i++)
  3. cnt[i]+=cnt[i-1];

P3804 【模板】后缀自动机 (SAM):求所有出现次数不为的子串出现次数乘该子串长度的最大值,直接统计就可以了。
P5341 [TJOI2019]甲苯先生和大中锋的字符串:求恰好出现次的子串中出现次数最多的长度,在Parent Tree上求一下各种信息,恰好出现次的状态都区间修改一下长度的出现次数,可以差分一下,然后前缀和统计答案。
CF802I Fake News (hard):求所有子串的出现次数平方和,直接统计就可以了。
CF123D String:求所有子串的出现次数乘(出现次数)之和,直接统计就可以了。
SP8222 NSUBSTR - Substrings:给定长度为的串,求长度为的子串在中最大出现次数。
CF316G3 Good Substrings:直接建广义SAM暴力算就好了

2.求本质不同子串个数

方法

  1. void dfs(int x){
  2. size[x]=len[x]-len[link[x]];
  3. for(int i=start[x];i;i=then[i]){
  4. int y=to[i];
  5. dfs(y);
  6. size[x]+=size[y];
  7. }
  8. }

方法

  1. void dfs(int x){
  2. if(ans[x])
  3. return ;
  4. ans[x]=1;
  5. for(int i=1;i<=26;i++)
  6. if(nxt[x][i]){
  7. dfs(nxt[x][i]);
  8. ans[x]+=ans[nxt[x][i]];
  9. }
  10. }

方法

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

SP694 DISUBSTR - Distinct Substrings求本质不同子串个数。
SP705 SUBST1 - New Distinct :求本质不同子串个数,是上一题加强版。
P2408 不同子串个数:求本质不同子串个数。
P4070 [SDOI2016]生成魔咒:求本质不同子串个数,离散化+map优化。
SP32951 ADASTRNG - Ada and Substring:求某个字母开头本质不同子串个数。
#6071. 「2017 山东一轮集训 Day5」字符串:求多个串可空子串拼接后不同子串个数,对每个串建SAM,然后用子序列自动机把DAWG拼接起来,形成的新DAG有个美妙的性质:它上面的路径对应着所有新字符串的子串。这样,我们就只需要在上面计算路径数量就可以了。
P3181 [HAOI2016]找相同字符:求两个字符串中各取出一个子串使得这两个子串相同的方案数。直接建出一个串的SAM,跑dfs求出出现次数,然后跑个匹配就好了。
P6139 【模板】广义后缀自动机(广义 SAM):求多串本质不同子串,建广义SAM后直接算
CF653F Paper task:求本质不同括号子串数量,用一个数据结构随便维护一下,用SAM判断本质不同就好了。

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

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

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

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

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

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

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

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

循环同构:的循环同构包括(比如说,的循环同构有)。

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

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

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

P3763 [TJOI2017]DNA:直接在DAWG上跑dfs就好了。

4.最小表示法

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

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

放的是第二种做法。

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

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

5.求大子串

本质不同小子串:

  1. void getcnt(int x){
  2. if(cnt[x])
  3. return ;
  4. cnt[x]=x==1? 0:1;
  5. for(int i=1;i<=26;i++){
  6. int y=nxt[x][i];
  7. if(y==0)
  8. continue;
  9. getcnt(y);
  10. cnt[x]+=cnt[y];
  11. }
  12. }
  13. void query(int x,int k){
  14. if(k<=0)
  15. return ;
  16. for(int i=1;i<=26;i++){
  17. int y=nxt[x][i];
  18. if(y==0)
  19. continue;
  20. if(k>cnt[y]){
  21. k-=cnt[y];
  22. continue;
  23. }
  24. printf("%c",i+96);
  25. query(y,k-1);
  26. return ;
  27. }
  28. }

位置不同小子串:

  1. void getsize(int x){
  2. size[x]=tag[x];
  3. for(int i=start[x];i;i=then[i]){
  4. int y=to[i];
  5. getsize(y);
  6. size[x]+=size[y];
  7. }
  8. }
  9. void getcnt(int x){
  10. if(cnt[x])
  11. return ;
  12. cnt[x]=x==1? 0:size[x];
  13. for(int i=1;i<=26;i++){
  14. int y=nxt[x][i];
  15. if(y==0)
  16. continue;
  17. getcnt(y);
  18. cnt[x]+=cnt[y];
  19. }
  20. }
  21. void query(int x,int k){
  22. if(k<=0)
  23. return ;
  24. for(int i=1;i<=26;i++){
  25. int y=nxt[x][i];
  26. if(y==0)
  27. continue;
  28. if(k>cnt[y]){
  29. k-=cnt[y];
  30. continue;
  31. }
  32. printf("%c",i+96);
  33. query(y,k-size[y]);
  34. return ;
  35. }
  36. }

P3975 [TJOI2015]弦论:求本质不同/位置不同的小子串
SP7258 SUBLEX - Lexicographical Substring Search:求本质不同小子串

6.求公共串

我们定义代表状态是否在串中出现,这样我们每一次插入可以记录数组。然后在Parent Tree上合并一下,可以用优化到为SAM状态数,为串数),实际上也可以用线段树合并(见下面的"维护")。

  1. bitset<maxn>end[maxn];
  2. int extend(int last,int x){
  3. ......
  4. end[now][t]=1;
  5. ......
  6. }
  7. void dfs(int x){
  8. for(int i=start[x];i;i=then[i]){
  9. int y=to[i];
  10. dfs(y);
  11. end[x]|=end[y];
  12. }
  13. for(int i=1;i<=n;i++)
  14. if(end[x][i]==0)
  15. return ;
  16. ans=max(ans,len[x]);
  17. }

SP1811 LCS - Longest Common Substring:求两串公共串,也可以用第二个串在第一个串上跑匹配。
SP1812 LCS2 - Longest Common Substring II:求多串公共串
SP10570 LONGCS - Longest Common Substring:多组数据,求多串公共串
P5546 [POI2000]公共串:求多串公共串
P2463 [SDOI2008]Sandy的卡片:求所有串的相同段(其中相同段的定义为这一段统一加上某个数就会变成另一端),差分一下就是求公共串的裸题

7.求后缀的/前缀的

,即,最长公共前缀;,即,最长公共后缀。

有一个性质,反串SAM的Parent Tree是正串的后缀树,然后我们就可以利用SAM建后缀树了。

  1. 把串反过来建SAM不需要看了吧QAQ

然后还有一个性质,两个串的是它们在后缀树上对应的结点的(很显然,感性理解一下就好了),这样我们就可以用SAM求一些神奇的操作了。

  1. LCA难道要看?

有时候会和虚树dp搭配在一起,SAM(细节少,不直观)+虚树dp(细节多,直观)=SAM上虚树dp(细节多,不直观)QAQ。

有时候会被卡,需要用ST表优化到

P4248 [AHOI2013]差异:求后缀的lcp,SAM辅助建后缀树,也可以考虑式子的意义,然后用点分治的思想给路径计数。
P1117 [NOI2016]优秀的拆分:SAM+部分结论+插点统计贡献(调和级数)
CF1073G Yet Another LCP Problem:SAM上虚树dp求
SP687 REPEATS - Repeats:论文题。

8.维护

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

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

CF1037H Security:给定组询问,每组询问给定,求字典序最小的满足的子串且的字典序大于(即求的前缀),我们可以用线段树合并维护出等价类,然后寻找替代每个字符的最小代价。具体地,我们边执行子串匹配(注意,这里的匹配不可以丢弃前面的字符,如果无法匹配下去就直接退出匹配),边从后面的字符到字符不断找,如果后面接上这个字符还在存在在限定的区间内(其中为已匹配的长度),那么这个字符是最小的能代替当前字符的字符。直到我们匹配完整个串,我们就贪心从后往前扫,扫到第一个可以替代的字符进行替代,然后输出。(有一个细节,匹配的长度需要到的长度,比如说,此时答案为
P4094 [HEOI2016/TJOI2016]字符串:SAM+线段树合并+二分答案
CF666E Forensic Examination,广义SAM+线段树合并+子串匹配+树上倍增
P4770 [NOI2018] 你的名字:SAM+线段树合并+子串匹配
P5161 WD与数列:SAM+线段树合并+分类讨论

参考文献

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注