CTS2019 重复
一年前的题又翻出来看。
本来以为这题该说的都应该被说完了,结果发现它有两种几乎互不相干的做法。一种充满理论分析,最后导出了一个十分简洁的计算公式,时间复杂度碾压标算;另一种充满套路,很有经典字符串题风格,目前占据主流。
表面上看,甚至难以想象这两种思路竟然是针对同一道题。但我坚信这两种做法一定存在某种联系,于是有了本文。(好像这已经不是第一次写这种文章了。
方法一
翻译一下题意,以下
求有多少长为
m 的串的无限重复中,至少包含一个字典序严格小于s 的,长恰为n 的子串
模板串处理
我们暂时忘掉无限重复,把文本串当成一个一般的无限长串,重点研究
假设acab,我们将发现任何小于ac。这是因为,要么acac,而后两位小于ab,则abac
推广到一般情况,我们发现如果
任何小于
s 的长为n 的串t ,一定也包含一个子串小于s' 。
证明与上述例子类似,
原本包含
提一句题外话。我们可以注意到,不断删除
下面我们直接称
文本串处理
现在是时候把
我们把文本串看成一个字符环,那么无限重复中的任意子串都是环上的一段(可能自交)。
下面我将说明,任何满足要求(所有子串
-
-
以
p 为前缀的任意长为n 的字符串字典序\ge s 。(也即若|p|<n ,p[|p|]>s[|p|] ;否则p[|p|]\ge s[|p|] )。
例如ababc,则b,ad,ae,abac,ababc,ababf都是满足要求的段。但a,bb,abaa不是满足要求的段。
显然,除非
(证明过程比较长,可以选择性跳过)
- 第一,如果一个字符环能被上述方式切分,那么方案是唯一的。
- 考察字符环的某种切分
A ,假设还存在另一种切分A' 与之不同。 - 这样一来,我们一定能在环上找出分别属于
A,A' 的段[l,r],[l',r'] ,使得l\le l'\le r< r' ,或反之。不妨设l\le l'\le r< r' 。 - 考虑
T[l',r] 。站在p' 的角度,T[l',r]=s[1,r-l'+1] ; - 但站在
p 的角度,因为它是lyndon word,应有T[l',r]>T[l,r-l'+l]\ge s[1,r-l'+1] 。这显然是不可能的。 - 因此至多只可能有一种给环分段的方案。
- 考察字符环的某种切分
- 第二,任何一个合法字符环都能被上述方式切分。
- 从字符环最小表示的开头开始,贪心地切分(一位一位看,能切就切)。
- 我们只需说明,不会出现到最后仍匹配了一段
s 的真前缀(如果恰好匹配了s 也合法)的情形。 - 反证,设第一段为
p ,最后一段为s 的真前缀为q 。我可以断言q+p<p 。 - 因为如果
|q|\ge|p| ,根据切分规则必有q[1,|p|]=s[1,|p|]<p ; - 否则根据
s 是lyndon word,将其后缀替换成等长前缀必然使字典序变小,也即(q+p)[1,|p|]=s[1,|q|]+s[1,|p|-|q|]\le s[1,|p|]<p 。特别地,若p=s ,我们有(q+p)[1,|p|]=s[1,|q|]+s[1,n-|q|]< s[1,n]=p 。 - 这样一来,从
q 开始才应该是字符环的最小表示,与假设矛盾。因此到最后一定恰好能匹配完,而不会出现遗留一段s 的真前缀无法切分的情况。
- 第三,任何能被上述切分表示的字符环一定合法。
- 考察环上可自交子串的开头。
- 如果恰是某段的开头,则根据切分条件的第二条,显然成立。
- 否则因为每段是lyndon word,从段间开头的字典序严格大于从段头开头的字典序。由上一条也成立。
最终做法
虽然上述证明比较繁琐,但是得到的结论却非常清新。因为我们已经可以用合法段的拼接直接表示答案。
设
而
到这一步明显可以
然而有趣的是,得到这个结论,并不止这一条思路。
方法二
考察
我们设
很容易发现
该转移边是该节点即其
fail 树上所有祖先的所有非平凡转移边中,字符最大的那个。
若不满足,可以找到大于当前转移字符的最高祖先,从该祖先的位置开始匹配就能的到长为
于是每个点能走的非平凡转移边个数
由于文本串是无限重复的,那么在足够远处,与
我们要么不走平凡转移边,那么只需考虑环基树每个环大小是否为
表面上看,这两种方法似乎没有任何联系。
上文说最终的自动机的非平凡转移构成一个「环基树和树的混合森林」。这句话固然没错,但是实际的情况比这简单的多。
如果我们先用方法一中的方法将
我们发现,可以将
更为精妙的是,任何一个方法一中提到的的合法段,都可以对应为这个新自动机上的一条从
我们再来考虑新自动机上的一个长为
在方法二中,我们首先枚举点,然后考虑以它为始末的回路个数。那么我们换一个求和顺序,先枚举回路,然后乘上会统计到它的点数。
-
首先所有回路都会被
0 统计到,那么先加上f_m 。 -
其次,枚举回路第一次返回
0 之前的点i-1 ,这条回路显然会被i-1 个非0 点统计到。同时从i-1 返回0 有a_i 种方案,还需走m-i 步,所以共有a_if_{m-i} 条这样的回路,它们对答案的贡献为(i-1)a_if_{m-i} 。
至此,我们完全用方法二的语言解读了方法一中的公式。
代码
#include<bits/stdc++.h>
#define P 998244353
int n,m,i=1,j=2,x=26,f[5050];
char s[5050];
int main(){
scanf("%d%s",&m,s+1);n=strlen(s+1),n=n<m?n:m;
for(;j<=n && s[i]<=s[j];j++) s[i]<s[j]?i=1:i++;
--s[n=j-i];f[0]=1;
for(i=1;i<=m;i++)for(j=1;j<=i && j<=n;j++)
(f[i]+=1ll*('z'-s[j])*f[i-j]%P*(i<m?1:j)%P)%=P;
for(i=1,j=m;j;j>>=1,x=1ll*x*x%P)if(j&1) i=1ll*i*x%P;
std::cout<<(i-f[m]+P)%P<<'\n';
}