题解 CF594E 【Cutting the Line】
部分几乎全部内容摘抄自 zzy 的解题报告,orz zzy。
题目来源
IOI2020 集训队作业中 AC 人数最少的一道,毒瘤字符串。
截至 2020 年 2 月 17 日的统计:
注:一共有
题目大意
给定一个长度为
数据范围
预知识
符号含义:
一、\text{Lyndon} 分解
我们定义一个串是
该命题等价于这个串是它的所有循环表示中字典序最小的。
引理 1:如果 u 和 v 都是 \text{Lyndon} 串并且 u<v ,则 uv 也是 \text{Lyndon} 串。
证明:
1、若 \operatorname{len}(u)\ge\operatorname{len}(v)
这时,
2、若 \operatorname{len}(u)<\operatorname{len}(v)
若
若
我们定义一个串
可以证明这种划分存在且唯一。
存在性证明
初始令
唯一性证明
假设对于字符串
不妨设
观察
那么由
矛盾。
引理2:若字符串 v 和字符 c 满足 vc 是某个 \text{Lyndon} 串的前缀,则对于字符 d>c 有 vd 是 \text{Lyndon} 串。
证明:
设该
则
所以
同时因为
故
\text{Duval} 算法
这个算法可以在
该算法中我们仅需维护三个变量
维持一个循环不变式:
-
-
s[i,k-1]=t^h+v(h>1)$ 是没有固定的分解,满足 $t$ 是 $\text{Lyndon}$ 串,且 $v$ 是 $t$ 的可为空的不等于 $t$ 的前缀,且有 $s_g>s[i,k-1]
如下图:
当前读入的字符是
分三种情况讨论:
- 当
s[k]=s[j] 时,直接k\leftarrow k+1,j\leftarrow j+1 ,周期k-j 继续保持 - 当
s[k]>s[j] 时,由引理 2 可知v+s[k] 是\text{Lyndon} 串,由于\text{Lyndon} 分解需要满足s_i\ge s_{i+1} ,所以不断向前合并,最终整个t^h+v+s[k] 形成了一个新的\text{Lyndon} 串。 - 当
s[k]<s[j] 时,t^h 的分解被固定下来,算法从v 的开头处重新开始。
复杂度分析:
代码如下(刚贡献的P6129):
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
char s[5000005];
int n,ans;
int main()
{
scanf("%s",s+1);
n=(int)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)//从v的开头继续
{
ans^=i+k-j-1;
i+=k-j;
}
}
printf("%d\n",ans);
return 0;
}
二、扩展 KMP / Z-algorithm
这个算法可以在
我们设
我们设
假设现在要求出
- 若
i>r ,那么令l=r=i ,暴力向后匹配。 - 否则,令
k=z_i-l+1 ,则如果z_k< r-i+1 ,就直接让z_i=z_k ,否则先让z_i=r-i+1 ,然后暴力匹配。
由于
再附一张图:
代码:
int l=0,r=0;
for(int i=2;i<=n;i++)
{
if(i>r)
{
l=r=i;
while(r<=n&&s[r-l+1]==s[r])r++;
z[i]=r-l,r--;
}
else
{
int k=i-l;
if(z[k]<r-i+1)z[i]=z[k];
else
{
l=i;
while(r<=n&&s[r-l+1]==s[r])r++;
z[i]=r-l,r--;
}
}
}
解题过程
令
Part 1:k=n
当
注意到,如果其中有一个子串没有翻转,那么我们可以把这个子串进一步分割成单个字符,并且每一个字符都翻转(因为单个字符所以是否翻转无影响),也就是说,一定存在一个最优解,满足划分出来的每一段都进行了翻转。
我们用
引理 3:一个字符串的最小后缀在这个字符串中出现的位置不交
证明:设最小后缀为
我们先找到
可以发现,我们当前操作把后缀
至此,对于
考虑
做一遍
Part 2: k\neq n
当
注意到,某些操作是可以合并的:
- 如果连续两次截取的字符串相同,那么可以合为一次。
- 如果连续两次截取的字符串都为回文串,那么可以合为一次。
第一条很简单,关于第二条,别忘了在原问题中我们是可以对字符串进行翻转的。
让
其中
case 1:k\ge 3
注意到,如果这个时候我们取的是
所以此时就是取最后的一段
但是我们很快找到了一组反例:
此时,
如果我们第一次把
而如果我们第一次截取的是
究其原因,我们发现此时
由于一个串的最小后缀除非长度为
在具体实现上,在
case 2:k\le 2
证明:如下图:
我们发现
定理 3 告诉我们,
枚举
继续分析性质
首先假设
再来一张大图:
直线表示平移,曲线表示翻转。
看下面两个,由于
再看上面两个,由于
换句话说,我们只需要找到最大的
由于一次比较是 因此暴力算几项就完事了!!1
至此,我们就得到一个非常优美的算法 2了:
步骤 1:特判
步骤 2:如果
步骤 3:如果
- 两次均翻转:答案即为
S 。 - 两次均不翻转:答案即为
S^r 的最小表示。 - 第一次翻转,第二次不翻转:用 KMP 算法实现查询 LCP。
- 第一次不翻转,第二次翻转:暴力比较求出最后一个
SB_q 比SB_{q-1} 优秀,则t_1=SB_q 就是答案。
时间复杂度 甚至
代码
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
char s[10000005],t[5000005],t1[5000005],t2[5000005],t3[5000005],t4[5000005];
int n,k,m,nowl,z[10000005],A[5000005],l[5000005];
void extend(char*s,int l)//在t串之后新加入s[:l-1]
{
for(int i=0;i<l;i++)t[++nowl]=s[i];
}
void solve1(int n)//两段都翻转,直接就是原串
{
for(int i=1;i<=n;i++)t1[i]=s[n-i+1];
}
void solve2(int n)//两段都不翻转,最小表示法
{
for(int i=n+1;i<=2*n;i++)s[i]=s[i-n];
int i=1,j=2,k=0;
while(i<=n&&j<=n&&k<n)
{
if(s[i+k]==s[j+k])k++;
else if(s[i+k]<s[j+k])j+=k+1,k=0;
else i+=k+1,k=0;
if(i==j)j++;
}
for(int x=0;x<n;x++)t2[x+1]=s[min(i,j)+x];
}
void solve3(int n)//第一段翻转第二段不翻转,扩展 KMP
{
for(int i=n+1;i<=2*n;i++)s[i]=s[n-(i-n)+1];
n*=2;
z[1]=n;
int l=1,r=1;
for(int i=2;i<=n;i++)
{
if(r<i)
{
l=r=i;
while(r<=n&&s[r]==s[r-l+1])r++;
z[i]=r-l;
r--;
}
else
{
if(z[i-l+1]<r-i+1)z[i]=z[i-l+1];
else
{
l=i;
while(r<=n&&s[r]==s[r-l+1])r++;
z[i]=r-l;
r--;
}
}
}//以上为扩展KMP算法
n/=2;
int j=n+1;
for(int i=n;i>=1;i--)
{
int l=i,r=j-1;
l=n-l+1,r=n-r+1;
int len=z[r+n];
if(len<l-r+1)
{
if(s[len+1]>s[j-len-1])j=i;
}
else
{
len=j-i;
l=len+1,r=j-1;
if(s[z[l]+1]<s[l+z[l]])j=i;
}
}
int tot=0;
for(int i=n;i>=j;i--)t3[++tot]=s[i];
for(int i=1;i<j;i++)t3[++tot]=s[i];
}
void solve4(int n)//第一次不翻转,第二次翻转,暴力比较
{
int p=m;
while((A[p+1]-A[p])*2<=(A[p]-A[p-1]))
{
bool flag=0;
for(int i=A[p]-1;i>=A[p-1];i--)
{
if(s[i]<s[n-i+A[p-1]])
{
flag=1;
break;
}
if(s[i]>s[n-i+A[p-1]])break;
}
if(flag)break;
p--;
}
p=A[p];
int tot=0;
for(int i=p;i<=n;i++)t4[++tot]=s[i];
for(int i=p-1;i>=1;i--)t4[++tot]=s[i];
}
void update(char*t1,char*t2,int n)//取字典序最小
{
for(int i=1;i<=n;i++)
{
if(t1[i]<t2[i])return;
if(t2[i]<t1[i])break;
}
for(int i=1;i<=n;i++)t1[i]=t2[i];
}
void solve(int n)
{
solve1(n);
solve2(n);
solve3(n);
solve4(n);
update(t1,t2,n);
update(t1,t3,n);
update(t1,t4,n);
extend(t1+1,n);
}
int main()
{
scanf("%s",s+1);
n=(int)strlen(s+1);
scanf("%d",&k);
for(int i=1;i*2<=n;i++)swap(s[i],s[n-i+1]);//取反串
if(k==1)//特判k=1
{
for(int i=1;i<=n;i++)t[i]=s[n-i+1];
for(int i=1;i<=n;i++)
{
if(s[i]<t[i])
{
printf("%s\n",s+1);
return 0;
}
if(s[i]>t[i])
{
printf("%s\n",t+1);
return 0;
}
}
printf("%s\n",s+1);
return 0;
}
for(int i=1;i<=n;)//Lyndon分解
{
int j=i,k=i+1;
while(s[j]<=s[k])
{
if(s[j]<s[k])j=i;
else j++;
k++;
}
A[++m]=i;
l[m]=k-j;
while(i<=j)i+=k-j;
}
A[m+1]=n+1;
while(m&&k>=3)//当k>=3时
{
extend(s+A[m],A[m+1]-A[m]);
if(l[m]!=1||l[m-1]!=1)k--;
m--;
}
if(!m)
{
printf("%s\n",t+1);
return 0;
}
solve(A[m+1]-1);//当k=2时
printf("%s\n",t+1);
return 0;
}
总结
本道题涉及到了后缀自动机,