最小表示法与Lyndon 分解 学习笔记
最小表示法
最小表示法指一个串所有循环同构串中字典序最小的一个
可以用这样的算法在线性时间内求出最小表示法
首先复制一份在最后
用两个指针一前一后进行扫描,找到第一个不一样的地方,比如 \(a[i+k]>a[j+k]\),这时说明 \(i...k\) 都不可能成为最小表示法了,那么将 \(i\) 移动到 \(i+k+1\) 即可
int main(){
n=read();
for(int i=1;i<=n;i++){
a[i+n]=a[i]=read();
}
int i=1,j=2;
while(i<=n&&j<=n){
int k=0;
for(;k<n&&a[i+k]==a[j+k];k++);
if(k==n)break;
if(a[i+k]>a[j+k]){
i=i+k+1;
if(i==j)i++;
}
if(a[i+k]<a[j+k]){
j=j+k+1;
if(i==j)j++;
}
}
ans=min(i,j);
for(int i=ans;i<=ans+n-1;i++){
printf("%d ",a[i]);
}
}
Lyndon 分解
首先定义是最小表示法(严格)的字符串为 \(Lyndon\) 串
Lyndon 分解指的是对于一个字符串分解为若干子串,使得每一个子串都是 \(Lyndon\) 串,并且字典序不升
可以证明每个串都有唯一的分解方式
-
引理 1:如果 \(u\) 和 \(v\) 都是 \(Lyndon\) 串并且 \(u<v\),则 \(uv\) 也是 \(Lyndon\) 串。
-
引理2:若字符串 \(v\) 和字符 \(c\) 满足 \(vc\) 是某个 \(Lyndon\) 串的前缀,则对于字符 \(d>c\) 有 \(vd\) 是 \(Lyndon\) 串。
可以画出来手模,这里就不放证明了
- \(Duval\) 算法:
可以结合 这篇博客 里的图理解
首先将串维护成三个部分 \(s_1,s_2,s_3\),第一部分已经分解完成,\(s_2\) 是一个循环节为 \(l\) 的串(末尾为循环节前缀),第三个是待处理部分
维护出三个指针 \(i,j,k\) 分别指向目前已经分解完成的部分,上一个循环节这一位置,以及未处理的第一个位置
现在试图把 \(s_3\) 的第一个字符放入 \(s_2\)
如果 \(a_j==a_k\),那么满足循环性质,\(j\) 指针后移即可
如果 \(a_j<a_k\),并不满足性质,那么直接将 \(s_2\) 与 \(s_k\) 合并在一起成为一个新的 \(Lyndon\) 串,将 \(j\) 回归到 \(s_2\) 开头
如果 \(a_j>a_k\),意味着整个块满足了性质,那么把每一个循环节拿出来当做分解出来的块,即移动 \(i\) 的过程
- 最小表示
用 \(Lyndon\) 分解可以求出最小表示,可以这样来求:
将 \(s\) 复制一倍在后面,寻找分解中跨过拼接点的一个 \(Lyndon\) 串,那么这个串的起点就是最小表示法的起点
int main(){
scanf("%s",a+1);
n=strlen(a+1);
for(int i=1;i<=n;){
int j=i,k=i+1;
while(k<=n&&a[k]>=a[j]){
if(a[k]>a[j])j=i;
else j++;
k++;
}
while(i<=j){
ans^=i+k-j-1;
i+=k-j;
}
}
cout<<ans;
return 0;
}