Lyndon分解
比较偏的算法,仅用于丧心病狂卡后缀数组的时候。
由于参考的文章写的很详细,所以这篇是补充一点自己的认识。
~ 两个概念 ~
Lyndon串:字符串$s$本身即为所有后缀中字典序最小的。即有$s<s[i..n],i>1$(字典序比较)。
Lyndon分解:将字符串$s$按顺序不重叠地分解成$s_1+...+s_m$共$m$个Lyndon串,且保证$s_i\geq s_{i+1},1\leq i<m$。
在很多后缀数组的题目中会出现Lyndon串的概念,不过并不怎么涉及Lyndon分解。
其实Lyndon分解也是可以用后缀数组进行求解的:首先$s_m$的起点必然是$rnk=1$的位置(即$sa[1]$),接着考虑$s_{m-1}+s_m$则是 除了$s_m$的后缀中 字典序最小的后缀...可以用一个数组标记$rnk=i$的后缀是否已经被包含,那么每次向后循环找到第一个未被包含的$rnk$(从后向前看,每个Lyndon串起点的$rnk$是单调增的)将其设为新增Lyndon串的开头即可,接着把该Lyndon串中的位置全标记为“已被包含”即可。
不过求后缀数组是$O(n\cdot logn)$的,在真实情况中会有人卡。于是需要用到一种$O(n)$的做法。
~ Duval算法 ~
在Lyndon分解问题中,我们并不需要求出所有后缀间的大小关系,只要能够选出每一个Lyndon串即可。
于是Duval算法考虑了一个字符串$s$为Lyndon串的条件:
1. $s[1]$必然为$s$中最小的字符。
2. 如果$s[1]$在$s$中多次出现,那么以每个$s[1]$字符为起点的子串必然小于开头的子串。
举两个栗子:$s=abcabcd$是Lyndon串,因为$s[1..4]=abca<s[4..7]=abcd$;而$s=abcabc$不是Lyndon串,因为$s[1..4]=abca>s[4..6]=abc$(可以将$s[4..6]$看做$s[4..7]$,其中$s[7]=0$)。
于是Duval算法考虑每次出现$s[1]$都进行比较。
有$i,j,k$三个指针,其中$i$为当前Lyndon串的起点,$k$为正在考虑是否加入当前Lyndon串的字符,$j$为与$s[k]$比较字典序的位置。
这时候,必然被当前Lyndon串包含的子串为$s[i..i+(k-j)-1]$。暂时看不懂没关系,结合下面的执行步骤就好理解了。
在刚开始运算时,$i=j=1,k=2$。接着不断将$k$增加,满足一定条件时改变$i,j$。
1. $s[j]=s[k]$,那么$j=j+1$继续比较。
2. $s[j]<s[k]$,那么$j=i$。可以这样理解:只要有$s[j]<s[k]$,那么以$k$之前(包括$k$)为起点的后缀必然都小于$s[i..k]$。
3. $s[j]>s[k]$,那么$i=i+(k-j)$。当前的Lyndon串就确定了,为$s[i..i+(k-j)-1]$,接着开始构造下一个。
在2中,可以确定的Lyndon串为$s[i..k]$,其长度为$k-j=k-i$。
然后尝试理解一下$3$。举个栗子,$s=abcabcabc$,根据上述规则在$k\leq 3$时$j=1$,在$3<k\leq 9$时$j=k-3$。这样一直执行到$k=9$,运算后$j=7$,到了$k=10$超出了字符串长度,则值为$0$,有$s[j]=a>s[k]=0$,此时开始截取Lyndon串。第一次截取长度为$k-j=3$的串$s[1..3]=abc$,第二次截取$s[4..6]=abc$,第三次截取$s[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)$的,因为$i,k$均单调增。
~ 一些题目 ~
HDU 6761 (Minimum Index,2020 Multi-University Training Contest 1)
对于一个字符串$s$进行Lyndon分解后,在大多数情况下,$s[1..i]$的minimum index就等于$s[1..i]$所包含的最后一个Lyndon串的开头位置。
不过观察样例中的$s=aab$能够发现,若在某个时刻$j\neq i$,那就说明$s[i..j]$与$s[k-(j-i)+1..k]$相等,此时minimum index等于$k-(j-i)+1$。有一个很讨巧的写法,就是用$j$处的minimum index加上$k-j$,其中$k-j$就相当于最后一个循环相对于第一个循环的位移。而$j=i$时,则有minimum index等于$i$。
对于两种确定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; }
(剩余内容暂时咕咕咕)