后缀数组

概念

实际上就是将一个字符串的所有后缀按照字典序排序

得到了两个数组 \(sa[i]\) 和 \(rk[i]\),其中 \(sa[i]\) 表示排名为 i 的后缀,\(rk[i]\) 表示后缀 i 的排名

注意到 \(rk\) 和 \(sa\) 是互逆的,即 \(sa[rk[i]]=rk[sa[i]]=i\)

先讨论几个关于 \(lcp\) 的性质,令 \(lcp(i,j)\) 表示 \(sa[i]\) 和 \(sa[j]\) 的最长公共前缀

  1. \(lcp(l,r)=min(lcp(l,i),lcp(i,r)),l\le i\le r\),注意这里只需要枚举任意一个 i 即可

    证明:

    令 \(p=min(lcp(l,i),lcp(i,r)),sa[l]=u,sa[r]=v,sa[i]=w\)

    由于 \(u\) 和 \(w\) 的前 \(p\) 位相同,\(v\) 和 \(w\) 的前 \(p\) 位相同

    所以 \(u\) 和 \(v\) 的 \(lcp\) 至少为 \(p\)

    假设 \(u\) 和 \(v\) 的 \(lcp>p\),不妨设其为 \(q=p+k\)

    我们知道 \(u[q]\neq w[q],v[q] \neq w[q]\),并且 \(w[q]\ge u[q],v[q]\ge w[q]\)

    那么 \(w[q]\) 只能是 \(>u[q]\),且 \(v[q]\) 只能是 \(>w[q]\),所以 \(u[q]\) 不可能等于 \(v[q]\)

  2. \(lcp(l,r)=min_{i=l+1}^rlcp(i,i-1)\)

    证明:

    注意到 \(lcp(l,r)=min(lcp(l,l+1),lcp(l+1,r))\)

    递归运算即可得到上式

根据这两个数组我们能得到 \(H\) 数组,\(H[i]\) 表示 \(sa[i]\) 和 \(sa[i-1 ]\) 的最长公共前缀的长度

\(H\) 有两个性质

  1. \(H[rk[i]]\ge H[rk[i-1]]-1\)

    证明:

    令 \(suf[k]\) 为排名正好在 \(suf[i-1]\) 前一个的后缀,那么它们的公共前缀的长度就是 \(H[rk[i-1]]\)

    注意到 \(su f[k+1]\) 的排名一定在 \(suf[i]\) 之前,而 \(suf[k+1]\) 和 \(suf[i]\) 的最长公共前缀至少是 \(H[rk[i-1]]-1\)

    所以 \(H[rk[i]]\ge H[rk[i-1]] - 1\)

  2. 后缀 \(x\) 和 \(y\) 的最长公共前缀为 \(\min_{i=rk[x]+1}^{rk[y]}H[i]\),实际上这就是 \(lcp\) 的第二个性质

  3. \(sa[i]\) 与 \(sa[1]\) 到 \(sa[i-1]\) 的最长公共前缀是与 \(sa[i-1]\) 的最长公共前缀

实际应用中求 \(sa\) 的方法就是倍增了 = =,具体实现原理就不写了 = =

#include<iostream>
#include<cstdio>
#include<cstring>
#define maxn 1000010
using namespace std; int n, m; char s[maxn]; int tax[maxn], tp[maxn], sa[maxn], rk[maxn], M = 122;
void rsort() { // tp[i] 表示排序时第二关键字为 i 的后缀是什么
// rsort 的实际作用就是按照 rk[i] 为第一关键字,tp[i] 为第二关键字从小到大排序
for (int i = 0; i <= M; ++i) tax[i] = 0;
for (int i = 1; i <= n; ++i) ++tax[rk[i]];
for (int i = 1; i <= M; ++i) tax[i] += tax[i - 1];
for (int i = n; i; --i) sa[tax[rk[tp[i]]]--] = tp[i];
} int c1, H[maxn]; // c1 表示不同的 rk[i] 有多少个,当 c1 == n 时,排序完毕
void SA() {
if (n == 1) return (void) (sa[1] = rk[1] = 1);
for (int i = 1; i <= n; ++i) rk[i] = s[i], tp[i] = i; rsort();
for (int k = 1; k < n; k *= 2) { // k 是已经排序完毕的长度
if (c1 == n) break; M = c1; c1 = 0; // c1 清零之后就暂时当做计数器用了
for (int i = n - k + 1; i <= n; ++i) tp[++c1] = i;
for (int i = 1; i <= n; ++i) if (sa[i] > k) tp[++c1] = sa[i] - k; // 更新 tp 数组
rsort(); // 在进行这个 rsort 之前,sa 和 rk 排序的长度依旧是 k
// rsort 结束之后,sa 得到了更新,所以下面的操作是更新 rk
swap(rk, tp); rk[sa[1]] = c1 = 1; // 这时候的 tp 变成了上一轮的 rk,c1 是计数器同时是不同的 rk[i] 的个数
for (int i = 2; i <= n; ++i) {
if (tp[sa[i - 1]] != tp[sa[i]] || tp[sa[i - 1] + k] != tp[sa[i] + k]) ++c1;
// 如果排名为 i - 1 和排名为 i 的串的前 k 位相同,并且前 2k 位也相同,那么这两个串的排名就暂时相同了
rk[sa[i]] = c1;
}
} int lcp = 0;
for (int i = 1; i <= n; ++i) {
if (lcp) --lcp;
int j = sa[rk[i] - 1];
while (s[j + lcp] == s[i + lcp]) ++lcp;
H[rk[i]] = lcp;
}
} int main() {
scanf("%s", s + 1); n = strlen(s + 1); SA();
for (int i = 1; i <= n; ++i) printf("%d ", sa[i]);
return 0;
}

应用

单个字符串

  1. 可重叠最长重复子串

    定义:对于一个串,如果它的一个子串在原串中出现出现至少两次,则称这个子串是一个重复子串

    可重叠指两次出现的位置可以重叠

    直接求 \(H\) 的最大值即可

    证明:同理,一个子串一定是某一个后缀的前缀

    所以原问题相当于求两个后缀的最长公共前缀的最大值,这个东西显然就是 \(H\) 的最大值

  2. 可重叠重复子串计数

    求有多少本质不同的子串出现至少两次

    再次重申,子串 = 某一个后缀的一个前缀

    我们考虑将所有后缀按照字典序加入,我们求没加入一个后缀对答案贡献多少

    首先他所贡献的重复的子串为 \(H[i]\),但是有一些已经被记过数了

    所以我们考虑去重

    以下开始胡扯 不知道给怎么说了

    给个答案吧 \(ans=\sum_{i=1}^nmax(H[i]-H[i-1],0)\)

    不过讲道理,这个东西 \(SAM\) 随便做

  3. 不可重叠最长重复子串

    注意到 \(H\) 这个东西是极大的,考虑二分答案 x

    那么我们考虑如果固定一个 \(sa[i]\),那么排名在 \(sa[i ]\) 前面的,且与 \(sa[i]\) 的最长公共前缀的大小至少为 \(x\),且与 \(sa[i]\) 的位置相差至少为 \(x\)(要不然就重合了

    假设这个串为 \(sa[j]\),注意到因为 \(H\) 这个东西是极大的,所以 \(sa[i]\) 与 \(sa[j+1]\) 到 \(sa[i-1]\) 这些串的最长公共前缀都至少为 \(x\),实际上 \(H[j+1]\) 到 \(H[i]\) 都至少为 \(x\)

    upd:那么反过来就是如果 \(H[j+1]\) 到 \(H[i]\) 都大于等于 \(x\),那么 \(sa[i]\) 和 \(sa[j]\) 的 \(lcp\) 至少为 \(x\)

    这启示我们将 \(H\) 按照二分的 \(x\) 分组,也就是说 \(\ge x\) 且连续的 \(H\) 分一组,如果某一组里的后缀的位置的最大值 - 最小值 \(\ge x\),则表示二分的这个值合法

  4. 可重叠的 \(k\) 次最长子串

    同样二分答案,只不过判断的是否有一个组的个数大于等于 \(k\)

    按照 3 的思路,我们发现只需要求每一个 \(H\) 的每一个 \(k-1\) 区间的最小值即可

    显然可以单调队列维护

  5. 本质不同的子串的个数

实际上就是求所有后缀有多少本质不同的前缀

我们考虑按照将所有后缀按照字典序排序,那么每次新加进来的一个后缀的前缀的个数为 \(n-sa[i]+1\),但是与前面的所有后缀重复的前缀有 \(H[i]\) 个,因为 \(H\) 是极大的

答案即为 \(\sum_{i=1}^nn-sa[i]+1-H[i]\)

  1. 重复出现次数最多的连续重复子串

    我们考虑枚举连续重复子串的长度 \(l\),然后我们将串分成 \(\lfloor\frac{n}{l}\rfloor\) 块

    求出相邻两块的 \(lcp\) 的长度 \(k\),那么出现次数为 \(k/l+1\)

    注意还要检查一下块前面的一部分是否可以加进去

    时间复杂度为 \(O(n\log n)\)

两个字符串

  1. 两个串的最长公共子串

    将两个串之间用没有出现过的字符拼接起来

    然后直接后缀排序,注意到不会有任何一个 \(H[i]\) 能够跨越未知字符

    如果 \(sa[i]\) 和 \(sa[i-1]\) 不是同一个串的后缀,那么可以拿 \(H[i]\) 来更新答案

    容易知道最大值一定是取在某个 \(sa[i]\) 和 \(sa[i-1]\)

    upd:为啥我现在不知道了

    还是随便口胡一下吧

    因为至少有一个 \(sa[i]\) 和 \(sa[i-1]\) 是分别属于不同串的后缀的

    如果只有一个的话,那么这个 \(H[i]\) 就是答案,因为 \(H\) 是极大的

    如果有多个的话,就取最大值好了

    或者换个说法如果最终答案是 \(sa[i]\) 和 \(sa[j]\) 的 \(lcp\),且 \(i\) 和 \(j\) 不相邻

    那么 \(H[j+1]\) 到 \(H[i]\) 都是至少有这么大的,中间一定有一对 \(sa[k]\) 和 \(sa[k+1]\) 不属于同一个串的后缀

  2. 长度不小于 \(k\) 的公共子串个数

题目列表:

LuoguP2408

LuoguP2852

LuoguP4051

SP1811

LuoguP4248

【字符串】后缀数组SA的更多相关文章

  1. 后缀数组(SA)总结

    后缀数组(SA)总结 这个东西鸽了好久了,今天补一下 概念 后缀数组\(SA\)是什么东西? 它是记录一个字符串每个后缀的字典序的数组 \(sa[i]\):表示排名为\(i\)的后缀是哪一个. \(r ...

  2. 后缀数组SA学习笔记

    什么是后缀数组 后缀数组\(sa[i]\)表示字符串中字典序排名为\(i\)的后缀位置 \(rk[i]\)表示字符串中第\(i\)个后缀的字典序排名 举个例子: ababa a b a b a rk: ...

  3. 后缀数组SA入门(史上最晦涩难懂的讲解)

    参考资料:victorique的博客(有一点锅无伤大雅,记得看评论区),$wzz$ 课件(快去$ftp$%%%),$oi-wiki$以及某个人的帮助(万分感谢!) 首先还是要说一句:我不知道为什么我这 ...

  4. Bzoj4556: [Tjoi2016&Heoi2016]字符串 后缀数组

    4556: [Tjoi2016&Heoi2016]字符串 Time Limit: 20 Sec  Memory Limit: 128 MBSubmit: 169  Solved: 87[Sub ...

  5. 【BZOJ 3473】 字符串 (后缀数组+RMQ+二分 | 广义SAM)

    3473: 字符串 Description 给定n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中至少k个字符串的子串? Input 第一行两个整数n,k. 接下来n行每行一个字符串 ...

  6. BZOJ 3277: 串/ BZOJ 3473: 字符串 ( 后缀数组 + RMQ + 二分 )

    CF原题(http://codeforces.com/blog/entry/4849, 204E), CF的解法是O(Nlog^2N)的..记某个字符串以第i位开头的字符串对答案的贡献f(i), 那么 ...

  7. bzoj3796(后缀数组)(SA四连)

    bzoj3796Mushroom追妹纸 题目描述 Mushroom最近看上了一个漂亮妹纸.他选择一种非常经典的手段来表达自己的心意——写情书.考虑到自己的表达能力,Mushroom决定不手写情书.他从 ...

  8. [笔记]后缀数组SA

    参考资料这次是真抄的: 1.后缀数组详解 2.后缀数组-学习笔记 3.后缀数组--处理字符串的有力工具 定义 \(SA\)排名为\(i\)的后缀的位置 \(rk\)位置为\(i\)的后缀的排名 \(t ...

  9. BZOJ3473:字符串(后缀数组,主席树,二分,ST表)

    Description 给定n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中至少k个字符串的子串? Input 第一行两个整数n,k. 接下来n行每行一个字符串. Output 一 ...

随机推荐

  1. yii框架里DetailView视图和GridView的区别

    1,首先从语义上分析 DetailView是数据视图,用于显示一条记录的数据,相当于网页中的详情页 GridView是网格视图,用于显示数据表里的所有记录,相当于网页里的列表页 2.用法上的区别 首先 ...

  2. 自己使用的jquery公用common.js

    /*解决ie8中js数组没有indexOf方法*/ jQuery.extend({ exportResport : function(url, method, params){ var paramCo ...

  3. selenium中元素操作之浏览器窗口滚动&网页日期控件操作(js操作)(五)

    js的滚动条scrollIntoView() Arguments[] - python与js之间的羁绊 1.移动到元素element对象的“底端”,与当前窗口的“底部”对齐: driver.execu ...

  4. Weak Session IDs

    工具的使用 首先github上下载火狐插件(正版收费),按F12调用 服务器生成sessionID通过response返回给浏览器,sessionID存放在浏览器cookie中,然后再通过cookie ...

  5. Commander基本使用

    随着NodeJs的不断发展,对于前端来说要做的东西也就更多,Vue脚手架React脚手架等等等一系列的东西都脱颖而出,进入到人们的视野当中,对于这些脚手架工具来讲也只是停留在应用阶段,从来没有想过脚手 ...

  6. 英语eschaunge交易所

    eschaunge  Eschaunge是一个外文单词,名词译为交易所,交易,交换,兑换(率),动词译为兑换, 交换,互换,交换,调换.是Exchange的替代形式 中文名:交易所,交易,交换 外文 ...

  7. 记录一次git回滚代码

    老大临时让更新一版代码到本地,熟练的git fetch/git merge 之后,出来了一批改动的文件,但是并不是我改动的. 我以为是版本迭代出来的其他同事改的,我就直接给add commit到我的版 ...

  8. Python学习日记(三十) Socket模块使用

    Socket(套接字) 套接字是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像文件一样的打开.读写和关闭等操作.套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信.网 ...

  9. 03-JavaScript语法介绍

    本篇主要关于原生JavaScript的介绍,其中包括其嵌入HTML页面方式,JavaScript的语法结构,以及贪吃蛇案例: 一.绪论 JavaScript是运行在浏览器端的脚步语言,JavaScri ...

  10. Redis开发与运维学习笔记

    <Redis开发与运维>读书笔记   一.初始Redis 1.Redis特性与优点 速度快.redis所有数据都存放于内存:是用C语言实现,更加贴近硬件:使用了单线程架构,避免了多线程竞争 ...