首页 > 其他分享 >后缀数组学习笔记

后缀数组学习笔记

时间:2024-04-17 17:56:47浏览次数:13  
标签:子串 后缀 笔记 height 数组 字符串 sa rk

定义

后缀数组是什么?

(下文用 \(Suf_S[i]\) 表示 \(S[i, i + 1, \cdots, |S|]\),对 \(Suf_T\) 同理。并用 \(S[l, r]\) 表示 \(S[l, l + 1, \cdots, r]\),对 \(T[l, r]\) 同理)

后缀数组包含两个数组 \(rk, sa\)。

  • \(rk[i]\) 表示后缀 \(Suf_S[i]\) 排序后的排名。
  • \(sa[i]\) 表示排名为 \(i\) 的后缀的编号。

显然有 \(rk[sa[i]] = sa[rk[i]] = i\)。

求法

倍增 \(O(n \log^2 n)\)。

对于第 \(i + 1\) 次排序,考虑求出长度为 \(2^i\) 的所有子串的排名,并将排名放入 \(rk_i\) 数组。求 \(rk_i\) 的方法是:先求出 \(rk_{i - 1}\) 数组,接着对于子串 \(S[l, l + 2^i - 1]\),将 \(rk_{i - 1}[l]\) 作为第一关键字,\(rk_{i - 1}[l + 2^{i - 1}]\) 作为第二关键字排序,排序后的编号数组即为 \(rk_i\)。

特别地,对于第 \(1\) 次排序,直接按字符排序即可。而对于 \(l + 2^i - 1 > n\) 的子串,其 \(rk_i[l]\) 视作 \(-\infty\)。

sort 可以做到 \(O(n \log^2 n)\)。code:

rep(i, 1, n) sa[i] = i, rk[i] = s[i];
for (int len = 1; len < n; len <<= 1) {
  auto cmp = [&](auto x, auto y) {return rk[x] == rk[y] ? rk[x + len] < rk[y + len] : rk[x] < rk[y];};
  sort(sa + 1, sa + 1 + n, cmp);
  int p = 0;
  rep(i, 1, n) {
    if (!cmp(sa[i], sa[i - 1]) && !cmp(sa[i - 1], sa[i])) o_rk[sa[i]] = p;
    else o_rk[sa[i]] = ++p;
  }
  copy(o_rk + 1, o_rk + 1 + n, rk + 1);
}
rep(i, 1, n) cout << sa[i] << ' ';

基数排序优化 \(O(n \log n)\)

先扯扯基数排序是什么。


基数排序

考虑比较两个字符串的大小,可以将一个字符串拆成 \(k\) 个关键字。那么对于字符串 \(a, b\),他们比较方式为:

  • 首先若 \(a\) 和 \(b\) 的长度不相同,那么在长度短的字符串后面补上若干个 \(-\infty\)。接着进入第一个关键字的比较。
  • 若 \(a_0 \ne b_0\):若 \(a_0 < b_0\) 则 \(a\) 更小,否则 \(b\) 更小。否则 \(a_0 = b_0\),进入下一个关键字的比较。
  • 若 \(a_1 \ne b_1\):若 \(a_1 < b_1\) 则 \(a\) 更小,否则 \(b\) 更小。否则 \(a_1 = b_1\),进入下一个关键字的比较。
    \(\cdots\)
  • 如果到最后都没有分出大小,则 \(a = b\)。

考虑把这种方式用在整数比较上。把整数拆成若干个关键字的集合,可以把整数的每 \(6\) 位定成一个关键字。

如果是对序列 \(a_1, a_2, \cdots, a_n\) 排序,对每个关键字桶排序即可。code:

const int M = 1E5;
copy(a + 1, a + 1 + n, b + 1);
rep(i, 1, n) ++cnt[b[i] % M];
re(i, 1, M) cnt[i] += cnt[i - 1];
per(i, n, 1) a[cnt[b[i] % M]--] = b[i];
copy(a + 1, a + 1 + n, b + 1);
fill(cnt, cnt + M, 0);
rep(i, 1, n) ++cnt[b[i] / M];
re(i, 1, M) cnt[i] += cnt[i - 1];
per(i, n, 1) a[cnt[b[i] / M]--] = b[i];

在这里基数排序可以替代 sort,因为基数排序本来就是处理多关键字排序的利器,这里恰巧是双关键字排序。

复杂度变为 \(O(n \log n)\)。code:

rep(i, 1, n) sa[i] = i, rk[i] = s[i], ++cnt[rk[i]];
int m = 127;
rep(i, 1, m) cnt[i] += cnt[i - 1];
per(i, n, 1) sa[cnt[rk[i]]--] = i;
int p = 0;
rep(i, 1, n) {
  if (rk[sa[i]] == rk[sa[i - 1]]) o_rk[sa[i]] = p;
  else o_rk[sa[i]] = ++p;
} copy(o_rk + 1, o_rk + 1 + n, rk + 1);
for (int len = 1; len < n; len <<= 1, m = n) {
  fill(cnt + 1, cnt + 1 + m, 0);
  copy(sa + 1, sa + 1 + n, id + 1);
  rep(i, 1, n) ++cnt[rk[id[i] + len]];
  rep(i, 1, m) cnt[i] += cnt[i - 1];
  per(i, n, 1) sa[cnt[rk[id[i] + len]]--] = id[i];
  copy(sa + 1, sa + 1 + n, id + 1);
  fill(cnt + 1, cnt + 1 + m, 0);
  rep(i, 1, n) ++cnt[rk[id[i]]];
  rep(i, 1, m) cnt[i] += cnt[i - 1];
  per(i, n, 1) sa[cnt[rk[id[i]]]--] = id[i];
  int p = 0;
  rep(i, 1, n) {
    if (rk[sa[i]] == rk[sa[i - 1]] && rk[sa[i] + len] == rk[sa[i - 1] + len]) o_rk[sa[i]] = p;
    else o_rk[sa[i]] = ++p;
  } copy(o_rk + 1, o_rk + 1 + n, rk + 1);
}
rep(i, 1, n) cout << sa[i] << ' ';

应用

  • P4051 字符串加密

将 \(S\) 复制一遍并将副本放在后面得到 \(SS\),对 \(SS\) 求一遍后缀数组即可。

  • 判断字符串中是否出现某一子串

假设要在 \(S\) 中多次查找是否出现某一子串 \(T\),观察到处理出来后缀数组后就有了字典序上的单调性,那么二分 \(sa\) 数组,每次比较某一后缀和 \(T\) 的大小关系即可,复杂度 \(O(|T| \log |S|)\)。

  • 「USACO07DEC」Best Cow Line

处理出来原串的后缀数组和其反串的后缀数组,每次取时比较一下两个 \(rk\) 的大小即可。

\(height\) 数组

定义

假设 \(lcp(i, j)\) 表示字符串 \(\textrm{Suf}_S[i]\) 与 \(\textrm{Suf}_S[j]\) 的最长公共前缀。

\(height\) 的定义是:\(height[i] = lcp(sa[i], sa[i - 1])\)。特别地,\(height[1] = 0\)。

求法

引理:\(height[rk[i]] \ge height[rk[i - 1]] - 1\)。

证明:当 \(height[rk[i - 1]] \le 1\) 时式子显然成立;当 \(height[rk[i - 1]] > 1\) 时:
根据定义有 \(height[rk[i - 1]] = lcp(sa[rk[i - 1]], sa[rk[i - 1] - 1]) > 1 \Rightarrow lcp(i - 1, sa[rk[i - 1] - 1]) < 1\)。
假设用 \(aA\) 来表示最长公共前缀,则 \(\textrm{Suf}_S[i - 1]\) 表示为 \(aAX\),\(\textrm{Suf}_S[sa[rk[i - 1] - 1]]\) 表示为 \(aAY\)。(\(X > Y\),\(AX > AY\),\(Y\) 可能是空串,\(X\) 不是空串)
可以得出 \(\textrm{Suf}_S[i] = AX\),\(\textrm{Suf}_S[sa[rk[i - 1] - 1] + 1] = AY\)。(\(AX > AY\))
又 \(\textrm{Suf}_S[sa[rk[i] - 1]]\) 且不存在后缀 \(Suf_S[k]\) 满足

\[\textrm{Suf}_S[sa[rk[i] - 1]] < \textrm{Suf}_S[k] < \textrm{Suf}_S[i] \]

故得到 \(AY \le \textrm{Suf}_S[sa[rk[i] - 1]] < AX\),所以 \(\textrm{Suf}_S[sa[rk[i] - 1]]\) 与 \(\textrm{Suf}_S[i]\) 至少有公共前缀 \(A\)。
即 \(height[rk[i]] = lcp(i, sa[rk[i] - 1]) \ge |A|\) 又因为 \(|A| = height[rk[i - 1]] - 1\),得到 \(height[rk[i]] \ge height[rk[i - 1]] - 1\),证毕。

根据这个结论,就可以像 KMP 那样求 \(height\) 了。因为变化量是 \(O(n)\) 的,所以复杂度也是 \(O(n)\) 的。

code:

int k = 0;
rep(i, 1, n) {
  if (k) --k;
  while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
  height[rk[i]] = k;
}

应用

根据子串是后缀的前缀这个性质,\(height\) 数组可以处理许多子串问题。

  • 两个子串的最长公共前缀

有定理 \(lcp(sa[i], sa[j]) = \min\limits_{k = i + 1}^j height[k]\)。很强的定理,但是不会证。OI-wiki 里面那篇关于后缀数组的文章格式有点抽象,所以没看。

基于这个定理,求子串 \(lcp\) 就变成了 RMQ 问题。

  • 比较一个字符串的两子串大小

假设需要比较 \(X = S[l_1, r_1]\) 和 \(Y = S[l_2, r_2]\) 的关系。

  • 若 \(lcp(l_1, l_2) \ge \min(|X|, |Y|)\),那么 \(|X| < |Y| \Leftrightarrow X < Y\)。(其实这部分也可以用哈希做)

  • 否则 \(X < Y \Leftrightarrow rk[l_1] < rk[l_2]\)。

  • 本质不同子串数量

首先子串是后缀的前缀,所以大体是枚举后缀,再看有多少个后缀的前缀重复了。

考虑按照后缀排序的顺序来计算重复的前缀数量,因为有 \(lcp(sa[i], sa[j]) = \min height[i, \cdots, j]\),所以后缀 \(\textrm{Suf}_S[sa[i]]\) 重复的前缀数是 \(height[i]\)。

故总计重复的前缀数就是 \(\sum\limits_{i = 1}^n height[i]\),答案为 \(\frac{n(n + 1)}{2} - \sum\limits_{i = 1}^n height[i]\)。

  • 是否有某字符串在文本串中至少不重叠地出现了两次

可以二分目标串的长度 \(|S|\) ,将 \(height\) 数组划分成若干个连续 \(\textrm{LCP}\) 大于等于 \(|S|\) 的段,利用 RMQ 对每个段求其中出现的数中最大和最小的下标,若这两个下标的距离满足条件,则一定有长度为 \(|S|\) 的字符串不重叠地出现了两次。

  • 最长回文子串

求一个字符串中的最长回文子串。

可以用哈希做,枚举回文中心后二分并判断反串是否等于正串即可,\(O(n \log n)\)。

这里还有一个 SA 的做法。设当前字符串为 \(S\),反串为 \(\hat{S}\),将串 \(S\) 拼在 \(\hat{S}\) 后面,并用分隔符 | 分割,记新串为 \(S'\)。对新串求后缀数组后,在新串的正串部分中枚举回文中心 \(i\),找到 \(S'\) 中反串部分该回文中心对应的点 \(j\),那么该以 \(i\) 为回文中心形成的最长回文串为 \(lcp(i, j)\),这样转化成了 RMQ 问题,复杂度 \(O(n \log n)\)。

  • 重复次数最多的连续重复子串

一个字符串为连续重复子串当前仅当存在一个字符串重复若干次后得到该串。求一个字符串中重复次数最多的连续重复子串。

这种问题的基本方法是枚举长度并处理关键点。

考虑枚举重复子串的长度 \(L\),接着每隔 \(L\) 个下标标记一个关键点,显然关键点下标是 \(1, L + 1, 2L + 1, 3L + 1, \cdots\)。可以发现重复次数等于子串里关键点数量。对于只包含 \(1\) 个的作特殊处理;考虑一个连续重复子串包含至少 \(2\) 个关键点的情况,对其中相邻的两个关键点求出其前缀的 \(lcs\)(最长公共后缀)和后缀的 \(lcp\) 并相应地计算即可。复杂度 \(O(n \log n)\)。

  • 最长公共子串

求两个字符串的最长公共子串。

这种两个字符串的方法是将一个放在另一个后面并用分隔符隔开,接着对新串用后缀数组。

假设是字符串 \(A\) 和 \(B\),并拼接出串 \(A|B\)(中间的 \(|\) 是分割符),处理出新串的 \(height\) 后,枚举相邻的两个 \(sa\) 并特判是否在同一子串中即可。

  • 不可重最长重复子串

求一个字符串中最长的重复了两遍的子串,且出现的位置不能重叠。

主要思想是对 \(height\) 的大小进行分组,这个思想也将在后面用到。

先二分答案,题目转化成:判定是否存在两个长度为 \(k\) 个不相交的相同子串。将 \(height\) 数组分组,分组后使每组相邻的两个后缀的 \(lcp \ge k\)。如,字符串为 \(aabaaaab\) 且 \(k = 2\) 时,有:

所在组编号 \(height\) 后缀 \(sa\)
\(1\) \(height[1]=0\) \(\texttt{aaaab}\) \(4\)
\(1\) \(height[2]=3\) \(\texttt{aaab}\) \(5\)
\(1\) \(height[3]=2\) \(\texttt{aab}\) \(6\)
\(1\) \(height[4]=3\) \(\texttt{aabaaaab}\) \(1\)
\(2\) \(height[5]=1\) \(\texttt{ab}\) \(7\)
\(2\) \(height[6]=2\) \(\texttt{abaaaab}\) \(2\)
\(3\) \(height[7]=0\) \(\texttt{b}\) \(8\)
\(4\) \(height[8]=1\) \(\texttt{baaaab}\) \(3\)

这样分组后,会有几个性质:

  1. 每组的字符串两两之间的 \(\textrm{LCP}\) 长度 \(\ge k\)。
  2. 对于同一组,字符串两两之间的 \(\textrm{LCP}\) 的长度为 \(k\) 的前缀都相同。
  3. 对于两个不同的组,字符串的 \(\textrm{LCP}\) 对应的长度为 \(k\) 的前缀不同。

根据这些性质,对于某组,只需判定是否存在两个在这组的不相交的子串即可。

  • 出现在至少 \(k\) 个字符串中的最长子串

有 \(n\) 个字符串,查找出现在至少 \(k\) 个字符串中的最长子串。

结合 最长公共子串不可重最长重复子串 的方法,先将这 \(n\) 个字符串连接起来,用分隔符分开,并求出其后缀数组与 \(height\)。再二分答案,将 \(height\) 分组,接着判断每组是否能拆成 \(k\) 个即可。复杂度 \(O(n\log n)\)。

标签:子串,后缀,笔记,height,数组,字符串,sa,rk
From: https://www.cnblogs.com/CTHOOH/p/18133046

相关文章

  • OP-TEE学习笔记(一)
    OP-TEE由于工作原因接触过一些使用TEE代替HSM来实现密钥存储、密钥对生成的方案,因此想了解一下开源的TEE方案与TEE供应商提供的方案有什么不同。关于OP-TEEOP-TEE是设计为与在Arm上运行的非安全Linux内核配合使用的一种受信任执行环境(TEE),Cortex-A内核使用TrustZone技术。OP......
  • JVM学习笔记
    1.运行时数据区1.2运行时数据区及线程概述JVM将内存划分为俩种类型的数据区域线程共享:JVM启动时创建,退出时才销毁线程私有:线程创建时创建,线程退出时销毁1.2.1运行时数据区JVM内存布局规定了Java运行过程中内存申请,分配,管理的策略,保证高效运行。不同JVM在内存划分和管理......
  • ES6中数组的高级用法
    1.箭头函数和数组方法的结合:使用箭头函数结合数组方法可以简化代码:constnumbers=[1,2,3,4,5];//使用箭头函数的map方法constdoubled=numbers.map((num)=>num*2);console.log(doubled);//输出:[2,4,6,8,10]2.解构赋值和数组方法的结合:constpoi......
  • C# 闲来笔记
    1.Debug.WriteLine、Trace.WriteLine//WriteLineWritePrintDebug.WriteLine("debug调试模式下--在跟踪窗口--输出指定内容:",message);//Trace.WriteTrace.WriteLine("debugreplease模式--在跟踪窗口--下输出指定内容",message);Debug.write()用于调试模式下在输出窗口,输出调......
  • mysql_笔记
    MySQL安装与连接安装MySQL官网下载MySQL选择社区免费版下载安装选择.msi安装包双击安装,安装过程可以无脑下一步MySQL启动/关闭开始菜单搜索cmd,找到命令提示符,然后使用管理员身份打开输入命令开启:netstartmysql80关闭:netstopmysql80注:命令中的mysql80取决于......
  • 【笔记】RedmiBookPro15锐龙板(7840hs)安装ubuntu2204注意事项
    /** 2024-04-17 12:53:52*/1、不要安装ubuntu2004,驱动问题很烦入,尤其是AMD的显卡驱动,不论哪个版本都不要打AMD的官方驱动,经常花屏,卡的完全不能操作,自带的开源驱动就行了,偶尔出现一两道花屏的,不影响使用,而且一会就消失了。如果经常出现在bios里调大显存试试,默认512估计不够,我......
  • 点分树(动态点分治)学习笔记
    1.定义在点分治的基础上加以变化,构造一颗支持快速修改的重构树,称之为点分树2.算法2.1.思路点分治的核心在于通过树的重心来划分联通块,减少合并层数,从而降低时间复杂度所以,我们可以按分治递归的顺序提出一颗树,易知树高至多为logn具体的说,对于每一个找到的重心,将上一次分治......
  • 「笔记」树同构
    目录写在前面树同构定义有根树同构无根树同构树哈希有根树无根树AHU算法例题UOJ#763.树哈希SP7826-TREEISO-TreeIsomorphismP5043【模板】树同构([BJOI2015]树的同构)写在最后写在前面vp的时候用到了于是来学一下。好水。抱歉了AHU,但是树哈希它实在是太好写了。树同......
  • ROS2笔记1--简介及开发环境搭建
    一、ROS2简介1.1、ROS2概述ROS2是第二代的RobotOperatingSystem,ROS1的升级版本,解决了ROS1存在的一些问题。与ROS1相比,Linux版本与ROS2版本的选择也有关系,对应关系如下:ROS2版本Ubuntu版本FoxyUbuntu20.04GalacticUbuntu20.04HumbleUbuntu......
  • JS 移除对象数组中,属性值全为空的项
    constarray=[{id:1,name:'John',age:25},{id:2,name:'Alice',age:null},{id:3,name:'Bob',age:undefined},{id:4,name:'Eve',age:''},{id:5,name:'',age:......