后缀数组【jiangly模板】

news/2024/11/13 11:10:51/文章来源:https://www.cnblogs.com/xiwen-ydys/p/18299719

目录
  • 后缀数组简介
  • 后缀数组可以用于什么场景
  • 如何实现后缀数组
    • 倍增法求后缀数组
  • \(height\) 数组
    • \(LCP\) (最长公共前缀)
    • \(height\)
  • 代码模板
  • 参考文章

后缀数组是一种非常强大的一种处理字符串问题的工具

后缀数组简介

前置知识:计数排序、基数排序

后缀数组(Suffix Array)主要关系到两个数组:sark

  • \(sa[i]\) :表示将所有后缀排序后\(i\) 小的后缀的编号 (也就是后缀数组);

    • 编号也就是其实下标,例如\(a\ b\ c\ d\ e\)中,\(cde\)的编号即是\(2\)(开始下标)
  • $ rk[i] $ :表示编号为 \(i\) 的后缀的排名,是重要的辅助数组;

此外还有一个 \(lc\) 数组

  • \(lc[i]\)\(lc[i]\) 其实是 \(height[i]\ =\ lcp(i, j)\),即第 \(i\) 名后缀与第 \(i - 1\) 名的后缀的最长公共前缀
    • 后续会介绍它

后缀数组可以用于什么场景

单纯的后缀数组 \(sa\)

  1. 寻找最小的循环移动位置
  2. 在字符串中寻找子串
  3. 给你一个字符串,每次从首或尾取一个字符组成字符串,问所有能够组成的字符串中字典序最小的一个

最长公共前缀 \(height\)

  1. 两子串最长公共前缀
  2. 比较一个字符串两个子串的大小关系
  3. 不同子串的数目
  4. 出现至少 k 次的子串的最大长度
  5. 是否有某字符串在文本串中至少不重叠地出现了两次
  6. 连续的若干相同子串

如何实现后缀数组

实现后缀数组有两种方法:

  1. 倍增法 \(O(nlogn)\)
  2. DC3法 \(O(n)\)

因为优化后的倍增法常数小,时间复杂度 \(O(nlogn)\) 也是完全够用的,以后有时间会写一下DC3法

倍增法求后缀数组

如果按照暴力的解法,就是将每个后缀丢进数组中直接sort即可,但是太慢了;

我们可以通过倍增的思想,使用基数排序(多关键字排序)

如果按照原生基数排序:

// 对第二关键字:id[i] + w进行计数排序
memset(cnt, 0, sizeof(cnt));
memcpy(id + 1, sa + 1, n * sizeof(int));  // id保存一份儿sa的拷贝,实质上就相当于oldsa
for (i = 1; i <= n; ++i) ++cnt[rk[id[i] + w]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[id[i] + w]]--] = id[i];// 对第一关键字:id[i]进行计数排序
// 当第一关键字的值相同时,原先排在后面的数还排在后面
memset(cnt, 0, sizeof(cnt));
memcpy(id + 1, sa + 1, n * sizeof(int));
for (i = 1; i <= n; ++i) ++cnt[rk[id[i]]];
for (i = 1; i <= m; ++i) cnt[i] += cnt[i - 1];
for (i = n; i >= 1; --i) sa[cnt[rk[id[i]]]--] = id[i];

这样常数很大

思考一下第二关键字排序的实质,其实就是把超出字符串范围(即\(sa[i]\ +\ w\ >\ n\))的 \(sa[i]\) 放到 \(sa\) 数组头部,剩下的依照原顺序放入,没必要计数排序一次。

//tmp存第二关键字序
for (int i = 0; i < k; ++i)tmp.push_back(n - k + i);
for (auto i : sa)if (i >= k)tmp.push_back(i - k);

然后依据 \(tmp\) 对第一关键字计数排序就可以了

std::fill(cnt.begin(), cnt.end(), 0);
for (int i = 0; i < n; ++i)++cnt[rk[i]];
for (int i = 1; i < n; ++i)cnt[i] += cnt[i - 1];
for (int i = n - 1; i >= 0; --i)sa[--cnt[rk[tmp[i]]]] = tmp[i];

然后依据计算出的 \(sa\) 数组,求出对应的 \(rk\) 即可

直接从前到后遍历 \(sa\)

  • \(sa[i]\) 从前的排名就比 \(sa[i\ -\ 1]\) 大,现在加上第二关键字亦然
  • \(sa[i\ -\ 1]\ +\ k\ ==\ n\),说明 \(sa[i]\) 不可能和 \(sa[i - 1]\) 一样大
  • \(sa[i]\) 的第二关键字比 \(sa[i\ -\ 1]\) 的第二关键字大

以上情况都是 \(sa[i]\)\(sa[i\ -\ 1]\) 大的情况,这种时候 \(sa[i]\)rank 必然比 \(sa[i-1]\)rank 多一

//将原来的rk放入tmp中
std::swap(rk, tmp);
rk[sa[0]] = 0;
for (int i = 1; i < n; ++i)rk[sa[i]] = rk[sa[i - 1]] + (tmp[sa[i - 1]] < tmp[sa[i]] || sa[i - 1] + k == n || tmp[sa[i - 1] + k] < tmp[sa[i] + k]);

如果 \(rk[n\ -\ 1]\ >=\ n\ -\ 1\) 了,则说明可以不用再继续排序了

因为这种时候,已经没有两个字典序的一样的串了,就没有必要再继续下去了

参照代码讲解一下:

struct SuffixArray {int n;//sa[i]:排名第i的后缀的开始下标//rk[i]:开始下标为i的后缀的排名//lc[i]:height数组std::vector<int> sa, rk, lc;SuffixArray(const std::string &s) {n = s.length();sa.resize(n);lc.resize(n - 1);rk.resize(n);//从0开始递增赋值std::iota(sa.begin(), sa.end(), 0);//长度为1的情况,直接根据字符串大小对sa排序std::sort(sa.begin(), sa.end(), [&](int a, int b) {return s[a] < s[b];});//由sa算出rk数组rk[sa[0]] = 0;for (int i = 1; i < n; ++i)//如果sa[i]!=sa[i-1],sa[i]必然比sa[i-1]大,//这时候排名要比前一个多1rk[sa[i]] = rk[sa[i - 1]] + (s[sa[i]] != s[sa[i - 1]]);int k = 1;std::vector<int> tmp, cnt(n);tmp.reserve(n);//当rk[sa[n-1]]==n-1的时候代表排序完成//或者当前长度已经全都不相同了,就没必要继续了while (rk[sa[n - 1]] < n - 1) {tmp.clear();//优化了对第二关键字的计数排序,tmp[i]记录的是排名为i的[[长度为2k的后缀]的第二关键字起始位置]for (int i = 0; i < k; ++i)tmp.push_back(n - k + i);for (auto i : sa)if (i >= k)tmp.push_back(i - k);std::fill(cnt.begin(), cnt.end(), 0);//对第一关键字计数排序,tmp靠后的在sa中也靠后for (int i = 0; i < n; ++i)++cnt[rk[i]];for (int i = 1; i < n; ++i)cnt[i] += cnt[i - 1];for (int i = n - 1; i >= 0; --i)sa[--cnt[rk[tmp[i]]]] = tmp[i];std::swap(rk, tmp);//根据sa求出rk,上面详解过这里rk[sa[0]] = 0;for (int i = 1; i < n; ++i)rk[sa[i]] = rk[sa[i - 1]] + (tmp[sa[i - 1]] < tmp[sa[i]] || sa[i - 
1] + k == n || tmp[sa[i - 1] + k] < tmp[sa[i] + k]);//倍增k *= 2;}//这里求height数组for (int i = 0, j = 0; i < n; ++i) {if (rk[i] == 0) {j = 0;} else {for (j -= j > 0; i + j < n && sa[rk[i] - 1] + j < n && s[i + j] == 
s[sa[rk[i] - 1] + j]; )++j;lc[rk[i] - 1] = j;}}}
};

\(height\) 数组

\(LCP\) (最长公共前缀)

\(lcp(x,\ y)\) :字符串 \(x\) 与字符串 y 的最长公共前缀,在这里指的是 x 号后缀与 y 号后缀的最长公共前缀

\(height\)

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

//这里求height数组
for (int i = 0, j = 0; i < n; ++i) {if (rk[i] == 0) {j = 0;} else {//就判断sa[i]开始和sa[rk[i] - 1]开始的最长公共前缀能到哪for (j -= j > 0; i + j < n && sa[rk[i] - 1] + j < n && s[i + j] == 
s[sa[rk[i] - 1] + j]; )++j;//lc即是heightlc[rk[i] - 1] = j;}
}

代码模板

struct SuffixArray {int n;std::vector<int> sa, rk, lc;SuffixArray(const std::string &s) {n = s.length();sa.resize(n);lc.resize(n - 1);rk.resize(n);std::iota(sa.begin(), sa.end(), 0);std::sort(sa.begin(), sa.end(), [&](int a, int b) {return s[a] < s[b];});rk[sa[0]] = 0;for (int i = 1; i < n; ++i)rk[sa[i]] = rk[sa[i - 1]] + (s[sa[i]] != s[sa[i - 1]]);int k = 1;std::vector<int> tmp, cnt(n);tmp.reserve(n);while (rk[sa[n - 1]] < n - 1) {tmp.clear();for (int i = 0; i < k; ++i)tmp.push_back(n - k + i);for (auto i : sa)if (i >= k)tmp.push_back(i - k);std::fill(cnt.begin(), cnt.end(), 0);for (int i = 0; i < n; ++i)++cnt[rk[i]];for (int i = 1; i < n; ++i)cnt[i] += cnt[i - 1];for (int i = n - 1; i >= 0; --i)sa[--cnt[rk[tmp[i]]]] = tmp[i];std::swap(rk, tmp);rk[sa[0]] = 0;for (int i = 1; i < n; ++i)rk[sa[i]] = rk[sa[i - 1]] + (tmp[sa[i - 1]] < tmp[sa[i]] || sa[i - 
1] + k == n || tmp[sa[i - 1] + k] < tmp[sa[i] + k]);k *= 2;}for (int i = 0, j = 0; i < n; ++i) {if (rk[i] == 0) {j = 0;} else {for (j -= j > 0; i + j < n && sa[rk[i] - 1] + j < n && s[i + j] == 
s[sa[rk[i] - 1] + j]; )++j;lc[rk[i] - 1] = j;}}}
};

参考文章

后缀数组简介 - OI Wiki (oi-wiki.org)

~(o°ω°o) (cnblogs.com)

模板来自【jiangly】算法模板,群里有

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/742977.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

P2120 [ZJOI2007] 仓库建设

在解决形如 $f_i=\max(f_j+x)$,$x$ 为一些式子的时候可以考虑使用斜率优化,将 $x$ 转成函数然后求这些能取到的函数在 $x$ 上的最高或最低点,从而得到优化。题目大意 \(n\) 个工厂,每个工厂有 \(p_i\) 的货物,货物运输一个单位距离的费用是 \(1\),工厂可以建造仓库,费用…

如何对Linux系统进行基准测试5工具UnixBench

UnixBenchUnixBench是一款跨平台基准测试工具,用于评估各种类Unix系统(包括Linux、BSD和macOS)的系统性能。它提供了一套全面的测试套件,可评估系统性能的不同方面,包括:系统调用: 此测试衡量进行系统调用的开销,系统调用是应用程序与操作系统内核交互的主要方式。 文件…

解决vscode项目中无法识别宏定义的问题

在c_cpp_properties.json中的"defines":[]中定义的宏无法被识别。 从而导致代码中的宏开关无法生效,造成代码的阅读不便利。 排查路线是: 关闭所有插件,删除当前工程目录下的.vscode文件夹。 经过一系列排查发现是C/C++插件与clangd插件有冲突, 解决方法很简单,…

vs使用AnkhSVN冲突

he problem seems to be the (as the error hints) the propertybag within the solution file问题似乎是解决方案文件中的 propertybag(如错误提示的那样) So a work around of this issue:因此,此问题的解决方法:You have a "solution.sln" created with VS 20…

0175-GDB 调试 multiboot 启动

环境Time 2022-11-12 WSL-Ubuntu 22.04 QEMU 6.2.0 NASM 2.15.05前言 说明 参考:https://os.phil-opp.com/multiboot-kernel/ 目标 使用编写好的内核可执行文件,直接从 QEMU 启动,启动时暂停 CPU,使用 GDB 调试。 汇编代码 section .multiboot_header header_start:dd 0x1B…

0172-执行内核文件

环境Time 2022-11-11 WSL-Ubuntu 22.04 QEMU 6.2.0 NASM 2.15.05前言 说明 参考:https://os.phil-opp.com/multiboot-kernel/ 目标 将编写好的内核可执行文件,制作成 ISO 镜像,然后从 QEMU 启动。 建立文件目录 isofiles └── boot├── grub│ └── grub.cfg└── …

TDA4VM-SK配置与应用杂谈

TDA4VM-SK配置与应用杂谈硬件信息:SK-TDA4VM 用户指南处理器SDK Linux边缘AI文档配置文档:SK-TDA4VM处理器SDK Linux文档-getting_started,详细说明了如何配置,下面是简要步骤:物料准备: SK板,microUSB串口线,USB camera,HDMI/DP显示器,≥16GB的内存卡,网线和局域网…

(技术贴)英雄无敌1——永久记住所学魔法——手把手教你怎么修改

喜欢英雄无敌1的朋友都知道,在1中英雄学习魔法只是记忆了该魔法的使用次数,一旦次数使用完,该魔法就会从魔法书中消失!而要想再次使用该魔法,就得去具有该魔法的魔法公会再次学习一次,自然这很不符合魔法学习的实际情况。我这里将其修改为,学过的魔法永远记住!但是学过…

连接mysql时提示is not allowed to connect不允许连接

1. 进入mysql服务器,mysql -u root -p 登录myql服务,SELECT user, host FROM mysql.user;看到root只允许localhost本机连接 2. 执行use mysql; update user set host = % where user = root; 再次查看user表,root允许所有ip连接 3.刷新缓存FLUSH PRIVILEGES; 以上仅供参考…

LCA 补充

LCA 之前学废了,回来补。 倍增版 首先是最常见的倍增版子,思路好理解,按倍增记录 \(father\),然后同时往上跳。 注意最后跳到的是那个 \(x \ne y\) 的,也就是 \(lca\) 的儿子,所以最后要返回父亲。 #include<bits/stdc++.h> using namespace std; const int N = 5e…

读人工智能全传11人工智能会出什么错

读人工智能全传11人工智能会出什么错1. 人工智能会出什么错 1.1. 一些报道是公正合理的,不过坦白地说,大部分报道都愚蠢得无可救药 1.2. 一些报道颇有知识性和引导性,而大部分则是杞人忧天式的恐吓 1.3. 滑稽的报道迎合了大众对人工智能的“终结者式恐惧” 1.3.1. 我们创造出…

dotnet 理解 X11 的 24 位或 32 位色深窗口

本文记录在 X11 里面的窗口与颜色的位色深关系本文属于学习 CPF 框架博客,感谢小红帽的 CPF 框架。更多关于 CPF 框架,请参阅 https://gitee.com/csharpui/CPF 本文这里的 24 色或 32 色表示的是用多少个 bit 表示一个像素的颜色。比如常见的 24 色就是 RGB 三个颜色分量,一…