把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【学习笔记】Lyndon Word

定义

若一个字符串s的最小后缀是它自己,我们称其为Lyndon串。

等价定义:若s是其所有循环重构串中字典序最小的串,则sLyndon串。


Lyndon分解

任意字符串s,都可以唯一分解成s=s1s2...sk,其中silyndon串,且sisi+1

存在性证明

先来看一个引理:

引理1:

如果u,v都是Lyndon串,并且字典序u<v,则uv也是Lyndon串。

(感觉比较显然吧,但还是证明一下

1.len(u)len(v)

因为v>u,所以v>uv,又因为vLyndon串,所以v的所有非自己的后缀都大于v,所以开头在v的部分的后缀都大于uv。因为u也是Lyndon串,所以u的所有后缀都大于uuv的开头在u部分的后缀也就大于uv(在u部分的比较就已经大于)

2.len(u)<len(v)

u不是v的前缀,那么在len(u)之前就有v>u,所以v>uv,同上可证。

uv是的前缀,那么v>uv,同上可证。


有了引理之后再来证明。

首先,s中的每一个单个字符都是一个Lyndon串,初始时每段都只有一个字符。

从左往右开始合并,左边已经合并了的字符串si大于当前字符s[j]的话就把s[j]并入si,根据引理1可得这样合并后分出来的每一段都是Lyndon串。

而每次比较的时候si<s[j]都并进去了,所以没有并进去的话,就满足每一段si>si+1

唯一性证明

(其实这个也比较显然吧,根据上面我们是能并就并了,没有其他可操作的空间,所以就只有一种

这个我们用反证法。

假设对于一个字符串s有两种Lyndon分解

我们记第一次不同的位置为i,设len(si)>len(si)

si=sisi+1...sksk+1[1...l]

sk+1[1...l]指第k+1段串中的前缀1..l

可以得到以下关系:
si<sk+1[1...l]siLyndon串,所以它的后缀大于它

sk+1[1...l]sk+1:一个字符串的前缀小于等于它自己

sk+1siLyndon分解中,前面的段的字典序大于后面的段的字典序。

si<sisisi的前缀,一个字符串的前缀小于等于它自己

综合上述不等式,可以得到si<si,产生矛盾,所以假设不成立


Duval算法

是一种在O(n)时间复杂度之内求出一个串的Lyndon分解的算法。

引理2:

若字符串v和字符c满足vc是某个Lyndon串的前缀,则对于字符d>cvdLyndon串。

(觉得还是可以感性理解

证明:
Lyndon串为vct

根据Lyndon串的性质可得,i[2,len(v)],v[i...len(v)]ctvct.

根据i的取值范围,v[i...len(v)]长度最大是len(v)1,那么v[i...len(v)]c的长度小于等于v

所以v[i...len(v)]c要么是v的前缀(v[i...len(v)]ctt的部分大于vct),要么大于v,总之v[i..len(v)]c不可能小于v,否则v[i...len(v)]ctvct是不会成立的。

所以可以得到:v[i...len(v)]cv

那么:v[i...len]d>v[i...len]c>v,所以v[i...len]>vd


算法流程

在这个算法中,我们维护三个变量i,j,k

其中,s[1...i1]=s1s2...sg是已经固定下来的分解,满足每一段l[1,g1],sl>sl+1

s[i...k1]=thv,h1是没有固定的分解,并且tLyndon串,vt的一个前缀(可为空)。

j=k|t|,当前扫到未处理的字符是s[k]

分三类情况讨论:

s[k]==s[j]:继续往前扫,保持周期

s[k]>s[j]:根据引理2,我们将thvs[k]合并起来,是一个Lyndon串(是向前合并,tvs[k]合并起来,再将tvs[k]与前面的t合并(注意相等的两个串并不能用引理1进行合并,因为uu的后缀u不大于uu)。

这里的合并是相当于把thvs[k]当成一个新的t,不是就把它当成Lyndon分解里的一段了,它还有可能和后面的串拼在一起形成一个新的Lyndon

s[k]<s[j]th的分解被固定为hLyndon串,算法从v的开头重新开始。

复杂度分析

i只会往右移,k移动的距离不超过i右移的距离(k移动|v|,而i移动|thv|

复杂度为O(n)

Code View

#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
#include<iostream>
#include<cstdio>
#include<cmath>
#include<map>
using namespace std;
#define N 5000005
#define INF 0x3f3f3f3f
#define LL long long
int rd()
{
	int x=0,f=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1; c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48); c=getchar();}
	return f*x;
}
int n,ans;
char s[N];
int main()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=1;i<=n;)
	{
		int j=i,k=i+1;
		while(k<=n&&s[j]<=s[k])
		{
			if(s[j]<s[k]) j=i;//合并 
			else j++;
			k++; 
		}
		while(i<=j)
		{
			ans=ans^(i+k-j-1);
			i+=k-j;
		}
	}
	printf("%d\n",ans);
    return 0;
}

——注:博客参考了金策的字符串算法选讲内容

posted @   Starlight_Glimmer  阅读(203)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
浏览器标题切换
浏览器标题切换end
点击右上角即可分享
微信分享提示