Z 函数

一些定义

字符串下标以 00 为起点,Z(i)Z(i) 表示 sss[i]s[i\cdots]ss 以下标 ii 开始的后缀)的最长公共前缀长度
z(0)=0z(0) = 0,并且 s[0z(i)1]s[0\cdots z(i)-1]s[ii+z(i)1]s[i\cdots i+z(i)-1] 构成最长公共前缀
ssii 下标开始能和原串 ss 匹配上的最长子串就是 s[ii+z(i)1]s[i\cdots i + z(i) - 1]

朴素算法

朴素算法根据定义暴力来求即可
暴力扩展的代码是

1
while (i + z[i] < n && s[z[i]] == s[i+z[i]]) z[i] += 1;

线性算法

思想,在计算 z(i)z(i) 的时候,利用好已经计算好的 z(0),z(1),z(i1)z(0), z(1), \cdots z(i-1)

对于 ii,我们称区间 [i,i+z(i)1][i, i+z(i)-1]ii匹配段
算法实现的时候,我们尝试维护右端点最靠右的匹配段,不妨记这样的匹配段是 [l,r][l, r]

根据定义,我们有
s[lr]=s[0rl]s[l\cdots r] = s[0\cdots r-l] (即 s[lr]s[l\cdots r]ss 的前缀)

对于当前匹配段 [i,i+z(i)1][i, i+z(i)-1]

  • 如果 iri \leqslant r,那么 s[i,r]=s[il,rl]s[i, r] = s[i-l, r-l],因此我们可以考虑已经算出来的 z(il)z(i-l)
    z(i)min(z(il),ri+1)z(i) \geqslant \min \left(z(i-l), r-i+1 \right)

    • 如果 z(il)<ri+1z(i-l) < r-i+1,直接令 z(i)=z(il)z(i) = z(i-l)
    • 否则的话,z(il)ri+1z(i-l) \geqslant r-i+1,令 z(i)=ri+1z(i) = r-i+1,此时意味着当前匹配段可能可以再往前扩展
      暴力扩展 z(i)z(i) 直到不能扩展为止
  • 如果 i>ri > r,从 s[i]s[i] 开始比较,暴力求出 z(i)z(i)

  • 求出 z(i)z(i) 之后,当前的匹配段是 s[i,i+z(i)1]s[i, i+z(i)-1],如果 i+z(i)1>ri+z(i)-1 > r,那么令
    l=i,r=i+z(i)1l = i, r = i+z(i)-1

应用

匹配所有子串
tt 为文本串,pp 为模式串,求文本串 tt 中模式串 pp 所有出现的位置

  • 构造一个新的字符串 s=p+#+ts = p + \# + t,其中 #\# 不能出现在 s,ts, t
  • 计算 sszz 函数,对于 tt 中的位置 i[0,t1]i \in [0, |t|-1]
    ss 中得到 lcp=z[p+1+i]\text{lcp} = z[|p|+1+i],如果 lcp=p\text{lcp} = |p|
    那么就有一个 pp 出现在 tt 的第 ii 个位置

本质不同子串数
给定一个长度为 nn 的字符串 ss,求 ss 的本质不同子串的数目

算法分析
考虑递推,假设已经知道当前 ss 的本质不同的子串数,考虑在 ss 末尾添加字符 cc
此时本质不同子串数

首先,令 t=(s+c)1t = (s+c)^{-1},表示 s+cs+c 的反串
那么新增加的子串,我们需要考虑以 cc 结尾的子串(其他子串我们递推之前考虑过了)
对应到 tt 中,就是以 cc 开头的前缀

很显然,这样的前缀有 t|t| 个,长度分别是 {1,2,,t}\{1, 2, \cdots, |t|\}
除此之外,我们还要扣除一部分前缀
如果一些前缀在 tt 的其他位置出现了,这些前缀是不需要计算的

计算 ttzz 函数并且找到其最大值 zmaxz_{\max}
那么有一个最长的前缀出现在某一个长度为 zmaxz_{\max} 的子串 [p,p+zmax1][p, p+z_{\max}-1]
同时,这个前缀所对应的子前缀,即长度 zmax\leqslant z_{\max} 的前缀也都会出现

所以有 zmaxz_{\max} 个前缀不需要计算
答案应该是 tzmax|t| - z_{\max}

字符串的整周期
给定一个长度为 nn 的字符串 ss,找一个最短的整数周期
即寻找一个最短的字符串 tt,使得 ss 可以被若干个 tt 拼接而成

  • 计算 sszz 函数,其整周期的长度为
    最小的 nn 的因子 ii,满足 i+z(i)=ni+z(i) = n

前缀函数

先看一些定义
真前缀是指 ss 除了其本身的前缀,真后缀ss 除了其本身的后缀

前缀函数是一个长度为 nn 的数组 π\piπ(i)\pi(i) 表示

  • 如果子串 s[0i]s[0\cdots i] 有一对相等的真前缀和真后缀,不妨设 S[0,k1]S[0, k-1]s[ik+1,i]s[i-k+1, i]
    π(i)\pi(i) 就是这个相等的真前缀和真后缀的长度,π(i)=k\pi(i) = k
  • 如果有不止一对相等的,那么 π(i)\pi(i) 就是其中最长的那一对的长度
  • 如果没有相等的,那么 π(i)=0\pi(i) = 0

π(i)\pi(i) 就是子串 s[0i]s[0\cdots i] 最长的相等的真前缀和真后缀的长度
特别的,π(0)=0\pi(0) = 0

计算前缀函数的朴素算法

  • 一开始令 π(0)=0\pi(0) = 0i[1n1]i \in [1\to n-1] 开始遍历
  • 然后尝试真前缀长度 jj,从大往小了尝试, j=ij = i 看看行不行,不行就减小
    如果当前 jj 真前缀和真后缀相等,我们记 π(i)=j\pi(i) = j,然后退出循环
    否则 jj1j \leftarrow j-1,继续尝试直到 j=0j = 0
  • 如果 j=0j = 0 并且仍然没有匹配,那么令 π(i)=0\pi(i) = 0,继续迭代下一个 ii

高效算法

第一个优化
相邻的前缀函数值最多增加 11
注意到第 ii 次迭代,最长公共真前缀和真后缀为
s[0π(i)1]s[0\cdots \pi(i) - 1],当 ii+1i \leftarrow i+1
最好的情况就是 s[π(i)]=s[i+1]s[\pi(i)] = s[i+1],此时 π(i+1)=π(i)+1\pi(i+1) = \pi(i) + 1

所以 ii+1i \leftarrow i+1 时,前缀函数的值要么增加 11,要么不变,要么减少
在迭代到 ii 的时候
j[π(i1)+10]j \in [\pi(i-1) + 1 \to 0] 开始迭代

1
2
3
4
5
for (int i = 1; i < n; i++) {
for (int j = pi[i-1] + 1; j >= 0; j--) {
// ...
}
}

这个算法的复杂度取决于最大字符串比较次数j=π(i1)+1j = \pi(i-1) + 1
π(i)\pi(i) 只有在最好的情况下才会增长,什么是最好的情况?
就是 s[π(i)]=s[i+1]s[\pi(i)] = s[i+1],此时 j[π(i1)+10]j \in [\pi(i-1)+1 \to 0] 只需要执行 11

而如果比较次数超过 11 次呢?实际上 π(i)\pi(i) 并不会增加,所以总的比较次数为 O(n)O(n)
算法的时间复杂度为 O(n2)O(n^2)

第二个优化
第一个优化我们考虑了
s[0π(i)1]+s[π(i)]s[0\cdots \pi(i)-1] + s[\pi(i)]
s[0i]+s[i+1]s[0\cdots i] + s[i+1]
此时有 s[π(i)]=s[i+1]s[\pi(i)] = s[i+1] 时候的情况,下面来看 s[i+1]s[π(i)]s[i+1] \neq s[\pi(i)] 的情况

从递推的角度来考虑
失配的时候,对于子串 s[0i]s[0\cdots i],尝试去找到仅次于 π(i)\pi(i) 的第二大的长度 jj
使得 s[0j1]=s[ij+1i]s[0\cdots j-1] = s[i-j+1\cdots i]
如果 s[j]=s[i+1]s[j] = s[i+1],那么令 π(i+1)=j\pi(i+1) = j
如果 s[j]s[i+1]s[j] \neq s[i+1],那么我们尝试去找到仅次于 jj 的次大长度 j(2)j^{(2)},以此类推
直到 j=0j = 0,如果仍然有 s[i+1]s[0]s[i+1] \neq s[0],那么 π(i+1)=0\pi(i+1) = 0

下面考虑 jj 如何求
注意到此时我们失配位置是 s[π(i)]s(i+1)s[\pi(i)] \neq s(i+1),同时我们已经有
s[0π(i)1]=s[iπ(i)+1i]s[0\cdots \pi(i)-1] = s[i-\pi(i)+1\cdots i],而我们要找的 jj 满足
s[0j1]=s[ij+1i]s[0\cdots j-1] = s[i-j+1\cdots i],同时 [ij+1,i][i-j+1, i] 对应到 [0,π(i)1][0, \pi(i)-1]后一半长度为 jj 的段
从而有 s[0j1]=s[ij+1i]=s[π(i)jπ(i)1]s[0 \cdots j-1] = s[i-j+1\cdots i] = s[\pi(i) - j \cdots \pi(i) - 1]

换句话说,jj 等价于 s[π(i)1]s[\cdots \pi(i) - 1] 的前缀函数值,即 j=π(π(i)1)j = \pi\left(\pi(i) - 1 \right)
同理,j(2)=π(j1)j^{(2)} = \pi(j-1)

jj状态转移方程如下
j(n)=π(j(n1)1), (j(n1)>0)\displaystyle j^{(n)} = \pi\left(j^{(n-1)} - 1 \right), \ (j^{(n-1)} > 0)

应用

kmp算法

给定一个文本串 tt 和字符串 ss,尝试在 tt 中找到 ss 所有出现的位置

构造一个字符串 s+#+ts + \# + t,其中 #\# 是既不在 ss 中也不在 tt 中出现的分隔符
其中 nn 表示 ss 的长度,mm 表示 tt 的长度,下标从 00 开始

我们考虑 i[n+1n+m]i \in [n+1 \to n+m] 这些位置,如果在某个位置有 π(i)=n\pi(i) = n 成立,根据前缀函数定义
那么说明 ttss 配上了,匹配段的终点是 r=i(n+1)r = i-(n+1),起点呢?只需要终点往前走 nn 的长度
匹配位置是 l=i(n+1)n+1=i2nl = i - (n+1) - n + 1 = i - 2n

字符串的周期

对字符串 ss0<ps0 < p \leqslant |s|,如果 s[i]=s[i+p]s[i] = s[i+p]所有 i[0,sp1]i \in [0, |s|-p-1] 成立
那么称 ppss 的周期

对字符串 ss0r<s0 \leqslant r < |s|,如果 ss 长度为 rr 的前缀和长度为 rr 的后缀相等
就称 ss 长度为 rr 的前缀是 ss 的 border

那么可以推出
ss 如果有长度为 rr 的 border,那么 sr|s| - r 就是 ss 的周期

注意,如果 s|s| 除去前后缀中还空出来一段,那么这一段也是周期的一部分)
这样其实不会违背周期的定义,周期是检查所有 i[0,sp1]i \in [0, |s|-p-1] 的位置,空出来的位置不需要检查)
此时仍然能保证 [0,r1][0, r-1] 中的所有位置满足 s[i]=s[i+p]s[i] = s[i+p]

根据前缀函数的定义,可以得到 ss 所有 border 的长度
π(n1),π(π(n1)1),\displaystyle \pi(n-1), \pi \left(\pi(n-1) - 1 \right), \cdots

那么 ss 最长的 border 长度是 π(n1)\pi(n-1),所以 ss 最小周期是
nπ(n1)n - \pi(n-1)

统计每个前缀出现次数

第一个问题
给定一个长度为 nn 的字符串 ss,我们希望统计每个前缀 s[0i]s[0\cdots i] 在同一个字符串的出现次数

考虑位置 ii,其前缀函数值为 π(i)\pi(i)
意味着字符串一个长度为 π(i)\pi(i) 的前缀以 ii 为右端点
同时,一定不存在更长的前缀以 ii 为右端点,可能存在更短的前缀以 ii 为右端点

那么这个问题就转换成为
给定一个长度为 jj 的前缀,同时也是一个右端点位于 ii 的后缀
那么下一个更小的前缀长度 j(2)<jj^{(2)} < j 是多少?
同时这个前缀也必须是一个以 ii 为右端点的后缀

因此,以 ii 为右端点的前缀,长度从大到小依次是
π(i),π(π(i)1),π(π(π(i)1)1),\displaystyle \pi(i), \pi \left(\pi(i) - 1 \right), \pi \left(\pi\left(\pi(i)-1 \right) - 1 \right), \cdots

假设我们已经知道了 x=π(i)x = \pi(i) 这个前缀出现的次数 ans(x)ans(x)
那么 {π(x1)}\{\pi(x-1)\cdots \}xx 有着相同的右端点,所以 xx 出现了
那么 {π(x1),π(pi(x1)1),}\{\pi(x-1), \pi(pi(x-1) - 1), \cdots \} 也都会出现

由此从后往前扫描每一个可能的 π=x\pi = xans(π(x1))+=ans(x)ans(\pi(x-1)) += ans(x)

1
2
3
4
vector<int> ans(n+1, 0);
for (int i = 0; i < n; i++) ans[ pi[i] ] += 1;
for (int x = n-1; x > 0; x--) ans[ pi[x-1] ] += ans[x];
for (int i = 0; i <= n; i++) ans[i]++;

第二个问题
统计每一个前缀 s[0i]s[0\cdots i] 在另一个给定文本串 tt 中出现次数

构造一个字符串 s+#+ts + \# + t,对 in+1i \geqslant n+1 的位置的 π(i)\pi(i)
执行第一个问题对应的算法即可

一个字符串中本质不同的子串数目

一个长度为 nn 的字符串 ss,希望计算其本质不同的子串的数目
还是用迭代的思路来解决,假设我们已经知道了当前计算出的本质不同子串数目、
考虑在 ss 末尾添加一个字符后,这些子串数目会如何变化

假设我们已经知道了当前 ss 本质不同的子串数量 resres,在 ss 末尾添加一个新字符 cc
我们希望,考虑以 cc 结尾的,并且之前没有遇到过的子串,对这些子串计数

构造字符串 t=s+ct = s+c,将其反转得到 t1t^{-1}
我们需要统计有多少 t1t^{-1} 的前缀未在 t1t^{-1} 的其他地方出现

我们计算 t1t^{-1} 每一个位置的前缀函数并取最大值 πmax\pi_{\max}
那么 t1t^{-1} 中最长的已出现的前缀长度就是 πmax\pi_{\max},自然更短的前缀也都会出现

因此新添加一个字符后,新出现的子串数目为
s+1πmax\displaystyle |s| + 1 - \pi_{\max}

字符串压缩

给定一个长度为 nn 的字符串 ss,我们希望找到 ss 的压缩表示
即寻找一个最短的字符串 tt,使得 ss 可以被 tt 的一份或者多份拷贝的拼接来表示出来

显然我们只需要知道 tt 的长度即可,知道 tt 的长度之后
答案就是 ss 中长度为 t|t| 的前缀

计算 ss 的前缀函数,对于该函数的 π(n1)\pi(n-1),定义 k=nπ(n1)k = n - \pi(n-1)
下面我们尝试证明
如果 knk \mid n,那么 kk 就是答案,否则不存在有效的压缩,答案就是 nn

先证必要性
如果 knk \mid n,意味着字符串可以被划分成长度为 kk 的若干块
nk=π(n1)n - k = \pi(n-1),也就是说,长度为 nkn - k 的前缀等于长度为 nkn-k 的后缀

s0s1s2ks3sπ(n1)1nk=π(n1)sn3sn2sn1k\displaystyle \overbrace{ \underbrace{ s_0s_1s_2 }_{k}s_3\cdots\cdots s_{\pi(n-1)-1} }^{n-k= \pi(n-1)}\overbrace{ s_{n-3}s_{n-2}s_{n-1} } ^{k}

从图中可以发现,长度为 kk 的串被前缀 [0,π(n1)1][0, \pi(n-1)-1] 所包含
再根据前缀函数的性质,最后一个块和倒数第二个块相等,以此类推
这样所有的长度为 kk 的块都是相等的,因此可以将字符串 ss 压缩成长度 kk
充分性用反证法很容易得到

再证明这个解是最优的
如果有一个比 kk 更小的压缩表示,那么就意味着前缀函数的最后一个值 π(n1)>nk\pi(n-1) > n-k
我们仍然取 k=nπ(n1)k = n - \pi(n - 1),这就是答案

如果 knk \nmid n,我们用反证法证明答案是 nn

Educational Codeforces Round 118

D. MEX Sequences
题目大意
给定一个整数序列 <x1,x2,,xk>\left<x_1, x_2, \cdots, x_k \right> 定义为 mex-correct\text{mex-correct}
当且仅当对于每一个 1ik1 \leqslant i \leqslant k,都有 ximex(x1,x2,,xi)1|x_i - \text{mex}(x_1, x_2, \cdots, x_i)| \leqslant 1
给定一个由 nn 个非负整数组成的数组 aa,计算给定数组的非空 mex-correct\text{mex-correct} 子序列的数量
并且对 998244353998244353 取模
注意,子序列只要 ai1ai2aima_{i1}a_{i2}\cdots a_{im} 至少一个下标不同,就被认为是不同的子序列
值相同与否无所谓

算法分析
注意到 mex\text{mex} 数组一定是单调非降的

如果一个序列的前缀 mex=x\text{mex} = x,那么这个序列要么是 [0,1,x][0, 1, \cdots x] 单调不降
要么是末尾几个数在 <x< x>x> x 的数之间来回横跳
dpdp 的时候要综合考虑两种情况

现在我们考虑第 ii 个位置,mex[0i]=x\text{mex}[0\cdots i] = x,此时序列有两种情况
[0,i]:<0,,0,1,,1,,x1,,x1>[0, i] :\left<\underline{0,\cdots, 0}, \underline{1, \cdots, 1}, \cdots, \underline{x-1,\cdots, x-1} \right>
[0,i]:<0,,0,1,,1,,x1,,x1,x+1,,x+1>[0, i]: \left<\underline{0, \cdots, 0}, \underline{1, \cdots, 1}, \cdots, \underline{x-1, \cdots, x-1}, \underline{x+1, \cdots, x+1} \right>

dpi(x)dp_i(x) 表示考虑第 ii 个位置,前缀 mex[0i]=x\text{mex}[0\cdots i] = x 时的方案数
这两种情况必须分开来 dpdp,因为第一种情况 dpi(x)dp_{i}(x) 可以转移到 dpi+1(x+1)dp_{i+1}(x+1)
第二种情况,添加 ai=xa_i = x 之后,dpi(x)dp_{i}(x) 会转移到 dpi+1(x+2)dp_{i+1}(x+2),而这是不合法的,因为此时 mexx=2|\text{mex} -x| = 2

情形1 单调不降
dp1(i,x)dp1(i, x) 表示当前考虑第 ii 位(考虑的意思是,将要把 a[i]a[i] 添加到序列末尾,但还未添加)
目前已得到序列的前缀 mex\text{mex} 值是 xx,此时对应的方案数

  • a[i]<x1 or a[i]>x+1a[i] < x-1 \ \textbf{or} \ a[i] > x+1,那么 a[i]a[i] 不能添加到末尾,否则 mexa[i]2|\text{mex} - a[i]| \geqslant 2 不合法
  • a[i]=x1a[i] = x-1,添加 a[i]a[i] 之后,mex\text{mex} 值不会变化,存在转移 dp1(i+1,x)dp1(i,x)dp1(i+1, x) \leftarrow dp1(i, x)
  • a[i]=xa[i] = x,添加 a[i]a[i] 之后,mex=x+1\text{mex} = x+1,存在转移 dp1(i+1,x+1)dp1(i,x)dp1(i+1, x+1) \leftarrow dp1(i, x)
  • a[i]=x+1a[i] = x+1,添加 a[i]a[i] 之后,mex\text{mex} 值还是 xx,但是就变成了第二种情形
    存在转移 dp2(i+1,x)=dp1(i,x)dp2(i+1, x) = dp1(i, x)

情形2 来回横跳
同样记 dp2(i,x)dp2(i, x) 表示第二种情形

  • a[i]<x1 or a[i]>x+1a[i] < x-1 \ \textbf{or} \ a[i] > x+1,此时 a[i]a[i] 同样不能添加到末尾
  • a[i]=x1a[i] = x-1,添加 a[i]a[i] 后,mex\text{mex} 值不变,存在转移 dp2(i+1,x)dp2(i,x)dp2(i+1, x) \leftarrow dp2(i, x)
  • a[i]=xa[i] = x,添加 a[i]a[i] 之后,mex=x+2\text{mex} = x+2,(因为这种情况我们考虑序列中已经有 x+1x+1 了)
    那么 mexa[i]=x+2x=2|\text{mex} - a[i]| = |x+2 - x| = 2 不符合题意
  • a[i]=x+1a[i] = x+1,添加 a[i]a[i] 之后,同样 mex\text{mex} 值不会变化,存在转移 dp2(i+1,x)dp2(i,x)dp2(i+1, x) \leftarrow dp2(i, x)

综上所述
遍历整个序列,令 x=a[i]x = a[i]
对于情形 11 存在转移
dp1i+1(x+1)+=dp1i(x+1),dp1i+1(x+1)+=dp1i(x),dp2i+1(x1)+=dp1i(x1)dp1_{i+1}(x+1) += dp1_{i}(x+1), \quad dp1_{i+1}(x+1) += dp1_{i}(x), \quad dp2_{i+1}(x-1) += dp1_{i}(x-1)

对于情形 22 存在转移
dp2i+1(x+1)+=dp2i(x+1),dp2i+1(x1)+=dp2i(x1)dp2_{i+1}(x+1) += dp2_{i}(x+1), \quad dp2_{i+1}(x-1) += dp2_{i}(x-1)

在实现的时候,可以省略第一维,但是要注意 dpdp 的顺序
dp2i+1(x1)+=dp1i(x1)dp2_{i+1}(x-1) += dp1_i(x-1) 一定要放在 dp2i+1(x1)+=dp2i(x1)dp2_{i+1}(x-1) += dp2_i(x-1) 之后
因为我们更新 dp2(x1)+=dp1(x1)dp2(x-1) += dp1(x-1) 的时候,所希望的一定是 dp2i+1(x1)dp2_{i+1}(x-1) 而不是 dp2i(x1)dp2_{i}(x-1)
所以应该先把 dp2i+1(x1)dp2_{i+1}(x-1) 全部更新了,那么对应的 dp2(x1)dp2(x-1) 就是新的 dp2i+1(x1)dp2_{i+1}(x-1)