这个字符串菜鸡来填坑了。

照着孔姥爷的博客学的所以这篇博客会和孔姥爷的那篇很像 一模一样

可能有一些 MathJax 炸了但应该问题不大。

一些记号和约定

  • 文章中的字符串下标从 $1$ 开始(代码中一般 index-0),$s_i,s[i]$ 表示字符串 $s$ 的第 $i$ 个字符,$|s|$ 表示 $s$ 的长度
  • $ > $ 和 $ < $ 表示字典序的比较,空串的字典序最小
  • $\Sigma$ 表示字符集大小,$\Sigma^*$ 表示字符集为 $\Sigma$ 的字符串,$\Sigma^+ = \Sigma^* \setminus {\epsilon}$,即非空串集合。
  • $\epsilon$ 表示空串,$\$$ 表示特殊字符,且 $\forall c \in \Sigma, \$ > c$
  • $\overline \Sigma = \Sigma \cup {$}$, $\overline{\Sigma^*}$ 表示字符集为 $\overline \Sigma$ 的字符串
  • 默认普通字符串中不包含特殊字符 $\$$
  • $st, s+t$ 均表示字符串拼接
  • $s^k$ 表示 $s$ 重复 $k$ 次得到的字符串,$s^\infty$ 表示 $s$ 重复无限次得到的字符串
  • $s^R$ 表示 $s$ 翻转过后的字符串
  • $s[i:j]$ 表示 $s$ 的第 $i$ 位到第 $j$ 位构成的子串,若 $i > j$ 则为 $\epsilon$,$s[:i] = s[1:i], s[i:] = s[i:|s|]$
  • $s \prec t \Leftrightarrow s + \$ < v, s \succ t \Leftrightarrow s > t + \$$
  • $\operatorname{lcp}(s,t)$ 表示 $s$ 和 $t$ 的最长公共前缀
  • $\operatorname{period}$:整数 $l$ 是 $s$ 的一个 $\operatorname{period}$ 当且仅当 $\forall i \in [1, |s| - l], s_i = s_{i+l}$
  • $\operatorname{border}$:整数 $l$ 是 $s$ 的 $\operatorname{border}$ 当且仅当 $\forall i \in [1,l], s_i = s_{n - l + i}$ + 整循环串:$s$ 是一个整循环串当且仅当 $\exists t \in \Sigma^*, s = t^k (k \geq 2)$
  • 循环串:$s$ 是一个循环串当且仅当 $\exists t \in \Sigma^*, s = t^k + t[:p]$, 其中 $k$ 是任意整数,$p<|t|$
  • 最短循环节:长度最小的 $t$ 满足 $s = t^k (k \geq 2)$

一些前置结论

  1. 若 $u \prec v$,则 $\forall w_1, w_2 \in \Sigma^*, u + w_1 \prec v + w_2$,$\succ$ 同理。

Lyndon Word

定义 $s$ 是一个 Lyndon Word 当且仅当 $s$ 比它所有的后缀都要小,即 $\forall i \in [2,|s|], s < s[i:]$。

比如 $\texttt{ababb}$ 是一个 Lyndon Word 而 $\texttt{abab}$ 不是。

Lyndon 的一些性质

  • 若 $s$ 是一个 Lyndon Word,则 $\forall i \in [1, |s|-1], s[:i] \prec s[n-i+1:]$
  • 若 $u$ 和 $v$ 都是 Lyndon Word 且 $u < v$,则 $uv$ 也是 Lyndon Word

证明显然。

Lyndon 分解

定义:把一个字符串分为若干 Lyndon Word,并且字典序单调不增。

形式化地说,对于一个字符串 $s$,若 $t_1 + t_2 + \cdots + t_n = s, \forall i \in [1,n], t_i \text{ is a Lyndon Word}$ 且 $\forall i>j, t_i \geq t_j$,那我们称 $t_1 \cdots t_n$ 为 $s$ 的一个 Lyndon 分解。

通常采用 Duval 算法求 Lyndon 分解。

Duval 算法

考虑依次插入字符,维护当前的 Lyndon 分解。

我们假设分解形式为 $T + t^k + t[:p]$,其中 $T = t_1t_2\cdots t_m$ 且满足 $t_1 \geq t_2 \geq \cdots \geq t_m > t$

设当前加入的字符为 $c$,$u = t[p + 1]$,分类讨论:

  • $c = u$,则转移到分解 $T + t^k + t[:p+1]$
  • $c < u$,则 $T \gets T + t + \cdots + t\text{(for m times)}$,并转移到分解 $T + (t + c)$
  • $c > u$,则 $t^k + t[:p] + c$ 是一个 Lyndon Word,则转移到 $T + (t^kt[:p]c)^1 + \epsilon$

具体地,我们考虑维护一个三元组 $(i,j,k)$ 其中 $i$ 是 $t+t[:p]$ 的首字母位置,$[j,k)$ 是最后一个循环节。

那么就可以如下转移:

  • $c = u: (i,j,k) \gets (i, j+1, k+1)$
  • $c < u: (i,j,k) \gets (k, k, k+1)$
  • $c > u: (i,j,k) \gets (i, i, k+1)$

代码如下:

struct duval_triple
{
 int index;
 int loop_l, loop_r;
};
static int *duval(int *dest, const char *src)
{
 struct duval_triple pos = { 0, 0, 1 };
 while (src[pos.index])
 {
   pos.loop_l = pos.index;
   pos.loop_r = pos.index + 1;
   while (src[pos.loop_r] && 
          src[pos.loop_l] <= src[pos.loop_r])
   {
     if (src[pos.loop_l] < src[pos.loop_r])
       pos.loop_l = pos.index;
     else pos.loop_l++;
     pos.loop_r++;
   }
   int len = pos.loop_r - pos.loop_l;
   while (pos.index <= pos.loop_l)
   {
     pos.index += len;
     *dest++ = pos.index;
   }
 }
 return dest;
}

Lyndon 数组

定义:对于一个字符串 $s$,定义 $\lambda_i$ 表示从 $i$ 开始的最长 Lyndon Word 的终止位置, 即 $s[i:\lambda_i]$ 是一个 Lyndon 且 $\forall j > \lambda_i s[i:j]$ 不是 Lyndon Word。

性质:若 $u$ 是一个 Lyndon Word,$s$ 的 Lyndon 分解是 $t_1t_2\cdots t_n$,那么 $u < t_1 \Leftrightarrow u+s < s$

证明:

  • $u < t_1 \Rightarrow u+s < s$
    若 $u \prec t_1$,那么根据前置结论 1,有 $u+s < t_1 + (t_2\cdots t_n)$
    否则有 $|u| < |t_1|$,设 $l=|t_1|-|u|$,那么由于 $t_1$ 是 Lyndon,$t_1[:l] < t_1[|t_1|-l+1:]$
  • $u+s < s \Rightarrow u < t_1$
    考虑使用反证法,即证明 $u\geq t_1 \Rightarrow u+s\geq s$
    同理若 $u \succ t_1$,则 $u+s > t_1 + (t_2\cdots t_n)$
    否则 $|u| \geq |t_1|, u[:|t_1|] = t_1$,设 $l=|u|-|t_1|$,则由 Lyndon 的性质可得 $u[|u|-l+1:] \succ u \geq t_1 \geq t_2$,所以 $u[:l] + u[|u|-l+1:] \succ t_1 + t_2$,即 $u+s\geq s$

算法实现:

从后往前加字符,考虑维护一个单调栈 $t$ 表示当前后缀的 Lyndon 分解($t_0$ 是栈顶),满足 $t_i \geq t_{i+1}$

假设我们当前加入的字符为 $c$。设 $s=c$,每次判断是否有 $s < t_0$,如果是就 $s \gets s+t_0$ 并弹出栈顶。重复上述步骤直到满足单调性,再将 $s$ 插入栈中。此时 $\lambda_i$ 就是 $s$ 的右端点。

复杂度瓶颈在于求 $\operatorname{lcp}$,总复杂度 $O(n \log n)$ 或 $O(n)$。

Runs

定义

定义三元组 $(i,j,p)$ 是 $s$ 的一个 run,当且仅当 $s[j+1] \neq s[j-p+1], s[i] \neq s[i+p-1]$ 且 $s[i:i+p-1]$ 是 $s[i:j]$ 长度最小的出现至少两次的循环节(不一定为整循环节)。

人话就是循环次数至少为 2 的循环串,并且不能往左右延伸。

比如在 $\texttt{ababababba}$ 这个串中:

  • $(1, 8, 2)$ 是一个 run
  • $(3, 8, 2)$ 不是,因为可以继续向左延伸
  • $(1, 8, 4)$ 不是,因为 $\texttt{abab}$ 不是 $\texttt{abababab}$ 的最小 period
  • $(6, 10, 3)$ 不是,因为 $\texttt{abb}$ 没有出现最少 2 次。

Runs: 定义 $\operatorname{Runs}(s)$ 为所有 $s$ 的 run 的集合。

指数:定义一个 run $(l,r,p)$ 的指数为 $\frac{r-l+1}{p}$,即 period $p$ 在 $[l:r]$ 中出现的次数,记为 $e(l,r,p)$

偏序关系:定义两种偏序关系 $<_0, <_1$,其中 $<_0$ 一般表示字典序较小, $<_1$ 一般表示字典序较大。
$\ell \in \{0,1\}$ 表示一种偏序关系,$\overline \ell$ 表示与 $\ell$ 相反的偏序关系,$< _\ell$ 表示 $\ell$ 对应的偏序关系。
在 $<_\ell$ 上定义的 Lyndon Word 意为将比较定义为 $<_\ell$ 定义下的 Lyndon Word。

Lyndon Root:定义 $s[\lambda_l:\lambda_r]$ 是 $(i,j,p)$ 在 $ <_\ell $ 上的 Lyndon Root, 当且仅当 $[\lambda_l, \lambda_r] \in [i,j]$ 且 $s[\lambda_l, \lambda_r]$ 是一个定义在 $<_\ell$ 上的 Lyindon Word。

run 的类型:run $(i,j,p)$ 是 $\ell(\ell \in {0,1})$ 型的,当且仅当 $s[j-p+1] <_\ell s[j+1]$。

$l_\ell(p)$:$l_\ell(p)$ 表示以位置 $p$ 开头的最长定义在 $\ell$ 上的 Lyndon Word (就是 Lyndon 数组)

推论

  • 对于 $\ell$ 型 run,其所有定义在 $<_\ell$ 上的 Lyndon Root $\lambda = [\lambda_l, \lambda_r]$ 都有 $\lambda = l_\ell(\lambda_l)$, 即所有 Lyndon Root 都是同起点中的最长的 Lyndon Word。

不难发现对于$\ell$ 型 run $(i,j,p)$,$\forall p \in [i,j], |l_\ell(p)| \leq j - p + 1$,也就是不存在跨于这个 run 的 Lyndon Word。

  • 对于任意 run $r(i,j,p)$,其所有 Lyndon Root 左端点不重复。

证明比较显然,考虑每一个开始位置,显然 Lyndon Root 的长度小于等于 $p$,根据 Duval 算法的过不难发现小于等于循环节的 Lyndon Word 仅有一个。

Runs 的求解

对于两种 run 分别求解再去重。

先求出所有的 $l_\ell$,即求出 Lyndon 数组。

我们令 $p_i = s[_\ell(i) : l_\ell(i)]$,观察其往左往右能延伸到最远的地方 $s[l_i':r_i']$。对于三元组 $(l_i', r_i', |p_i|)$,我们只需要判断是否满足 $|p_i| \geq \frac{r_i'-l_i'+1}{2}$ 即可。

不难证明这样一定能找到所有的 run,至此完成了 Runs 的求解。

代码

#include <bits/stdc++.h>
constexpr int N = 1000005;
constexpr int B = 2333;
char str[N];
uint64_t s[N];
int n;
uint64_t h[N], bs[N];
uint64_t get(int l, int r)
{
 return h[r] - h[l - 1] * bs[r - l + 1];
}
int lcp(int x, int y)
{
 int l = 1, r = n - std::max(x, y) + 1, res = 0;
 while (l <= r)
 {
   int mid = (l + r) >> 1;
   if (get(x, x + mid - 1) == get(y, y + mid - 1))
     l = mid + 1, res = mid;
   else
     r = mid - 1;
 }
 return res;
}
int lcs(int x, int y)
{
 int l = 1, r = std::min(x, y), res = 0;
 while (l <= r)
 {
   int mid = (l + r) >> 1;
   if (get(x - mid + 1, x) == get(y - mid + 1, y))
     l = mid + 1, res = mid;
   else
     r = mid - 1;
 }
 return res;
}
bool cmp(int l1, int r1, int l2, int r2)
{ // s[l1:r1]<s[l2:r2]
 int l = lcp(l1, l2);
 if (l > std::min(r1 - l1, r2 - l2))
   return r1 - l1 < r2 - l2;
 return s[l1 + l] < s[l2 + l];
}
struct runs
{
 int i, j, p;
 runs(int i = 0, int j = 0, int p = 0) : i(i), j(j), p(p)
 {
 }
 bool operator==(const runs a) const
 {
   return i == a.i && j == a.j && p == a.p;
 }
 bool operator<(const runs a) const
 {
   return i == a.i ? j < a.j : i < a.i;
 }
};
std::vector<runs> ans;
int st[N], t, run[N];
void lyndon()
{
 t = 0;
 for (int i = n; i; i--)
 {
   st[++t] = i;
   for (; t > 1 && cmp(i, st[t], st[t] + 1, st[t - 1]); t--)
     ;
   run[i] = st[t];
 }
}
void init()
{
 bs[0] = 1;
 for (int i = 1; i <= n; i++)
   h[i] = h[i - 1] * B + s[i], bs[i] = bs[i - 1] * B;
}
void get_runs()
{
 for (int i = 1; i <= n; i++)
 {
   int l1 = i, r1 = run[i], l2 = l1 - lcs(l1 - 1, r1), r2 = r1 + lcp(l1, r1 + 1);
   if (r2 - l2 + 1 >= (r1 - l1 + 1) * 2)
     ans.push_back(runs(l2, r2, r1 - l1 + 1));
 }
}
int main()
{
 scanf("%s", str + 1);
 n = (int)std::strlen(str + 1);
 for (int i = 1; i <= n; i++)
   s[i] = (uint64_t)(str[i] - 'a' + 1);
 init();
 lyndon();
 get_runs();
 for (int i = 1; i <= n; i++)
   s[i] = 27 - s[i];
 lyndon();
 get_runs();
 sort(ans.begin(), ans.end());
 ans.erase(unique(ans.begin(), ans.end()), ans.end());
 printf("%d\n", (int)ans.size());
 for (auto u : ans)
   printf("%d %d %d\n", u.i, u.j, u.p);
 return 0;
}



Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.