Lyndon 分解学习笔记
一些定义
\(s_1+s_2\),\(s_1s_2\) 均表示字符串拼接,\(s^k\) 表示字符串 \(s\) 重复 \(k\) 遍。\(<,>,\le,\ge\) 均表示字典序的比较。
Lyndon 串:定义一个串 \(s\) 是 Lyndon 串当且仅当 \(s\) 的字典序严格小于 \(s\) 的所有后缀。特别的,一个字符也是 Lyndon 串。
Lyndon 分解:我们将串 \(s\) 划分为 \(w_1+w_2+\cdots+w_k\),其中所有 \(w_i\) 均为 Lyndon 串,且 \(\forall i\in[1,k-1],w_i\ge w_{i+1}\)。这一组 \(w\) 成为 \(s\) 的 Lyndon 分解。可以证明,对于任意字符串 \(s\),这样的分解存在且唯一。
Lyndon 分解
- 若串 \(u,v\) 为 Lyndon 串且 \(u<v\),则 \(uv\) 也为 Lyndon 串。
证明:若 \(|u|>|v|\) 或 \(u\) 不是 \(v\) 的前缀,直接比较即可证明;否则设 \(v=uw\),因为 \(v\) 是 Lyndon 串,\(w\) 的所有后缀都 \(>v\),这样 \(w\) 的所有后缀都 \(>uv\)
这样我们就得到了一个暴力的 Lyndon 串分解的做法:
先把整个串分解为 \(|s|\) 个字符,每个字符都是一个 Lyndon 串。接下来每次找到一个 \(w_i<w_{i+1}\) 的地方,把这两个字符串合并,不断合并直到无法再合并为止。最终得到 $w_1\ge w_2\ge \cdots \ge w_k $。
- 若字符串 \(v\) 和字符 \(c\),满足 \(vc\) 是 Lyndon 串的前缀,则对于字符 \(d>c\) 有 \(vd\) 是 Lyndon 串。
Duval 算法
我们将我们要分解的串 \(S\) 分成三个部分:\(s_1s_2s_3\),其中 \(s_1\) 是已经分解完成的部分,\(s_2\) 是正在分解的部分,\(s_3\) 是未分解的部分。
我们需要保证任意时刻,\(s_2=u^t+u'\),其中 \(u\) 是 Lyndon 串,\(u'\) 是 \(u\) 的前缀(可以为空)。每次我们将 \(s_3\) 中的第一个字符 \(S_k\) 从 \(s_3\) 中加入 \(s_2\) 中。令 \(j=k-|u|\)。
- \(S_k=S_j\),直接将 \(S_k\) 加入 \(s_2\) 即可,唯一的影响是 \(u'\) 变大,可能会令 \(t+1\) 且 \(u'\) 变成空串。
- \(S_k>S_{j}\),根据上面的性质 \(2\),这时 \(u'S_k\) 是一个 \(>u\) 的 Lyndon 串。再根据性质 \(1\),这个 Lyndon 串会不断往前合并,也就是 \(u^tu'S_k\) 变成一个新的 Lyndon 串,作为新的 \(s_2\) 的 \(u\)。
- \(S_k<S_j\),这时我们可以确定 \(u^t\) 这 \(t\) 个串不会再被合并,可以直接确定他们在最终的 Lyndon 分解中。\(s_1+=u^t\),令 \(u'\) 中的元素重新返回 \(s_3\),重新开始分解。
容易发现复杂度是均摊 \(O(n)\) 的。实际实现的时候我们维护三个指针 \(i,j,k\),\(i\) 表示 \(s_2\) 的开头位置,\(j,k\) 和上面相同,这样 \(k-j\) 就表示了 \(|u|\)。三个情况对 \(i,j,k\) 的变化是
- \(j\gets j+1,k\gets k+1\)
- \(j\gets i,k\gets k+1\)
- \(i\gets i+\lfloor\frac{k-i}{k-j}\rfloor(k-j)\),然后 \(j\gets i,k\gets i+1\)。
view code
#include <bits/stdc++.h>
using namespace std;
const int N=1e7+5;
char s[N];
int n,ans;
inline void build(int l,int r){ans^=r;}
int main(){
scanf("%s",s+1);
n=strlen(s+1);
int i=1,j,k;
while(i<=n){
j=i;k=i+1;
for(;k<=n;++k){
if(s[k]==s[j])++j;
else if(s[k]>s[j])j=i;
else break;
}
int len=k-j;
while(i+len-1<k)build(i,i+len-1),i+=len;
}
printf("%d\n",ans);
return 0;
}
Lyndon 分解与最小表示法
对于长度为 \(n\) 的串 \(s\),我们需要找到他的最小表示法。
- 做法 \(1\):我们将串 \(s\) 倍长,变为 \(ss\)。我们求出 \(ss\) 的 Lyndon 分解中,覆盖 \(n\) 的那个。以它的左端点作为 Lyndon 分解的左端点即可。
- 做法 \(2\):
[JSOI2019]节日庆典
我们要求一个字符串的所有前缀的最小表示法。
我们先把串 \(s\) 的 Lyndon 分解写成 \(s=w_1^{t_1}w_2^{t_2}\cdots w_n^{t_n}\),其中 \(w_1,w_2,\cdots,w_n\) 均为 Lyndon 串,且 \(w_1>w_2>\cdots>w_n\)。这时,我们有结论:最小表示法一定是某个 \(w_i^{t_i}\) 的开头。
更进一步地,我们发现如果 \(w_{n-1}^{t_{n-1}}\) 的开头比 \(w_n^{t_n}\) 的开头优的话,因为 \(w_{n-1}>w_{n}\) 所以 \(w_{n-1}\) 一定是 \(w_n\) 的前缀。以此类推,如果 \(w_i^{t_i}\) 是最终的答案的话,那么 \(w_i\) 要是 \(w_{i+1}\) 的前缀,\(w_{i+1}\) 要是 \(w_{i+2}\) 的前缀, ……。
这样可能的起点数量就只有 \(O(\log n)\) 个,我们只需要比较这 \(O(\log n)\) 个点谁更优即可。又因为这 \(O(\log n)\) 个点都有前缀关系,所以我们并不需要求出一个串任意两个后缀的 LCP,我们只需要求一个后缀和整个串的 LCP,扩展 kmp 即可。
这样我们得到了一个 \(O(n\log n)\) 的做法,但是我们还可以做到更优。
考虑 Duval 算法,在 Lyndon 分解的同时求出答案。\(s_2=u^t+u'\),其中 \(u\) 是 Lyndon 串,\(u'\) 是 \(u\) 的前缀(可以为空)。每次我们将 \(s_3\) 中的第一个字符 \(S_k\) 从 \(s_3\) 中加入 \(s_2\) 中。令 \(j=k-|u|\)。
- \(S_k<S_j\),显然在 \(k\) 之后的所有位置中,选择 \(u'\) 前面的任何一个位置作为最小表示法的起点都不如 \(u'\),所以我们直接把 \(u^t\) 忽略不考虑,Duval 算法继续分解 \(u'\) 即可。
- \(S_k>S_j\),这时 \(u^tu'S_k\) 变成一个新的 Lyndon 串,而 \(u\) 前面的所有位置已经确定是不优的,那么我们唯一的选择就是最后一个 Lyndon 串 \(u^tu'S_k\) 的起点作为 \(k\) 的答案。
- \(S_k=S_j\),此时有多种选择:选择 \(u^t\) 的开头或者选择 \(u'\) 中的某个位置。(根据上面所说的,我们不会选择 \(u^t\) 中除了 \(u^t\) 的开头的其它位置)。\(u'\) 是 \(u\) 的一个前缀,所以选择 \(u'\) 中的某个位置这部分的答案是之前算过的,我们取 \(j\) 对应的位置(即 \(i-(j-ans_j)\))即可。这两种情况比较一下取更优的一个即可。
和上面一样,我们要比较的东西有前缀关系,我们只需要求一个后缀和整个串的 LCP,扩展 kmp 即可。这样总复杂度就是 \(O(n)\) 的了。
view code
#include <bits/stdc++.h>
using namespace std;
namespace iobuff{
const int LEN=1000000;
char in[LEN+5],out[LEN+5];
char *pin=in,*pout=out,*ed=in,*eout=out+LEN;
inline void pc(char c){
pout==eout&&(fwrite(out,1,LEN,stdout),pout=out);
(*pout++)=c;
}
inline void flush(){fwrite(out,1,pout-out,stdout),pout=out;}
template<typename T> inline void putint(T x,char div='\n'){
static char s[20];
static int top;
top=0;
x<0?pc('-'),x=-x:0;
while(x) s[top++]=x%10,x/=10;
!top?pc('0'),0:0;
while(top--) pc(s[top]+'0');
pc(div);
}
}
using namespace iobuff;
const int N=3e6+5;
int z[N],n;
char s[N];
inline void exkmp(){
z[1]=n;
for(int i=2,r=0,l=1;i<=n;++i){
if(i<=r)z[i]=min(r-i+1,z[i-l+1]);
while(i+z[i]<=n&&s[i+z[i]]==s[z[i]+1])++z[i];
if(i+z[i]-1>r)l=i,r=i+z[i]-1;
}
}
inline int getmn(int x,int y,int r){
if(x>y)swap(x,y);
int p1=x+(r-y+1),len1=z[p1];
if(len1>=r-p1+1){
len1=r-p1+1;
int p2=len1+1,len2=z[p2];
if(p2+len2-1>=y)return x;
return s[p2+len2]<s[len2+1]?y:x;
}else{
if(len1>=y)return x;
return s[len1+1]<s[p1+len1]?y:x;
}
}
int ans[N];
int main(){
scanf("%s",s+1);
n=strlen(s+1);
exkmp();
for(int i=1,j,k;i<=n;){
if(!ans[i])ans[i]=i;
for(j=i,k=i+1;s[k]>=s[j];++k){
int len=k-j;
if(s[k]==s[j]){
if(!ans[k]){
int u=(k-i)%len+i;
if(ans[j]>=i)ans[k]=getmn(k+ans[j]-j,i,k);
else ans[k]=i;
}
++j;
}else{
if(!ans[k])ans[k]=i;
j=i;
}
}
int len=k-j;
while(i+len-1<k)i+=len;
}
for(int i=1;i<=n;++i)putint(ans[i],' ');
flush();
return 0;
}