后缀数组SA

\(sa[i]\)与\(rk[i]\)

  • \(sa[i]\) 表示排名为 \(i\) 的后缀是哪一个(在原串中开头位置)。
  • \(rk[i]\)(或\(rank[i]\))表示开头位置是 \(i\) 的后缀的排名。

两者是互相映射关系,即 \(sa[rk[i]] = i\)。

后缀排序(倍增)

假设我们求出了只考虑长度为\(w\)的每一个后缀的前缀的 \(sa\) 和 \(rk\),怎么求考虑长度为 \(2w\) 的每一个后缀的前缀的\(sa\)和\(rk\) .

对于两个后缀 \(i\) 和\(j\), 由于我们求出了在 \(w\) 下的 $sa $ 和 \(rk\),实际上可以通过比较两个二元组 \((rk_i,rk_{i+w}),(rk_j, rk_{j+w})\)来确定大小关系,这里定义一个后缀\(i\)的两维度:第一维字符串\([i...i+w-1]\),第二维字符串\([i+w,i+2w-1]\)。

为了方便实现以及减小常数,我们开一个辅助数组\(tmp[i]\)表示上一轮排序(长度\(w\)),排名为 \(i\) 的后缀的长度为\(w\)的前缀对应的是现在的哪一个后缀的第二维,\(tmp\)可以由\(w\)下的\(sa\)求得。

  1. cnt = 0;
  2. for (int i = n - w + 1; i <= n; ++ i) tmp[++cnt] = i;
  3. for (int i = 1; i <= n; ++ i) if (sa[i] > w) tmp[++cnt] = sa[i] - w;

那么基数排序时先把所有 \(rk\) 放进桶里,然后用\(tmp\)数组从大到小在它对应的后缀的排名的桶里给\(sa\)求出新排完序的位置(有点拗口,可能讲的也不是很清楚,看代码)。

  1. inline void Rsort()
  2. {
  3. fill(buc, buc + 1 + M, 0);
  4. for (int i = 1; i <= n; ++ i) buc[rk[i]]++;
  5. for (int i = 1; i <= M; ++ i) buc[i] += buc[i - 1];
  6. for (int i = n; i >= 1; -- i) sa[buc[rk[tmp[i]]]--] = tmp[i];//好好体会下这句话(其实是我说不出来)
  7. }

贴完整代码:(注意那个 \(swap\) 操作)

  1. M = 76;//字符集
  2. for (int i = 1; i <= n; ++ i)
  3. rk[i] = str[i - 1] - '0', tmp[i] = i;
  4. Rsort();
  5. for (int w = 1, cnt = 0; cnt < n; w <<= 1, M = cnt)
  6. {
  7. cnt = 0;
  8. for (int i = n - w + 1; i <= n; ++ i) tmp[++cnt] = i;
  9. for (int i = 1; i <= n; ++ i) if (sa[i] > w) tmp[++cnt] = sa[i] - w;
  10. Rsort(), swap(rk, tmp); rk[sa[1]] = cnt = 1;
  11. for (int i = 2; i <= n; ++ i)
  12. rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + w] == tmp[sa[i - 1] + w]) ? cnt : ++cnt;//swap后tmp就是原来的rk数组
  13. }

Height数组

\(height[i]\) 表示 \(lcp(sa[i], sa[i - 1])\) ,即排名为 \(i\) 的后缀和排名 \(i-1\) 的后缀的最长公共前缀。

\(H[i]\) 表示 \(height[rk[i]]\) ,即后缀\(i\)和排在它前面一位的最长公共前缀。

性质:\(H[i]\geq H[i - 1] - 1\)

证明:设 \(i-1\) 号后缀和 \(k\) 号后缀在排序中是相邻的(\(rk[i - 1] = rk[k] + 1\)),那么 \(H[i - 1] = height[rk[i-1]]\),那么 \(i\) 号后缀会和 \(k-1\) 号后缀有长度为 \(H[i - 1] - 1\) 的公共前缀,所以此时必然有 \(H[i]\geq H[i - 1] - 1\) 。

求 \(Height\) 数组:

  1. void getHt()
  2. {
  3. int len = 0;
  4. For (i, 1, n)
  5. {
  6. if (len) len --;
  7. int j = sa[rk[i] - 1];
  8. while (str[j + len - 1] = str[i + len - 1]) len ++;
  9. ht[rk[i]] = len;
  10. }
  11. }

经典应用

1. 求 \(lcp(x, y)\)
\(lcp(x, y) = min(height[rk[x] + 1],\cdots ,height[rk[y]])\)默认 \(x\) 排名小于 \(y\) 排名,用 \(rmq\) 维护。
2.本质不同的子串数量

排名为 \(i\) 的后缀对答案的贡献为 \(len(i) - height[i]\)。

后缀自动机(SAM)

本文只是为了方便复习,而不适合用来学习SAM。

学习的话推荐吴作同的课件及oi-wiki上的教程

SAM中的每一个状态(点)对应的是一个endpos集合,且互不相同,否则两个状态可以合并

后文可能会说到点的endpos,代表的意义就是这个点接受子串的endpos,之后不再区分。

SAM中每一个状态接受的子串长度是连续的,比如:ba,bba,cbba,结合 endpos 的定义理解下

maxlen 与 minlen

一个状态的maxlen为它接受的最长的子串,如一个点接受的串为 ba, bba, cbba, 那么它的maxlen就是4。

minlen同理。

后缀链接link

两个状态 \(fa\) 和 \(u\) ,若有

  • \(endpos(u)\subseteq endpos(fa)\)

  • \(endpos(fa)\) 的大小是满足上面条件下最小的那一个

那么 \(u\) 的后缀链接为 \(fa\) 。

对于一个点显然只有一个 \(link\) ,所以这个结构是一颗以 \(t_0\) 为根的树,称它为 \(parent\) 树。

由 \(endpos\) 及 \(link\) 的定义,\(minlen(u) = maxlen(link(u)) + 1\) 。

所以想象一下,每个状态接受的串末尾对齐是一个梯形的形状(他们的endpos是一样的),如

  1. bacas
  2. bbacas
  3. cbbacas

而它把它和它所以祖先接受的串按从上到下的顺序拼接,会是一个完美的等腰直角三角形,如

\(link(u)\) 是这样:

  1. cas
  2. acas

\(u\) 是这样:

  1. bacas
  2. bbacas
  3. cbbacas

把u和它所有祖先的串拼起来:

  1. s
  2. as
  3. ---分界
  4. cas
  5. acas
  6. -----分界
  7. bacas
  8. bbacas
  9. cbbacas

体会到一些东西了吧,parent树还有一个性质:它是 \(s\) 的反串的后缀树!

一个点到根的parent树上的状态的转移边是有单调性的,即一个存在一个节点它所有祖先都有 \(c\) 这条边,而它下面的点都没有这条边,由于一个点的endpos集合是被其父亲包含的,所以不难证明这一点。

构造 \(\&\) 实现:

看吴作同的课件,自己手动模拟下。

变量声明:

  1. int Ncnt, last;//Ncnt 节点数(不包括根节点0),last 插入上一个字符接受所有后缀的节点。
  2. struct Status
  3. {
  4. int len, link;
  5. //link 后缀链接
  6. //len 该状态接受的最长串长度,即 maxlen,这里不记minlen是因为可以由link的maxlen推得。
  7. int ch[26];//这里默认小写字母,26条转移边
  8. } st[maxN + 2];

算法流程:

我们从前往后一个个加入字符:设当前加入SAM中的 \(s\) 长 \(len\)

新建节点 \(cur\), \(st[cur].len\leftarrow st[last].len + 1\),把最长的后缀接受过来。

然后考虑在尾部加入字符 \(c\) 后 可能 会影响哪些状态,那么就是 \(endpos\) 包含了最后一位的的状态,即last的所有祖先,由于到根路径上的点的转移边是有单调性的,所以只需要考虑最下面的一段。

所以我们跳 \(last\) 的后缀链接,直到该点拥有 \(c\) 这条出边或者到根,否则向 \(cur\) 连一条 \(c\) 的转移边,每向 \(cur\) 连一条边,它接受的字符串的梯形就会变高(想象一下),\(minlen\) 就会更新为当前跳到的点的 \(minlen+1\)。

假设我们跳到 \(p\) ,跳上来的那个儿子是 \(son\) ,它通过 \(c\) 转移到 \(q\) ,有可能会出现这样的两种情况:

  • \(maxlen(q) = minlen(son)+1\) 那么把 \(cur\) 的 \(link\) 指向 \(q\) ,\(cur\) 的所有祖先和它的串就正好形成了一个等腰三角形,注意到 \(maxlen(q) = minlen(son)+1\) 和 \(maxlen(q) = maxlen(p) + 1\) 是等价的,因为 \(maxlen(p) = minlen(cur) - 1=minlen(son) - 2\)。

  • \(maxlen(q) \not= minlen(son)+1\) 这时可以理解成它不能和 \(cur\) 接受的梯形完美拼合上,所以我们把它拆成两个节点,即 \(q \rightarrow q' \&\ clone\) ,令 \(clone\) 的 \(maxlen = maxlen(p) + 1\) 让 \(cur\) 和 \(q'\) 的 \(link\) 指向 \(clone\) ,$ clone$ 的后缀链接依然是 \(q\) 的后缀链接 ,并把 \(q\) 的所以出边复制到 \(clone\) 上,再把 \(p\) 到根路径上出边 \(c\) 指向 \(q\) 的点指向 \(clone\) 。

这样就处理完增加一个字符对SAM结构的影响。

代码:

  1. namespace SAM
  2. {
  3. int Ncnt, last, size[2 * maxN + 2];
  4. struct Status
  5. {
  6. int link, len;
  7. int ch[26];
  8. } st[2 * maxN + 2];
  9. void init() { last = 0, st[0].link = -1, st[0].len = 0; }//插入第一个字符前记得初始化
  10. void insert(char ch)
  11. {
  12. int c = ch - 'a';
  13. int cur = ++Ncnt;
  14. int p = last;
  15. st[cur].len = st[last].len + 1;
  16. while (p != -1 and !st[p].ch[c])//把所有last不含c边的祖先向cur连边。
  17. {
  18. st[p].ch[c] = cur;
  19. p = st[p].link;
  20. }
  21. if (p == -1)
  22. st[cur].link = 0;
  23. else
  24. {
  25. int q = st[p].ch[c];
  26. if (st[q].len == st[p].len + 1)
  27. st[cur].link = q;
  28. else
  29. {
  30. int clone = ++Ncnt;
  31. st[clone] = st[q];
  32. st[clone].len = st[p].len + 1; //把 q 拆出来个 clone
  33. while (p != -1 and st[p].ch[c] == q) //把所有p的祖先转移到q的c边连向clone
  34. {
  35. st[p].ch[c] = clone;
  36. p = st[p].link;
  37. }
  38. st[cur].link = st[q].link = clone;//更新link
  39. }
  40. }
  41. last = cur;
  42. }
  43. }

SA & SAM的更多相关文章

  1. SA / SAM 题目集

    上一次做 SA / SAM 相关的题还要数到某场毒瘤 NOIP 模拟赛--这么久没做了都快忘光了--写点东西记录一些最近做到的水好题. LOJ2059 「TJOI / HEOI2016」字符串 题意 ...

  2. BZOJ 2946 SA/SAM

    思路: 1. 二分+后缀数组 2.SAM //By SiriusRen #include <cstdio> #include <cstring> #include <al ...

  3. BZOJ 2882: 工艺 (SA/SAM/最小表示法)

    我写的O(nlogn)O(nlogn)O(nlogn)的SA 8000ms 被 O(n)O(n)O(n)的SAM 2800ms 和 O(n)O(n)O(n)的最小表示法 500ms 头都锤爆- COD ...

  4. 【NOI2019模拟2019.6.29】字符串(SA|SAM+主席树)

    Description: 1<=n<=5e4 题解: 考虑\(f\)这个东西应该是怎样算的? 不妨建出SA,然后按height从大到小启发式合并,显然只有相邻的才可能成为最优答案.这样的只 ...

  5. SAM 做题笔记(各种技巧,持续更新,SA)

    SAM 感性瞎扯. 这里是 SAM 做题笔记. 本来是在一篇随笔里面,然后 Latex 太多加载不过来就分成了两篇. 标 * 的是推荐一做的题目. trick 是我总结的技巧. I. P3804 [模 ...

  6. SAM 感性瞎扯

    SAM 做题笔记. 这里是 SAM 感性瞎扯. 最近学了后缀自动机(Suffix_Automaton,SAM),深感其巧妙之处,故写文以记之. 部分文字与图片来源于 OI-Wiki,hihoCoder ...

  7. ExKMP(Z Algorithm) 讲解

    目录 问题引入 CaiOJ 1461 [EXKMP]最长共同前缀长度 算法讲解 匹配过程 next 的求解 复杂度证明 代码解决 一些例题 UOJ #5. [NOI2014]动物园 CF1051E V ...

  8. 牛客网暑期ACM多校训练营(第一场)I Substring

    题意:给你一个只有abc的字符串,求不相同的子串,(不同构算不同,例如aba和bab算同构) 题解:很显然,如果不考虑同构的问题,我们直接上sa/sam即可,但是这里不行,我们考虑到只有abc三种字符 ...

  9. 【[TJOI2015]弦论】

    \(SA+SAM\) 第一问显然是一个\(SAM\)的经典问题,我们排完序之后直接使用一直往下找\(n+1-sa[i]-het[i]\)就好了,找到\(K\)减不动了输出就好了 第二问是\(SAM\) ...

随机推荐

  1. docker常用命令及操作

    1).镜像操作 操作 命令 说明 检索 docker search 关 键 字 eg:docker search redis 我们经常去docker hub上检索镜像的详细信息,如镜像的TAG. 拉取 ...

  2. maven 坐标获取方式

    问题:我们在开发时pom.xml文件中的 <dependencies>     <dependency>         <groupId>org.mybatis& ...

  3. 进程队列补充、socket实现服务器并发、线程完结

    目录 1.队列补充 2.关于python并发与并行的补充 3.TCP服务端实现并发 4.GIL全局解释器锁 什么是保证线程安全呢? GIL与Lock 5.验证多线程的作用 对结论的验证: 6.死锁现象 ...

  4. JS高阶函数--------map、reduce、filter

    一.filter filter用于对数组进行过滤.它创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素. 注意: filter() 不会对空数组进行检测. 注意: filter() ...

  5. js+css--单选按钮,自定义选中的颜色???(性别按钮,男女)

    效果图: html: <div class="item"><div class="rad"></div><span c ...

  6. POJ 1161 Walls ( Floyd && 建图 )

    题意 :  在某国,城市之间建起了长城,每一条长城连接两座城市.每条长城互不相交.因此,从一个区域到另一个区域,需要经过一些城镇或者穿过一些长城.任意两个城市A和B之间最多只有一条长城,一端在A城市, ...

  7. HDU-6705 Path

    Description You have a directed weighted graph with n vertexes and m edges. The value of a path is t ...

  8. 几种Android混淆和逆向工具介绍

    针对Android面临的安全风险,一些防止逆向的方法应运而生.代码混淆在X86架构上曾经被广泛研究,他被用于保护软件的只是产权,但是恶意软件也常常使用他来对抗杀毒软件或者对抗逆向分析者,代码混淆可以给 ...

  9. ES6 Object.setPrototypeOf ()方法和defineProperty()方法的使用

    将一个指定的对象的原型设置为另一个对象或者null(既对象的[[Prototype]]内部属性). 示例: <script type="text/javascript"> ...

  10. win7 编译postgresql9.6.8

    一.环境 windows7 postgresql9.6.8 vs2010 perl5.24.3 二.编译安装 1.安装perl,安装到C:\Perl64路径下,安装完成后设置环境变量PATH和Perl ...