Lyndon 分解
- 首先有性质,任意字符串存在唯一 \(Lyndon\) 分解。
- Lyndon 串,须满足,Lyndon 串本身,是它的最小后缀。
- Lyndon 分解,就是把字符串划分成若干 Lyndon 串 \(w_1,w_2,\cdots,w_n\),需满足 \(w_1\ge w_2\cdots\ge w_n\)
- 我们考虑给出一种策略。
- 和大部分字符串算法一样,我们考虑一次加入 \(s_k\) 这个字符,看影响。
- 嗯,凭经验,我们需要有一个变量 \(j\),表示 \([1,j]\) 已经 Lyndon 分解完了。
- 显然即使 串 \([j+1,k]\) 是合法的 Lyndon 串,也不能就直接划分。
- 考虑反例充实算法。可能破坏 \(w_p\ge w_{p+1}\) 的性质,那我们考虑若一个字符大于 \(s_{j+1}\) 的都划分在一个 Lyndon 串中。
- 可是等于呢?设这个位置为 \(t\) 等于之后若是后面再加入的部分不和前面循环就不好了,循环指 \([j+1,t-1]\) 的前缀等于 \([t,i]\)。
- 考虑大于上次循环位置的情况,这不会破环 Lyndon 串本身的性质,所以不用管,由于循环改变直接,设 \([j+1,i]\) 为新的循环即可。
- 若是小于上次循环的位置,我们一定不能将 \(s_i\) 加入,要不必然会破坏 Lyndon 串的性质。于是我们把前面的所有循环依次剥离成为小的 Lyndon 串,然后重新匹配剩余部分。
- 额可是我们为什么不直接遇到 \(s_i=s_{j+1}\) 直接分段呢?还要记录循环,那时因为直接分段就无法判断 大于上次循环位置的情况,而这种情况恰恰要求必须和前面和二为一
- 接下来就是代码实现的细节,和时间复杂度,时间复杂度线性带一些小常数而以(没学证明,就逃了)
- 我们维护 \(i,j,k\) 分别表示 \([1,i]\) 已经划分完,新加入 \(s_k\),\(j\) 是 \(k\) 应该所对应的循环节的位置。
#include <cstring>
#include <iostream>
using namespace std;
const int N = 5000100;
char s[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
cin >> s + 1;
int n = strlen(s + 1), i = 1;
int ans = 0;
while (i <= n)
{
int j = i + 1, k = i;
while (j <= n && s[j] >= s[k])
{
if (s[j] == s[k])k++;
else k = i; j++;
}
while (i <= k)i += j - k, ans ^= i - 1;//[i-j+k,i-1]
}
cout << ans << '\n';
return 0;
}