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;
}
posted @ 2022-06-26 19:51  harryzhr  阅读(84)  评论(0编辑  收藏  举报