Lyndon分解

 

比较偏的算法,仅用于丧心病狂卡后缀数组的时候。

推荐看xht37 - 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;
}
View Code

 

(剩余内容暂时咕咕咕)

posted @ 2020-07-23 02:13  LiuRunky  阅读(577)  评论(0编辑  收藏  举报