Lyndon 分解学习笔记
一些定义
, 均表示字符串拼接, 表示字符串 重复 遍。 均表示字典序的比较。
Lyndon 串:定义一个串 是 Lyndon 串当且仅当 的字典序严格小于 的所有后缀。特别的,一个字符也是 Lyndon 串。
Lyndon 分解:我们将串 划分为 ,其中所有 均为 Lyndon 串,且 。这一组 成为 的 Lyndon 分解。可以证明,对于任意字符串 ,这样的分解存在且唯一。
Lyndon 分解
- 若串 为 Lyndon 串且 ,则 也为 Lyndon 串。
证明:若 或 不是 的前缀,直接比较即可证明;否则设 ,因为 是 Lyndon 串, 的所有后缀都 ,这样 的所有后缀都
这样我们就得到了一个暴力的 Lyndon 串分解的做法:
先把整个串分解为 个字符,每个字符都是一个 Lyndon 串。接下来每次找到一个 的地方,把这两个字符串合并,不断合并直到无法再合并为止。最终得到 。
- 若字符串 和字符 ,满足 是 Lyndon 串的前缀,则对于字符 有 是 Lyndon 串。
Duval 算法
我们将我们要分解的串 分成三个部分:,其中 是已经分解完成的部分, 是正在分解的部分, 是未分解的部分。
我们需要保证任意时刻,,其中 是 Lyndon 串, 是 的前缀(可以为空)。每次我们将 中的第一个字符 从 中加入 中。令 。
- ,直接将 加入 即可,唯一的影响是 变大,可能会令 且 变成空串。
- ,根据上面的性质 ,这时 是一个 的 Lyndon 串。再根据性质 ,这个 Lyndon 串会不断往前合并,也就是 变成一个新的 Lyndon 串,作为新的 的 。
- ,这时我们可以确定 这 个串不会再被合并,可以直接确定他们在最终的 Lyndon 分解中。,令 中的元素重新返回 ,重新开始分解。
容易发现复杂度是均摊 的。实际实现的时候我们维护三个指针 , 表示 的开头位置, 和上面相同,这样 就表示了 。三个情况对 的变化是
- ,然后 。
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 分解与最小表示法
对于长度为 的串 ,我们需要找到他的最小表示法。
- 做法 :我们将串 倍长,变为 。我们求出 的 Lyndon 分解中,覆盖 的那个。以它的左端点作为 Lyndon 分解的左端点即可。
- 做法 :
[JSOI2019]节日庆典
我们要求一个字符串的所有前缀的最小表示法。
我们先把串 的 Lyndon 分解写成 ,其中 均为 Lyndon 串,且 。这时,我们有结论:最小表示法一定是某个 的开头。
更进一步地,我们发现如果 的开头比 的开头优的话,因为 所以 一定是 的前缀。以此类推,如果 是最终的答案的话,那么 要是 的前缀, 要是 的前缀, ……。
这样可能的起点数量就只有 个,我们只需要比较这 个点谁更优即可。又因为这 个点都有前缀关系,所以我们并不需要求出一个串任意两个后缀的 LCP,我们只需要求一个后缀和整个串的 LCP,扩展 kmp 即可。
这样我们得到了一个 的做法,但是我们还可以做到更优。
考虑 Duval 算法,在 Lyndon 分解的同时求出答案。,其中 是 Lyndon 串, 是 的前缀(可以为空)。每次我们将 中的第一个字符 从 中加入 中。令 。
- ,显然在 之后的所有位置中,选择 前面的任何一个位置作为最小表示法的起点都不如 ,所以我们直接把 忽略不考虑,Duval 算法继续分解 即可。
- ,这时 变成一个新的 Lyndon 串,而 前面的所有位置已经确定是不优的,那么我们唯一的选择就是最后一个 Lyndon 串 的起点作为 的答案。
- ,此时有多种选择:选择 的开头或者选择 中的某个位置。(根据上面所说的,我们不会选择 中除了 的开头的其它位置)。 是 的一个前缀,所以选择 中的某个位置这部分的答案是之前算过的,我们取 对应的位置(即 )即可。这两种情况比较一下取更优的一个即可。
和上面一样,我们要比较的东西有前缀关系,我们只需要求一个后缀和整个串的 LCP,扩展 kmp 即可。这样总复杂度就是 的了。
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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?