【模板】Lyndon 分解
题意
定义 $\text{Lyndon}$ 串为:其本身是最小后缀的串。
再定义 $\text{Lyndon}$ 分解为:将一个串分解为一些 $\text{Lyndon}$ 串,且这些串的字典序是一个单调不升的关系。
求一个串的 $\text{Lyndon}$ 分解的方案。
思路
首先,我们能确定:$\text{Lyndon}$ 分解一定存在且唯一。
用构造法证明:首先,找出最小后缀,作为一个 $\text{Lyndon}$ 串。然后,在剩余字符串里一直重复此过程。一直到剩余的字符串为空为止。
首先,上述证明有解是显而易见的;其次,每次我们若不按照上述方法取的话,分两种情况:
-
取的一段比最小后缀长
显然这个字符串不是 $\text{Lyndon}$ 串。
-
取的后缀比最小后缀短
不满足 $\text{Lyndon}$ 串单调不升的定义。假设最小后缀从第 $i$ 个字符开始,那么 $i$ 位置必然作为一个 $\text{Lyndon}$ 串的开头(不然 $i$ 位置所处的那个串就不是 $\text{Lyndon}$ 串)。再由于当前是一段后缀,位置必定在剩下的所有分解的串之后,显然 $i$ 位置所处的那个 $\text{Lyndon}$ 串与当前后缀不满足不升序的关系。
故而只存在唯一解。
那我们该怎么求呢?
方法 $1$:后缀数组 $\text{SA}$
我们考虑按照上述构造法中规中矩地求。会发现求最小后缀一事可以对后缀进行排序,然后把所有的后缀丢进一个堆里,堆顶为拥有最小字典序的后缀。若我们发现堆顶的后缀是一个合法的后缀(假设当前的剩余字符串为 $1$ 到 $x$,然后堆顶是 $y$ 开头的后缀,合法当且仅当 $1 \leq y \leq x$),那么最小后缀就是它。否则删除堆顶元素,因为之后它也不可能合法了。
注:上述合法的定义表达的意思是堆顶的后缀与当前剩余的字符串有交,即产生的 $\text{Lyndon}$ 串不为空。
用倍增法求解,时间复杂度 $O(n \log n)$;若是 $\text{DC3}$ 算法,时间复杂度 $O(n)$。
方法 $2$:$\text{Duval}$ 算法
注意,本方法与上述构造法无关。
考虑正着求。我们将把字符串 $s$ 分为 $3$ 个部分:
- $s1$ 表示前面已经分解完的 $\text{Lyndon}$ 串。
- $s2$ 表示当前正在处理的 $\text{Lyndon}$ 串。
- $s3$ 表示未分解、未在处理的剩下的串。
有 $s=s1+s2+s3$($+$ 表示字符串拼接)。
$\text{Duval}$ 算法的思想就是每次保证 $s2$ 是 $\text{Lyndon}$ 串的情况下,且让 $s2$ 的元素越多越好。具体来说,是要维护一个周期,下面会有详细的讲解。
-
先给出以下定理:
若 $a,b$ 都是一个 $\text{Lyndon}$ 串,且 $a<b$,那 $a+b$ 必定也是 $\text{Lyndon}$ 串。
证明:考虑 $b$ 既然是 $\text{Lyndon}$ 串,那最小后缀必定为其本身。要想 $a+b$ 不是 $\text{Lyndon}$ 串,必定有 $a>b$,与条件不符。
于是我们设当前 $s2$ 的开头为位置 $i$,要考虑加入的字符为 $k$,当前 $k$ 与周期中的 $j$ 位置作比较,有以下三种情况:
-
$s_k=s_j$
无事发生。$j$ 自增 $1$,$k$ 也自增 $1$,继续对下一个字符的比较。
-
$s_k>s_j$
即当前的字符加入依旧是一个 $\text{Lyndon}$ 串(根据上述定理),由于字符的不一样,前面的所有周期加这个 $s_k$ 都变成同一个 $\text{Lyndon}$ 串。
由于当前整个变成了一个新周期,继续从头开始比较,所以有:$j$ 赋值为 $i$,$k$ 自增 $1$。
-
$s_k<s_j$
显然不再满足一个 $\text{Lyndon}$ 串的性质。假设 $t$ 为当前 $s2$ 中的一个周期,$t'$ 为 $t$ 的前缀,有 $s2=t+t+...+t+t'$,所以把之前的每个周期 $t$ 分解为一个 $\text{Lyndon}$ 串。对于 $t'$,继续和后面的字符做 $\text{Lyndon}$ 分解。
若论更为具体的处理,我们有:
- 周期长度为 $k-j$。
- 从 $i$ 开始,一直加上这个周期,边加边统计答案 $i-1$。重复此操作直到 $i + (k-j) \leq k$(即 $k$ 前的最后一个周期),移项得 $i \leq j$。
时间复杂度 $O(n)$。假设每次寻找到的周期长度为 $len$,那么我们每次会处理掉那些完整的周期 $t$,最后的 $t'$ 留到下次去处理。发现 $t'$ 的长度必定不大于周期的长度。所以每删除一段,最多产生双倍的线性时间,且删除了就放入了 $s1$ 不会再恢复。故而为 $O(n)$。
#include <bits/stdc++.h>
#define L(i, a, b) for(int i = (a); i <= (b); i++)
#define R(i, a, b) for(int i = (a); i >= (b); i--)
using namespace std;
const int N = 5e6 + 10;
int n, i = 1, j, k, ans;
char s[N];
int main(){
scanf("%s", s + 1), n = strlen(s + 1);
while(i <= n){
for(j = i, k = i + 1; k <= n && s[k] >= s[j]; k++)
j = (s[k] == s[j]? j + 1 : i);
for(; i <= j; i += k - j, ans ^= i - 1);
}
printf("%d\n", ans);
return 0;
}