Runs 小记

来一点不严谨、不详细的备忘性质的速通学习笔记。不放证明了,证明大家可以去洛谷题解区看我稍微看了一眼,然后忘了

首先什么是 Runs?我的理解是它就是一个压缩了所有本质不同的平方串的结构。对于一个字符串 S,定义它的一个 Runs 是满足如下条件的三元组 (l,r,p)

  • S[l,r] 的最小周期为 p

  • 2prl+1

  • 该结构不能继续向左向右扩展。即 Sl1Sl+p1,Sr+1Srp+1。(这里认为边界填充了一个不在字符集中的极小值)

Runs 和平方串之间的关系:显然每个 Runs 的长度为 2p 的倍数的子串一定是一个平方串,反过来也很容易得到任何一个平方串一定至少位于一个 Runs 中,因为一个平方串就是一个未经扩展的 Runs。

一个更强的性质是,一个平方串可以用上述方式唯一被一个 Runs 统计到。证明的大致思想大概是你考虑一个互相包含的 Runs,大的 Runs 的最小周期必须严格大于小的 Runs 的最小周期。而我们可以证明,对于一个 Runs 的所有长度至少为 2p 的子串,其最小周期一定为 p。所以一个最小周期为 p 的平方串(长度 2kp),其只会被最小周期恰好为 p 的 Runs 统计恰好一次。

如何求出 Runs?一个非常简单的想法是考虑在《优秀的拆分》中运用到的调和级数分块。我们枚举当前 Runs 的周期 p,然后每隔 p 个点扔一个关键点。接下来枚举相邻的两个关键点,钦定每一个 Runs 在它的区间中前两个关键点统计到。那么从这两个关键点往前做 lcs 匹配,往后做 lcp 匹配,如果总匹配长度 2p 且往前做的 lcs 没有包含上一个关键点,就找到了一个不一定满足第一个条件的准 Runs。

对于一个 Runs (l,r,p),其可能在 2p,3p 处多次统计,那么对于所有你求出来的准 Runs 中,(l,r) 相同的取 p 最小的哪些就是真正的 Runs 了,复杂度可以用计数排序去重做到严格 O(nlogn)

一般来说,让你写平方串的题目,写调和级数分块已经足够解决问题了。

既然已经有了调和级数分块,我们为什么还需要 Runs 这个结构呢?一大原因是,对于刻画所有平方串组成的集合的性质的题目,我们想要更低的复杂度。

Runs 的一个关键性质是,对于一个串,至多有 2n 种 Runs。我们可以通过一种构造 Runs 算法来说明这个性质:

我们考虑减少调和级数分块做法中,我们需要枚举的关键点对数量。对于一个 Runs,其所有长度为 p 的子串是同一个串的不同循环移位,我们找出其中的最小表示法 S[k,k+p1]

可以证明的一个关键性质:S[k,k+p1] 不存在任何 Border。

这带来两个非常好的推论:一个是 S[k,k+p1] 是一个 Lyndon 串;另一个是,如果钦定 kl+p1,该最小表示法的位置是唯一的,后文的算法钦定在此最小表示法的位置处统计该 Runs。

那么这意味着,字符串以 k 开始的后缀 S[k,n] 严格小于所有 t[k+1,k+p1] 的后缀 S[t,n]

我们先求出每个后缀在后缀数组中的排名 rk,然后对于每一个 i,找到其之后第一个在 rki>rkj 位置 j。将 i,j 作为关键点对扩展出一个 Runs。(这里为了不重复统计同一个 Runs,同样可以钦定我们统计的一定是该 Runs 中出现的第一个循环最小表示法;当然也可以事后去重)。

这样扩展出的 Runs 一定满足性质:rkk>rkk+p,通过 Runs 的周期性质可以发现这个条件等价于 Srp+1>Sr+1

那么那种 Srp+1<Sr+1 的 Runs 该怎么统计呢?对于上述的所有讨论,我们人为逆转字符集的大小关系(包括字符串末尾填充的空字符,从极小值逆转成极大值),相当于 reverse 了一下后缀数组,然后按照刚才的方法再次找出 O(n) 对关键点对扩展。

由于上述讨论,我们就证明了 Runs 的个数不超过 2n,且给出了一种更加清新的 Runs 构造方法。如果使用 SA-IS + O(n)O(1) RMQ 算法实现以上过程,可以把复杂度降低到 O(n)

最后讲一个某道互测题里用到了的性质,对于所有 Runs,rl+1p 之和也是线性级别的。这是因为我们之前在统计 Runs 的时候,我们是把它贡献到前 p 个长度为 p 的串中最小的那一个串统计的。那么,实际上对于该 Runs 的每 p 个串,都可以把它挂到一个你求出来的关键点对上去。

#include <algorithm>
#include <cassert>
#include <cstdio>
#include <cstring>
using namespace std;
int read() {
char c = getchar();
int x = 0;
while (c < 48 or c > 57)
c = getchar();
do
x = (x * 10) + (c ^ 48), c = getchar();
while (c >= 48 and c <= 57);
return x;
}
const int N = 1000003;
const int Lg = 20;
int n, q;
struct LCP {
char s[N];
int buc[N], rk[N], sa[N], od[N], id[N], ht[Lg][N], w, p;
bool eq(int x, int y) {
return od[x] == od[y] && od[x + w] == od[y + w];
}
void getSA(int m) {
for (int i = 1; i <= n; ++i)
++buc[rk[i] = s[i]];
for (int i = 1; i <= m; ++i)
buc[i] += buc[i - 1];
for (int i = n; i; --i)
sa[buc[rk[i]]--] = i;
for (int i = 1; i <= m; ++i)
buc[i] = 0;
w = 1;
p = 0;
while (true) {
for (int i = n; i > n - w; --i)
id[++p] = i;
for (int i = 1; i <= n; ++i)
if (sa[i] > w)
id[++p] = sa[i] - w;
for (int i = 1; i <= n; ++i)
++buc[od[i] = rk[i]];
for (int i = 1; i <= m; ++i)
buc[i] += buc[i - 1];
for (int i = n; i; --i)
sa[buc[rk[id[i]]]--] = id[i];
for (int i = 1; i <= m; ++i)
buc[i] = 0;
rk[sa[1]] = p = 1;
for (int i = 2; i <= n; ++i) {
if (!eq(sa[i], sa[i - 1]))
++p;
rk[sa[i]] = p;
}
if (p == n)
break;
w <<= 1, m = p, p = 0;
}
}
void getLCP() {
s[n + 1] = '!';
for (int i = 1, k = 0; i <= n; ++i) {
if (k)
--k;
if (rk[i] == 1)
continue;
while (s[i + k] == s[sa[rk[i] - 1] + k])
++k;
ht[0][rk[i]] = k;
}
for (int t = 1; t < Lg; ++t)
for (int i = 2; i + (1 << t) - 1 <= n; ++i)
ht[t][i] = min(ht[t - 1][i], ht[t - 1][i + (1 << (t - 1))]);
}
int qry(int x, int y) {
if (x == y)
return n - x + 1;
x = rk[x], y = rk[y];
if (x > y)
swap(x, y);
int k = __lg(y - x);
return min(ht[k][x + 1], ht[k][y - (1 << k) + 1]);
}
} A, B;
char s[N];
int wl[N << 1], wr[N << 1], wp[N << 1], num;
void expand(int x, int y) {
if (s[x] != s[y])
return;
int len = y - x;
int lA = A.qry(x, y);
int lB = B.qry(n - x + 1, n - y + 1);
if (lB <= len and lA + lB > len) {
++num;
wl[num] = x - lB + 1;
wr[num] = y + lA - 1;
wp[num] = len;
}
}
int stk[N], tp;
int p[N << 1], pp[N << 1], loc[N];
void sort_by(int *arr) {
for (int i = 1; i <= num; ++i)
++loc[arr[i]];
for (int i = 1; i <= n; ++i)
loc[i] += loc[i - 1];
for (int i = num; i; --i)
pp[loc[arr[p[i]]]--] = p[i];
for (int i = 1; i <= num; ++i)
p[i] = pp[i];
for (int i = 1; i <= n; ++i)
loc[i] = 0;
}
int main() {
scanf("%s", s + 1);
n = strlen(s + 1);
for (int i = 1; i <= n; ++i)
A.s[i] = B.s[n - i + 1] = s[i];
A.getSA(123), A.getLCP();
B.getSA(123), B.getLCP();
tp = 0;
for (int i = n; i; --i) {
while (tp and A.rk[stk[tp]] > A.rk[i])
--tp;
if (tp)
expand(i, stk[tp]);
stk[++tp] = i;
}
tp = 0;
for (int i = n; i; --i) {
while (tp and A.rk[stk[tp]] < A.rk[i])
--tp;
if (tp)
expand(i, stk[tp]);
stk[++tp] = i;
}
for (int i = 1; i <= num; ++i)
p[i] = i;
sort_by(wp), sort_by(wr), sort_by(wl);
printf("%d\n", num);
for (int i = 1; i <= num; ++i)
printf("%d %d %d\n", wl[p[i]], wr[p[i]], wp[p[i]]);
return 0;
}
posted @   yyyyxh  阅读(200)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示