浅谈Lyndon分解
前言
\(Lyndon\)分解是一个和最小表示法相关的字符串概念。
利用一些常见的字符串算法(如二分+哈希、后缀数组等)都能实现,但最好的还是与其配套的\(Duval\)算法。
\(Lyndon\)串和\(Lyndon\)分解
首先必须要搞清楚\(Lyndon\)串和\(Lyndon\)分解的概念。
定义一个串是\(Lyndon\)串,当且仅当这个串最小的后缀就是它本身。
这其实也意味着它在它的所有循环表示中是最小的。(所以说这个概念和最小表示法相关)
注意这里的最小指严格最小,例如\(aa\)并不算\(Lyndon\)串。
而\(Lyndon\)分解,指的是把一个字符串划分为若干段\(s_1,s_2,...,s_m\),使得任意\(s_i\)都是\(Lyndon\)串,且\(s_i\ge s_{i+1}\)。
对于任意字符串,\(Lyndon\)分解存在且唯一。
\(Lyndon\)串的基本性质
性质一:若字符串\(u,v\)是\(Lyndon\)串且\(u<v\),则\(uv\)是\(Lyndon\)串
应该还是比较显然,也很容易感性理解的。
要严格去证明的话,就分\(u\)是否为\(v\)的前缀讨论。
如果\(u\)不是\(v\)的前缀,根据\(u<v\)可以确定\(u,v\)存在差异位,则\(uv<v\)也显然成立。而\(v\)的所有后缀比\(v\)还要大,那么也就更比\(uv\)大了。
如果\(u\)是\(v\)的前缀,我们分别除去\(uv\)和\(v\)的前\(len(u)\)个相同字符,就变成比较\(v\)与\(v\)从\(len(u)+1\)开始的后缀的大小,由于\(v\)是\(Lyndon\)串,显然\(v\)更小,所以\(uv<v\),之后同上得证。
(通过这个其实也很好理解为什么\(Lyndon\)分解要求\(s_i\ge s_{i+1}\),因为\(s_i<s_{i+1}\)时相邻两\(Lyndon\)串就可以合并,无法保证唯一性)
性质二:若字符串\(u\)和字符\(c,d\)满足\(uc\)是某个\(Lyndon\)串的前缀且\(c<d\),则\(ud\)是\(Lyndon\)串
假设\(uc\)是\(Lyndon\)串\(ucv\)的前缀。
那么对于\(u\)的每一个真后缀\(u'\),都满足\(ucv<u'cv<u'd\)。
又因为\(u<ucv\),所以\(u<u'd\)。
而显然\(len(u)>len(u')\),即\(len(u)\ge len(u'd)\),因此\(u,u'd\)肯定已经存在差异位,即便在\(u\)后面添上一个\(d\)仍然满足\(ud<u'd\),得证。
后缀数组求\(Lyndon\)串
\(Lyndon\)串要求这个串本身是最小的后缀,结合后缀数组很容易想到这就等价于后缀排序后的第一名是这个串本身,即\(SA[1]=1\)。
而要进行\(Lyndon\)分解,我们同样可以利用\(SA\)数组。
一开始,我们把从\(SA[1]\)开始的后缀从原串中分出,显然它一定是一个\(Lyndon\)串。
接着我们不断重复这个过程,按照排名顺序枚举每一个后缀\(SA[i]\),如果它尚未被从原串中分出,就分出从它开始的后缀,否则无需操作。
反正就是一个非常简单粗暴的过程,然而后缀数组复杂度是\(O(nlogn)\),并不优秀。
当然你也可以用后缀自动机来\(O(n)\)建后缀数组,但接下来要介绍的做法比后缀数组要优美得多,是专门为\(Lyndon\)分解而诞生的,却又能进一步扩展到更深层次的问题上去。
\(Duval\)算法
可以做到在\(O(n)\)的时间复杂度、\(O(1)\)的空间复杂度(不计字符串本身)内求出\(Lyndon\)分解,而且代码也十分简洁。
考虑已经分解完的部分我们肯定不会再去动它。
因此,只需考虑当前正在分解的部分,可以表示为\(u^tv\),其中\(u\)是一个\(Lyndon\)串,\(v\)是\(u\)的一个可以为空的真前缀。
具体实现中,我们用\(i\)来表示当前分解部分的起始位置,维护两个指针\(j,k\),其中\(k\)表示当前正在插入的字符,\(j\)表示上一个循环节中对应位置上的字符。
初始时,\(s[i]\)作为一个单独的字符显然是\(Lyndon\)串,因此\(j\)指向\(i\),\(k\)指向\(i+1\)。
如果\(s[j]=s[k]\),相当于可以继续匹配,同时将\(j,k\)加上\(1\)。
如果\(s[j]<s[k]\),根据先前的性质二,\(v+s[k]\)是一个\(Lyndon\)串;而根据性质一,\(u^t<v+s[k]\),可以合并起来得到\(u^tv+s[k]\)作为一个新的\(Lyndon\)串。具体实现就是将\(k\)加\(1\)并将\(j\)移回\(i\)。
如果\(s[j]>s[k]\),那么就到此为止了,这\(t\)个\(u\)将会作为分解固定下来,然后我们更新\(i\)到\(v\)开始的位置,重复之前的算法。
代码(模板题)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1048576
using namespace std;
int n;char s[N+5];
int main()
{
RI i,j,k;for(scanf("%s",s+1),n=strlen(s+1),i=1;i<=n;)
{
j=i,k=i+1;W(k<=n&&s[j]<=s[k]) s[j]<s[k]?j=i:++j,++k;//只要s[j]小于等于s[k]就还未固定
W(i<=j) printf("%d ",i+k-j-1),i+=k-j;//固定下来了,以k-j为循环节分解
}return 0;
}