后缀数组小结

前言 :Orz ShichengXiao 冬令营的时候就早解决了

字符串算法还是不能随意放弃啊 要认真学了!!

这个算法常用于解决字符串上的 LCP 问题 和 一些字符串匹配的问题

这个算法思维难度不是很大 但是代码难度还是有一些的

想学好这个算法 一定要牢牢的记住各个数组的含义 不然容易弄混

原理介绍

  • 还是先简单介绍一下原理吧 :

    后缀数组就是将一个字符串的后缀全部进行排序 然后把下标存入一些数组里

    用那些数组来进行字符串的一些常用操作

    为了后缀排序 我们常常使用 O(nlogn) 的倍增算法

    (而不用 O(n)DC3 因为它常数和空间大,并且十分不好写)

倍增算法

那接下来介绍一下倍增算法qwq

考虑这样一个小问题 我们比较任意两后缀的字典序大小 有没有什么快速比较的方法?

当然有 就是预处理出他们的一个前缀和后缀的大小关系 然后我们就能用另外两个来比较了。

倍增的思路大概就是如此 我们从小到大 每次长度乘二 排序长度是那些的后缀 然后用之前得到的信息去比较就行了。

具体就是变成双关键字排序,第一关键字就是前半部分的排名,第二关键字就是后半部分的排名。

基数排序

这个东西套上基数排序就能优化一个 log 。(基数排序具体见网上讲解吧qwq)

基数排序:我理解的就是 类似于桶排 我们开个桶来标记一下他们出现的次数

然后几个前缀和 那么这个数组的值就是他的排名 (可以模拟一下)

然后要满足双关键字 那么我们如果有几个元素第一关键字相同 它们在同一个下标上

那么我们就使第二关键字较大的先获得排名 其他的排名就比他小了。

具体实现见程序中的 Radix_Sort 就行了。

然后大概原理就是这样咯qwq (没讲清的话。。请去看看 刘汝佳的《算法竞赛入门经典》

数组含义

然后我介绍一下等下要出没的数组含义(很重要!!)

  1. str[i] 表示 i 这个位置上的字符;
  2. sa[i] 表示 后缀排序后第 i 名后缀的起始点下标;
  3. rk[i] 表示 i 为起始点下标的后缀的排名(在 swap 后就重新构建就行了);
  4. tmp[i] 表示以 i 为第二关键字排名的数位置(在 swap 后作为 上一次第一关键字起始点下标为 i 后缀排名);
  5. c[i] 表示在基数排序中在字符集中编号为 i 的出现次数(后面作为前缀和);
  6. 还有两个变量 : n 为字符串长度 m 为字符集大小

然后这样就可以直接做后缀排序了

代码解释

代码中有详细解释 可以看看

int sa[N], tmp[N], c[N], rk[N], m, n; char str[N]; inline void Radix_Sort() { For (i, 1, m) c[i] = 0; //清空 For (i, 1, n) ++ c[rk[i]]; //先标记一下 此时 rk 是第一关键字 For (i, 1, m) c[i] += c[i - 1]; //记前缀和 Fordown(i, n, 1) sa[c[rk[tmp[i]]] --] = tmp[i]; //得到当前的 sa 数组,第二关键字 tmp 大的先得到更大的排名 } inline void Build_Sa() { For (i, 1, n) rk[i] = str[i], tmp[i] = i; m = 255; Radix_Sort(); //简单的初始化 for (register int k = 1, p; k <= n; k <<= 1) { //开始倍增 p = 0; For (i, n - k + 1, n) tmp[++ p] = i; //后 k 个的第二关键字为空 所以是最大的 For (i, 1, n) if (sa[i] > k) tmp[++ p] = sa[i] - k; //前面的话 只有 sa > k 的 sa 作为它前第 k 个第二关键字 Radix_Sort(); swap(rk, tmp); //基数排序,然后交换两个关键字 rk[sa[1]] = 1, m = 1; //初始化 For (i, 2, n) rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? m : ++ m; //重新得到 rk 双关键字相同的 rk 一样 if (m >= n) return ; //如果当前排名两两不同就可以终止了 } }

有了这个其实我们搞不了太大的新闻

height 数组的功能

所以我们还有一个神奇的数组 能做一些事情

height[i] 定义为 sa[i1]sa[i] 的最长公共前缀 (LCP) 的长度

为什么这个有用呢? 因为有这样一个结论。

对于两个后缀 jk ,不妨设 rank[j]<rank[k] ,则不难证明后缀 jkLCP 的长度等于

mini=rank[j]+1rank[k]height[i]

这个结论很显然 画图自己比对一下。

然后如何求这个数组呢 暴力求显然是 O(n2) 的 不满足要求啦

我们又有这样一个结论

height[rank[i]]height[rank[i1]]1

这个证明的话我们如此考虑即可

设排在后缀 i1 前一个的是后缀 k 。后缀 k 和后缀 i1 分别得到后缀 k+1 和 后缀 i

因此后缀 k+1 一定排在后缀 i 前面(这是因为后缀 k 的排名比后缀 i1 要高),

并且最长公共前缀长度为 height[rank[i1]]1 (考虑后缀 k 与 后缀 k1LCP

可以自己手动举例子来理解这个东西qwq

用这个结论去构建的话 总复杂度就是 O(n) 了 如何考虑呢

每次长度最多 1 然后 height 最多就到 n 那复杂度就是这样了。

然后代码就很好写了

int height[N]; inline void Build_Height() { for (register int i = 1, j, k = 0; i <= n; ++ i) { if (k) -- k; //前一个减1 j = sa[rk[i] - 1]; //rk 上一名所在的位置 while (str[i + k] == str[j + k]) ++ k; //和前一个去比较 height[rk[i]] = k; } }

例题讲解

说了这么多 也要一些例题啊qwq

洛谷P3809【模板】后缀排序

和标题一样 就是模板题。。。。

可以用二分哈希比较 LCP 然后去排序 这样复杂度 是 O(nlog2n) 的 跑了 7000ms 后缀数组 1000ms....

BZOJ : 1717: [Usaco2006 Dec]Milk Patterns 产奶的模式

这道题也类似一道模板题

给你一个字符串,求至少出现 k 次的串的最长长度。

这个我们就直接二分答案 ans 然后看是否存在一段 连续的 height 长度 k 且 值都大于 ans 就行了。

/************************************************************** Problem: 1717 User: DOFY (用大佬号子交题233) Language: C++ Result: Accepted Time:84 ms Memory:5592 kb ****************************************************************/ #include <bits/stdc++.h> #define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i) #define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i) #define Set(a, v) memset(a, v, sizeof(a)) using namespace std; inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;} inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;} inline int read() { int x = 0, fh = 1; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1; for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48); return x * fh; } void File() { #ifdef zjp_shadow freopen ("P2852.in", "r", stdin); freopen ("P2852.out", "w", stdout); #endif } const int N = 20010; int n, k; int str[N]; int m, sa[N], rk[N], tmp[N], height[N], c[1001000]; void Radix_Sort() { For (i, 1, m) c[i] = 0; For (i, 1, n) ++ c[rk[i]]; For (i, 1, m) c[i] += c[i - 1]; Fordown (i, n, 1) sa[c[rk[tmp[i]]] --] = tmp[i]; } int maxv; void Build_Sa() { For (i, 1, n) rk[i] = str[i], tmp[i] = i; m = maxv; Radix_Sort(); for (int k = 1, p; k <= n; k <<= 1) { p = 0; For (i, n - k + 1, n) tmp[++ p] = i; For (i, 1, n) if (sa[i] > k) tmp[++ p] = sa[i] - k; Radix_Sort(); swap(rk, tmp); rk[sa[1]] = 1, m = 1; For (i, 2, n) rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? m : ++ m; if (m >= n) break; } } void Build_Height() { for (register int i = 1, j, k = 0; i <= n; ++ i) { if (k) -- k; j = sa[rk[i] - 1]; while (str[i + k] == str[j + k]) ++ k; height[rk[i]] = k; } } int Last, ans = 0; inline bool Check(int val) { int cnt = 1; For (i, 1, n) { if (height[i] >= val) ++ cnt; else cnt = 1; if (cnt >= k) return true; } return false; } int main () { File(); n = read(); k = read(); For (i, 1, n) { str[i] = read(); chkmax(maxv, str[i]); } Build_Sa(); Build_Height(); int l = 1, r = n; while (l <= r) { int mid = (l + r) >> 1; if (Check(mid)) ans = mid, l = mid + 1; else r = mid - 1; } printf ("%d\n", ans); return 0; }

BZOJ : 4566: [Haoi2016]找相同字符

给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。

两个方案不同当且仅当这两个子串中有一个位置不同。

这题有些难度。。。

但思路还是很简单?

就是将两个字符串先拼起来 然后再进行后缀排序

对于每个位置 统计 rk 前面和它 height 有贡献的 且不是同一个串的贡献

统计两遍 一个是后面为 a 串,一个是 b 串。

然后这个直接用单调栈维护就行了qwq

至于如何维护 考虑前连续一段的 height 的贡献就行了 因为 height 连续一段的 min 是单调递减的

cnt,tot 统计前面出现了不同串的个数 cal 存的答案 sta 统计的是当前的 height 。。。

/************************************************************** Problem: 4566 User: zjp_shadow Language: C++ Result: Accepted Time:5108 ms Memory:50516 kb ****************************************************************/ #include <bits/stdc++.h> #define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i) #define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i) #define Set(a, v) memset(a, v, sizeof(a)) using namespace std; inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;} inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;} inline int read() { int x = 0, fh = 1; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1; for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48); return x * fh; } void File() { #ifdef zjp_shadow freopen ("P3818.in", "r", stdin); freopen ("P3818.out", "w", stdout); #endif } const int N = 800100; int c[N], rk[N], sa[N], tmp[N], n, m; void Radix_Sort() { For (i, 1, m) c[i] = 0; For (i, 1, n) ++ c[rk[i]]; For (i, 1, m) c[i] += c[i - 1]; Fordown (i, n, 1) sa[c[rk[tmp[i]]] -- ] = tmp[i]; } char str[N]; void Build_Sa() { For (i, 1, n) rk[i] = str[i], tmp[i] = i; m = 255; Radix_Sort(); for (register int k = 1, p; k <= n; k <<= 1) { p = 0; For (i, n - k + 1, n) tmp[++ p] = i; For (i, 1, n) if (sa[i] > k) tmp[++ p] = sa[i] - k; Radix_Sort(); swap(rk, tmp); rk[sa[1]] = 1, m = 1; For (i, 2, n) rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? m : ++ m; if (m >= n) return; } } int height[N]; void Get_Height() { for (register int i = 1, j, k = 0; i <= n; ++ i) { if (k) -- k; j = sa[rk[i] - 1]; while (str[i + k] == str[j + k]) ++ k; height[rk[i]] = k; } } char str1[N], str2[N]; typedef long long ll; ll ans, cnt[N], cal[N], top, sta[N]; int main () { File(); scanf ("%s", str1 + 1); int len1 = strlen(str1 + 1); scanf ("%s", str2 + 1); int len2 = strlen(str2 + 1); n = len1 + len2 + 1; For (i, 1, n) { if (i <= len1) str[i] = str1[i]; else if (i > len1 + 1) str[i] = str2[i - len1 - 1]; else str[i] = 'X'; } Build_Sa(); Get_Height(); For (i, 1, n) { if (!height[i]) { top = 0; continue ; } int tot = sa[i - 1] <= len1 ? 1 : 0, val = height[i]; while (top && sta[top] > val) tot += cnt[top --]; if (tot) sta[++ top] = val, cnt[top] = tot; cal[top] = cal[top - 1] + sta[top] * cnt[top]; if (sa[i] > len1 + 1) ans += cal[top]; } top = 0; For (i, 1, n) { if (!height[i]) { top = 0; continue ; } int tot = sa[i - 1] > len1 + 1 ? 1 : 0, val = height[i]; while (top && sta[top] > val) tot += cnt[top --]; if (tot) sta[++ top] = val, cnt[top] = tot; cal[top] = cal[top - 1] + sta[top] * cnt[top]; if (sa[i] <= len1) ans += cal[top]; } printf ("%lld\n", ans); return 0; }

__EOF__

本文作者zjp_shadow
本文链接https://www.cnblogs.com/zjp-shadow/p/8727237.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   zjp_shadow  阅读(690)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示