前言 :Orz ShichengXiao 冬令营的时候就早解决了
字符串算法还是不能随意放弃啊 要认真学了!!
这个算法常用于解决字符串上的 LCP 问题 和 一些字符串匹配的问题
这个算法思维难度不是很大 但是代码难度还是有一些的
想学好这个算法 一定要牢牢的记住各个数组的含义 不然容易弄混
原理介绍
-
还是先简单介绍一下原理吧 :
后缀数组就是将一个字符串的后缀全部进行排序 然后把下标存入一些数组里
用那些数组来进行字符串的一些常用操作
为了后缀排序 我们常常使用 O(nlogn) 的倍增算法
(而不用 O(n) 的 DC3 因为它常数和空间大,并且十分不好写)
倍增算法
那接下来介绍一下倍增算法qwq
考虑这样一个小问题 我们比较任意两后缀的字典序大小 有没有什么快速比较的方法?
当然有 就是预处理出他们的一个前缀和后缀的大小关系 然后我们就能用另外两个来比较了。
倍增的思路大概就是如此 我们从小到大 每次长度乘二 排序长度是那些的后缀 然后用之前得到的信息去比较就行了。
具体就是变成双关键字排序,第一关键字就是前半部分的排名,第二关键字就是后半部分的排名。
基数排序
这个东西套上基数排序就能优化一个 log 。(基数排序具体见网上讲解吧qwq)
基数排序:我理解的就是 类似于桶排 我们开个桶来标记一下他们出现的次数
然后几个前缀和 那么这个数组的值就是他的排名 (可以模拟一下)
然后要满足双关键字 那么我们如果有几个元素第一关键字相同 它们在同一个下标上
那么我们就使第二关键字较大的先获得排名 其他的排名就比他小了。
具体实现见程序中的 Radix_Sort
就行了。
然后大概原理就是这样咯qwq (没讲清的话。。请去看看 刘汝佳的《算法竞赛入门经典》)
数组含义
然后我介绍一下等下要出没的数组含义(很重要!!)
- str[i] 表示 i 这个位置上的字符;
- sa[i] 表示 后缀排序后第 i 名后缀的起始点下标;
- rk[i] 表示 i 为起始点下标的后缀的排名(在 swap 后就重新构建就行了);
- tmp[i] 表示以 i 为第二关键字排名的数位置(在 swap 后作为 上一次第一关键字起始点下标为 i 后缀排名);
- c[i] 表示在基数排序中在字符集中编号为 i 的出现次数(后面作为前缀和);
- 还有两个变量 : n 为字符串长度 m 为字符集大小
然后这样就可以直接做后缀排序了
代码解释
代码中有详细解释 可以看看
有了这个其实我们搞不了太大的新闻
height 数组的功能
所以我们还有一个神奇的数组 能做一些事情
height[i] 定义为 sa[i−1] 和 sa[i] 的最长公共前缀 (LCP) 的长度
为什么这个有用呢? 因为有这样一个结论。
对于两个后缀 j 和 k ,不妨设 rank[j]<rank[k] ,则不难证明后缀 j 和 k 的 LCP 的长度等于
rank[k]mini=rank[j]+1height[i]
这个结论很显然 画图自己比对一下。
然后如何求这个数组呢 暴力求显然是 O(n2) 的 不满足要求啦
我们又有这样一个结论
height[rank[i]]≥height[rank[i−1]]−1
这个证明的话我们如此考虑即可
设排在后缀 i−1 前一个的是后缀 k 。后缀 k 和后缀 i−1 分别得到后缀 k+1 和 后缀 i ,
因此后缀 k+1 一定排在后缀 i 前面(这是因为后缀 k 的排名比后缀 i−1 要高),
并且最长公共前缀长度为 height[rank[i−1]]−1 (考虑后缀 k 与 后缀 k−1 的 LCP)
可以自己手动举例子来理解这个东西qwq
用这个结论去构建的话 总复杂度就是 O(n) 了 如何考虑呢
每次长度最多 −1 然后 height 最多就到 n 那复杂度就是这样了。
然后代码就很好写了
例题讲解
说了这么多 也要一些例题啊qwq
和标题一样 就是模板题。。。。
可以用二分哈希比较 LCP 然后去排序 这样复杂度 是 O(nlog2n) 的 跑了 7000ms 后缀数组 1000ms....
这道题也类似一道模板题
给你一个字符串,求至少出现 k 次的串的最长长度。
这个我们就直接二分答案 ans 然后看是否存在一段 连续的 height 长度 ≥k 且 值都大于 ans 就行了。
#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;
}
给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。
两个方案不同当且仅当这两个子串中有一个位置不同。
这题有些难度。。。
但思路还是很简单?
就是将两个字符串先拼起来 然后再进行后缀排序
对于每个位置 统计 rk 前面和它 height 有贡献的 且不是同一个串的贡献
统计两遍 一个是后面为 a 串,一个是 b 串。
然后这个直接用单调栈维护就行了qwq
至于如何维护 考虑前连续一段的 height 的贡献就行了 因为 height 连续一段的 min 是单调递减的
cnt,tot 统计前面出现了不同串的个数 cal 存的答案 sta 统计的是当前的 height 。。。
#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__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 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】