• Lyndon 分解学习笔记


    一些定义

    \(s_1+s_2\)\(s_1s_2\) 均表示字符串拼接,\(s^k\) 表示字符串 \(s\) 重复 \(k\) 遍。\(<,>,\le,\ge\) 均表示字典序的比较。

    Lyndon 串:定义一个串 \(s\) 是 Lyndon 串当且仅当 \(s\) 的字典序严格小于 \(s\) 的所有后缀。特别的,一个字符也是 Lyndon 串。

    Lyndon 分解:我们将串 \(s\) 划分为 \(w_1+w_2+\cdots+w_k\),其中所有 \(w_i\) 均为 Lyndon 串,且 \(\forall i\in[1,k-1],w_i\ge w_{i+1}\)。这一组 \(w\) 成为 \(s\) 的 Lyndon 分解。可以证明,对于任意字符串 \(s\),这样的分解存在且唯一。

    Lyndon 分解

    • 若串 \(u,v\) 为 Lyndon 串且 \(u<v\),则 \(uv\) 也为 Lyndon 串。

    证明:若 \(|u|>|v|\)\(u\) 不是 \(v\) 的前缀,直接比较即可证明;否则设 \(v=uw\),因为 \(v\) 是 Lyndon 串,\(w\) 的所有后缀都 \(>v\),这样 \(w\) 的所有后缀都 \(>uv\)

    这样我们就得到了一个暴力的 Lyndon 串分解的做法:

    先把整个串分解为 \(|s|\) 个字符,每个字符都是一个 Lyndon 串。接下来每次找到一个 \(w_i<w_{i+1}\) 的地方,把这两个字符串合并,不断合并直到无法再合并为止。最终得到 $w_1\ge w_2\ge \cdots \ge w_k $。

    • 若字符串 \(v\) 和字符 \(c\),满足 \(vc\) 是 Lyndon 串的前缀,则对于字符 \(d>c\)\(vd\) 是 Lyndon 串。

    Duval 算法

    我们将我们要分解的串 \(S\) 分成三个部分:\(s_1s_2s_3\),其中 \(s_1\) 是已经分解完成的部分,\(s_2\) 是正在分解的部分,\(s_3\) 是未分解的部分。

    我们需要保证任意时刻,\(s_2=u^t+u'\),其中 \(u\) 是 Lyndon 串,\(u'\)\(u\) 的前缀(可以为空)。每次我们将 \(s_3\) 中的第一个字符 \(S_k\)\(s_3\) 中加入 \(s_2\) 中。令 \(j=k-|u|\)

    • \(S_k=S_j\),直接将 \(S_k\) 加入 \(s_2\) 即可,唯一的影响是 \(u'\) 变大,可能会令 \(t+1\)\(u'\) 变成空串。
    • \(S_k>S_{j}\),根据上面的性质 \(2\),这时 \(u'S_k\) 是一个 \(>u\) 的 Lyndon 串。再根据性质 \(1\),这个 Lyndon 串会不断往前合并,也就是 \(u^tu'S_k\) 变成一个新的 Lyndon 串,作为新的 \(s_2\)\(u\)
    • \(S_k<S_j\),这时我们可以确定 \(u^t\)\(t\) 个串不会再被合并,可以直接确定他们在最终的 Lyndon 分解中。\(s_1+=u^t\),令 \(u'\) 中的元素重新返回 \(s_3\),重新开始分解。

    容易发现复杂度是均摊 \(O(n)\) 的。实际实现的时候我们维护三个指针 \(i,j,k\)\(i\) 表示 \(s_2\) 的开头位置,\(j,k\) 和上面相同,这样 \(k-j\) 就表示了 \(|u|\)。三个情况对 \(i,j,k\) 的变化是

    • \(j\gets j+1,k\gets k+1\)
    • \(j\gets i,k\gets k+1\)
    • \(i\gets i+\lfloor\frac{k-i}{k-j}\rfloor(k-j)\),然后 \(j\gets i,k\gets i+1\)
    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 分解与最小表示法

    对于长度为 \(n\) 的串 \(s\),我们需要找到他的最小表示法。

    • 做法 \(1\):我们将串 \(s\) 倍长,变为 \(ss\)。我们求出 \(ss\) 的 Lyndon 分解中,覆盖 \(n\) 的那个。以它的左端点作为 Lyndon 分解的左端点即可。
    • 做法 \(2\)

    [JSOI2019]节日庆典

    我们要求一个字符串的所有前缀的最小表示法。

    我们先把串 \(s\) 的 Lyndon 分解写成 \(s=w_1^{t_1}w_2^{t_2}\cdots w_n^{t_n}\),其中 \(w_1,w_2,\cdots,w_n\) 均为 Lyndon 串,且 \(w_1>w_2>\cdots>w_n\)。这时,我们有结论:最小表示法一定是某个 \(w_i^{t_i}\) 的开头。

    更进一步地,我们发现如果 \(w_{n-1}^{t_{n-1}}\) 的开头比 \(w_n^{t_n}\) 的开头优的话,因为 \(w_{n-1}>w_{n}\) 所以 \(w_{n-1}\) 一定是 \(w_n\) 的前缀。以此类推,如果 \(w_i^{t_i}\) 是最终的答案的话,那么 \(w_i\) 要是 \(w_{i+1}\) 的前缀,\(w_{i+1}\) 要是 \(w_{i+2}\) 的前缀, ……。

    这样可能的起点数量就只有 \(O(\log n)\) 个,我们只需要比较这 \(O(\log n)\) 个点谁更优即可。又因为这 \(O(\log n)\) 个点都有前缀关系,所以我们并不需要求出一个串任意两个后缀的 LCP,我们只需要求一个后缀和整个串的 LCP,扩展 kmp 即可。

    这样我们得到了一个 \(O(n\log n)\) 的做法,但是我们还可以做到更优。

    考虑 Duval 算法,在 Lyndon 分解的同时求出答案。\(s_2=u^t+u'\),其中 \(u\) 是 Lyndon 串,\(u'\)\(u\) 的前缀(可以为空)。每次我们将 \(s_3\) 中的第一个字符 \(S_k\)\(s_3\) 中加入 \(s_2\) 中。令 \(j=k-|u|\)

    • \(S_k<S_j\),显然在 \(k\) 之后的所有位置中,选择 \(u'\) 前面的任何一个位置作为最小表示法的起点都不如 \(u'\),所以我们直接把 \(u^t\) 忽略不考虑,Duval 算法继续分解 \(u'\) 即可。
    • \(S_k>S_j\),这时 \(u^tu'S_k\) 变成一个新的 Lyndon 串,而 \(u\) 前面的所有位置已经确定是不优的,那么我们唯一的选择就是最后一个 Lyndon 串 \(u^tu'S_k\) 的起点作为 \(k\) 的答案。
    • \(S_k=S_j\),此时有多种选择:选择 \(u^t\) 的开头或者选择 \(u'\) 中的某个位置。(根据上面所说的,我们不会选择 \(u^t\) 中除了 \(u^t\) 的开头的其它位置)。\(u'\)\(u\) 的一个前缀,所以选择 \(u'\) 中的某个位置这部分的答案是之前算过的,我们取 \(j\) 对应的位置(即 \(i-(j-ans_j)\))即可。这两种情况比较一下取更优的一个即可。

    和上面一样,我们要比较的东西有前缀关系,我们只需要求一个后缀和整个串的 LCP,扩展 kmp 即可。这样总复杂度就是 \(O(n)\) 的了。

    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;
    }
    
  • 相关阅读:
    Java代理(jdk静态代理、动态代理和cglib动态代理)
    Hive安装
    Spark 集群安装
    Flume 远程写HDFS
    Spark Idea Maven 开发环境搭建
    oracle 通不过网络的原因
    oracle一些基本问题
    linux-redhat配置yum源
    liunx虚拟机网络连接
    redhat安装jdk、tomcat、mysql
  • 原文地址:https://www.cnblogs.com/harryzhr/p/16414198.html
Copyright © 2020-2023  润新知