Lyndon分解
比较偏的算法,仅用于丧心病狂卡后缀数组的时候。
由于参考的文章写的很详细,所以这篇是补充一点自己的认识。
~ 两个概念 ~
Lyndon串:字符串ss本身即为所有后缀中字典序最小的。即有s<s[i..n],i>1s<s[i..n],i>1(字典序比较)。
Lyndon分解:将字符串ss按顺序不重叠地分解成s1+...+sms1+...+sm共mm个Lyndon串,且保证si≥si+1,1≤i<msi≥si+1,1≤i<m。
在很多后缀数组的题目中会出现Lyndon串的概念,不过并不怎么涉及Lyndon分解。
其实Lyndon分解也是可以用后缀数组进行求解的:首先smsm的起点必然是rnk=1rnk=1的位置(即sa[1]sa[1]),接着考虑sm−1+smsm−1+sm则是 除了smsm的后缀中 字典序最小的后缀...可以用一个数组标记rnk=irnk=i的后缀是否已经被包含,那么每次向后循环找到第一个未被包含的rnkrnk(从后向前看,每个Lyndon串起点的rnkrnk是单调增的)将其设为新增Lyndon串的开头即可,接着把该Lyndon串中的位置全标记为“已被包含”即可。
不过求后缀数组是O(n⋅logn)O(n⋅logn)的,在真实情况中会有人卡。于是需要用到一种O(n)O(n)的做法。
~ Duval算法 ~
在Lyndon分解问题中,我们并不需要求出所有后缀间的大小关系,只要能够选出每一个Lyndon串即可。
于是Duval算法考虑了一个字符串ss为Lyndon串的条件:
1. s[1]s[1]必然为ss中最小的字符。
2. 如果s[1]s[1]在ss中多次出现,那么以每个s[1]s[1]字符为起点的子串必然小于开头的子串。
举两个栗子:s=abcabcds=abcabcd是Lyndon串,因为s[1..4]=abca<s[4..7]=abcds[1..4]=abca<s[4..7]=abcd;而s=abcabcs=abcabc不是Lyndon串,因为s[1..4]=abca>s[4..6]=abcs[1..4]=abca>s[4..6]=abc(可以将s[4..6]s[4..6]看做s[4..7]s[4..7],其中s[7]=0s[7]=0)。
于是Duval算法考虑每次出现s[1]s[1]都进行比较。
有i,j,ki,j,k三个指针,其中ii为当前Lyndon串的起点,kk为正在考虑是否加入当前Lyndon串的字符,jj为与s[k]s[k]比较字典序的位置。
这时候,必然被当前Lyndon串包含的子串为s[i..i+(k−j)−1]s[i..i+(k−j)−1]。暂时看不懂没关系,结合下面的执行步骤就好理解了。
在刚开始运算时,i=j=1,k=2i=j=1,k=2。接着不断将kk增加,满足一定条件时改变i,ji,j。
1. s[j]=s[k]s[j]=s[k],那么j=j+1j=j+1继续比较。
2. s[j]<s[k]s[j]<s[k],那么j=ij=i。可以这样理解:只要有s[j]<s[k]s[j]<s[k],那么以kk之前(包括kk)为起点的后缀必然都小于s[i..k]s[i..k]。
3. s[j]>s[k]s[j]>s[k],那么i=i+(k−j)i=i+(k−j)。当前的Lyndon串就确定了,为s[i..i+(k−j)−1]s[i..i+(k−j)−1],接着开始构造下一个。
在2中,可以确定的Lyndon串为s[i..k]s[i..k],其长度为k−j=k−ik−j=k−i。
然后尝试理解一下33。举个栗子,s=abcabcabcs=abcabcabc,根据上述规则在k≤3k≤3时j=1j=1,在3<k≤93<k≤9时j=k−3j=k−3。这样一直执行到k=9k=9,运算后j=7j=7,到了k=10k=10超出了字符串长度,则值为00,有s[j]=a>s[k]=0s[j]=a>s[k]=0,此时开始截取Lyndon串。第一次截取长度为k−j=3k−j=3的串s[1..3]=abcs[1..3]=abc,第二次截取s[4..6]=abcs[4..6]=abc,第三次截取s[7..9]=abcs[7..9]=abc。
也就是说,3是在处理循环的分割。
模板题:洛谷P6114
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=5000005; int n; char s[N]; int main() { scanf("%s",s+1); n=strlen(s+1); int ans=0; for(int i=1;i<=n;) { int j=i,k=i+1; while(k<=n && s[j]<=s[k]) j=(s[j]==s[k++]?j+1:i); while(i<=j) i+=(k-j),ans^=(i-1); } printf("%d\n",ans); return 0; }
这个算法是的时间复杂度显然是O(n)O(n)的,因为i,ki,k均单调增。
~ 一些题目 ~
HDU 6761 (Minimum Index,2020 Multi-University Training Contest 1)
对于一个字符串ss进行Lyndon分解后,在大多数情况下,s[1..i]s[1..i]的minimum index就等于s[1..i]s[1..i]所包含的最后一个Lyndon串的开头位置。
不过观察样例中的s=aabs=aab能够发现,若在某个时刻j≠ij≠i,那就说明s[i..j]s[i..j]与s[k−(j−i)+1..k]s[k−(j−i)+1..k]相等,此时minimum index等于k−(j−i)+1k−(j−i)+1。有一个很讨巧的写法,就是用jj处的minimum index加上k−jk−j,其中k−jk−j就相当于最后一个循环相对于第一个循环的位移。而j=ij=i时,则有minimum index等于ii。
对于两种确定minimum index的方式分开计算就结束了。

#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=1000005; int n; char s[N]; int mpos[N]; int main() { int T; scanf("%d",&T); while(T--) { scanf("%s",s+1); n=strlen(s+1); for(int i=1;i<=n;) { int j=i,k=i+1; mpos[i]=i; while(k<=n && s[j]<=s[k]) { j=(s[j]==s[k++]?j+1:i); mpos[k-1]=(j==i?i:mpos[j-1]+(k-j)); } while(i<=j) i+=(k-j); } int ans=0; for(int i=n;i>=1;i--) ans=(1112LL*ans+mpos[i])%1000000007; printf("%d\n",ans); } return 0; }
(剩余内容暂时咕咕咕)
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· 互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(四):结合BotSharp
· 一个基于 .NET 开源免费的异地组网和内网穿透工具
· 《HelloGitHub》第 108 期
· Windows桌面应用自动更新解决方案SharpUpdater5发布
· 我的家庭实验室服务器集群硬件清单
2019-07-23 快速线性递推