CTSC2006 歌唱王国

发布时间 2023-11-09 09:43:42作者: 小山云盘

[Luogu P4548 CTSC2006] 歌唱王国

提前约定:\(\text{border}\) 指的是公共前后缀。


题意

一个无限长的字符串 \(S\),其中每个字符都是 \([1,n]\) 中的一个随机整数,求对于给定的字符串 \(t\)\(S\) 中包含 \(t\) 的最短前缀的期望长度。

数据范围:\(1\le n\le 10^5\)\(1\le m\le 10^5\)。有多测,数据不超过 \(50\) 组。


题解

有两种做法:

  • 做法1:AC自动机上DP

做一个单串的AC自动机(KMP自动机),生成的显然是一张有向有环图,需要从根节点到达目标节点。

因为走每条边概率相等,可以参考早期学习概率与期望时的高斯消元算法来求解期望。

但是很显然这样的做法时间复杂度太高,没有办法通过本题。

那么我们现在开始好好看看如何优化。

如果我们设 \(g_i\) 表示到达 \(i\) 点所需的期望,那么此时显然我们能够得到DP式:

\[\large g_i=mg_{i-1}+m-\sum g{j-1} \]

其中 \(j\) 是连到的所有点。上述的DP式的含义可以理解为要么从上一个点过来,要么在上一个点走错了从别的地方回到上一个点再来。

如果我们把 \(j\) 看作 \(\text{border}\) 的结尾,那么此时DP式变成:

\[\large g_i=mg_{i-1}+m-\sum g_j \]

接下来我们发现这样子时间复杂度从 \(\mathcal{O}(n^3)\)缩减到了 \(\mathcal{O}(nm)\),还是不行,于是继续优化。

\(f_i=\sum g_j\),我们如何求解 \(f_i\)

此时我们观察AC自动机,发现上面的 \(\text{fail}\) 指针就是KMP的 \(\text{next}\) 指针,并且形成了一棵树,其父亲正好就是最长的 \(\text{border}\)(每个节点都是指向父亲的),进一步,所有的祖先其实都是 \(\text{border}\)

考虑我们求解 \(f_i\) 实际上可以认为是在 \(i-1\) 的情况下额外加入了一个字符。在这些 \(\text{border}\) 中,有一些依旧是 \(\text{border}\),但有些不是了。

现在我们加上所有的 \(\text{border}\) 的贡献,然后减去所有不合法的,就能得到答案。

此时能够得到DP式:

\[\large f_i=f_{next_i}+g_{next_{i+1}}-g_{next_i+1} \]

这里不做深入,因为我其实也没太弄懂,Luogu题解传送门

随后你发现这就没了,时间复杂度 \(\mathcal{O}(n)\),甚至简单到只需要跑一遍 \(next\) 数组。

给份代码:code


  • 做法2:概率生成函数(概率母函数,又称 \(\text{PGF}\)

首先要明白生成函数是什么,如果你已经知道了,可以往下翻到下一个分割线,在这里浅谈一下关于生成函数我个人的理解。

生成函数实质上是一个多项式,常见的标准形态长这样:

\[\large F(x)=\sum\limits_{i=0}^{+\infty} a_ix^i \]

考虑对于生成函数的操作,对于 \(F(x)\),如果我们将其乘上一个 \(x\),那么有:

\[\large xF(x)=\sum\limits_{i=0}^{+\infty}a_ix^{i+1}=\sum\limits_{i=1}^{+\infty}a_{i-1}x^i \]

当然,上面的操作可以逆着来,但是我们先暂时不进行讨论。注意观察,对于上面的式子,如果我们再次乘上一个 \(x\),会发生什么?

\[\large x^2F(x)=\sum\limits_{i=1}^{+\infty}a_{i-1}x^{i+1}=\sum\limits_{i=2}^{+\infty}a_{i-2}x^i \]

此时其实可以发现,乘法可以看作 \(a\) 数列平移

实际上,我们正是利用这一点来做题。举个简单的例子吧,我们的 \(\text{Fibbonacci}\) 数列的通项公式就是利用生成函数求解的。

\(F(x)=\sum\limits_{i=0}^{+\infty}f_ix^i\),其中 \(f_i\) 表示 \(\text{Fibbonacci}\) 数列的第 \(i\) 项。

则我们发现:

\[\large F(x)=xF(x)+x^2F(x)+x \]

我知道各位很懵逼,咱们来看看这是怎么得到的:

\[\begin{equation}\begin{split} F(x) & =\sum\limits_{i=0}^{+\infty}f_ix^i \\ & =f_0+xf_1+\sum\limits_{i=2}^{+\infty}f_ix^i \\ & =f_0+xf_1+\sum\limits_{i=2}^{+\infty}(f_{i-1}+f_{i-2})x^i \\ & =-xf_0+f_0+xf_1+\sum\limits_{i=1}^{+\infty}f_{i-1}x^i+\sum\limits_{i=2}^{+\infty}f_{i-2}x^i \\ & =xF(x)+x^2F(x)+xf_1-(x-1)f_0 \\ & =xF(x)+x^2F(x)+x \\ \end{split}\end{equation} \]

怎样?是不是更懵逼了。

那么接下来我们得到 \(F(x)=\dfrac{x}{1-x-x^2}\)

有一个众所周知(并不)的结论:\(\sum\limits_{i=0}^{+\infty}x^i=\dfrac{1}{1-x}\)

证明很扯淡,但是天衣无缝:

\[\begin{equation}\begin{split} & S=\sum\limits_{i=0}^{+\infty}x^i \\ & xS=\sum\limits_{i=1}^{+\infty}x^i \\ & \therefore (x-1)S=-1 \\ & \therefore S=\dfrac{1}{1-x} \\ \end{split}\end{equation} \]

于是我们可以考虑把上面求出的 \(F(x)\) 按照这个方法展开,此时两种方法(整体法及待定系数法)可以得到两种不同的通项公式,可以见OI-wiki中“普通生成函数”的介绍。

生成函数的一个好处在于它是多项式,我们不需要知道实际意义是怎样,只需要知道,这是对的,那也是对的,那么最后无论答案是什么,它就是对的。

可能答案甚至没有实际意义,就好比这道题最后由生成函数推出的答案谁也解释不了是什么,但是它就是对的,就像方程你也写不出实际意义一样。


我们回到本题,现在我们考虑如何利用生成函数来解决问题。

\(f_i\) 表示到 \(i\) 结束的期望,\(g_i\) 表示到 \(i\) 没结束的期望。

此时我们得到对应的两个生成函数 \(F(x)=\sum\limits_{i=0}^{+\infty}f_ix^i\)\(G(x)=\sum\limits_{i=0}^{+\infty}g_ix^i\),会发现一个特别的东西:\(F(1)=1\)

这很显然,代入之后一眼就知道了,它在后面的解题中会起到作用,现在我们先不去管它。

接下来是这道题最关键的三个点:

  • 1.\(G(1)\) 即为所求

相信各位看到这里都是一脸懵逼:啥玩意儿?这和答案有关系?

我们有简单的求导证明,但是各位都不会求导,那只能感性理解。

考虑到答案其实就是 \(\sum\limits_{i=0}^{+\infty}if_i\),仔细想想会发现,\(g_i=\sum\limits_{j=i+1}^{+\infty}f_j\)

那接下来各位不难发现 \(g\) 的总和就是答案,也就是 \(G(1)=\sum\limits_{i=0}^{+\infty}g_i\)

具体来说,每一个 \(f_i\) 正好被 \(j\in [0,i-1]\) 中所有的 \(g_j\) 计算了一遍,总共就被计算了 \(i\) 遍。

接下来我们继续走,目标就很明确了,需要求解 \(G(1)\) 的值。

  • 2.\(\dfrac{g_i}{n^m}=\sum\limits_{S_[1,t]\text{is S's border}}\dfrac{f_{i+t}}{n^{m-t}}\)

在这里我们设给出的字符串为 \(S\),其长度为 \(m\)

这个式子非常有意思,实际上它代表了一种情况的答案。

这种情况描述起来大概就是这样:(转自luogu博客

有一天你开始随机,随机了 \(i\) 个字符还没有随机出 \(S\),你失去了耐心,说:“无论如何,再给我随机 \(m\) 个!”然后 \(m\) 个过后,你发现这 \(m\) 个字符恰好是 \(S\)

那么显然式子的左边就是这个玩意儿,它表示前 \(i\) 个字符没有结束,并且直接得到了一个 \(S\),由于 \(S\) 的出现概率为 \(\dfrac{1}{n^m}\),所以显然正确。

接着右边又是怎么回事呢?此时你其实在随机完之前就已经结束咧!但是你并没有选择结束,而是继续进行了。

由于 \(f_i\) 保证在 \(i\) 之前都没有结束(见定义),因此你会发现,右边这个式子也正好完美地表示了上述的情况(此处注意 \(S\) 也是 \(\text{S's border}\))。

更详细地说,在随机完 \(m\) 个字符前想要结束就必须找到一个 \(\text{border}\),在这个 \(\text{border}\) 的前面让它被其它字符补齐。

接下来把这个式子两边的分母去掉,得到 \(g_i=\sum\limits_{S_[1,t]\text{is S's border}}f_{i+t}n^t\)

发现这个式子中出现了 \(f,g\),尝试将其变成生成函数的形式。

  • 3.\(x^mG(x)=\sum\limits_{S_[1,t]\text{is S's border}}x^{m-t}F(x)n^t\)

这里我们要用到一个定理:两个多项式每一项系数相等,则两个多项式相等。这很显然,就不证了(也不用证吧?)。

那么实际上由于上面得到的等式对于每一个 \(i\) 都成立,因此观察上面的生成函数,会发现乘完之后恰好每一项都是一致的。

其实换种说法,两个元素(\(g_i,f_{i+t}\))的下标不同,将其转化为生成函数的时候 需要平移的距离也不同

如此由于系数一一对应相等,得到生成函数相等的式子,我们就可以将 \(x=1\) 代入进去,得到 \(G(1)=\sum\limits_{S_[1,t]\text{is S's border}}F(1)n^t\)

这个时候一开始被我们落下的 \(F(1)=1\) 派上用场了,上面的 \(F(1)\) 直接去掉,我们得到了我们所需求解的答案就是 \(\sum\limits_{S_[1,t]\text{is S's border}}n^t\)

这各位要是还不会就回幼儿园学一学识字吧(无慈悲)。

可以用KMP算出所有的 \(\text{border}\),也可以用字符串 \(\text{Hash}\) 乱搞,总之时间复杂度 \(\mathcal{O}(n)\),随便过,code


后记

看不见代码是不是很烦?