神奇的概率生成函数——歌唱王国

· · 题解

神奇的概率生成函数——歌唱王国

题意

字符集大小为 n。有 t 组数据。

每一组数据包含一个长度为 m 的字符串 S。现在有一个空串 T,每一次在字符集内随机一个字符加入到 T 的末尾,当 ST 的子串时停止。求停止时 T 的期望长度。

做法简述

思路太清奇了,我也不知道怎么描述这个思路。这篇题解只能做到“尽量讲清楚这个做法”吧。

需要用到:生成函数基本知识(包括求导等)、KMP。

概率生成函数的性质

首先让我们认识一个叫概率生成函数的东西:f(x)=\sum_{i=0}^{+\infty} \mathrm{P}(X=i)x^i,也就是 i 次项的系数是随机变量 X 等于 i 的概率。

这个东西有什么用呢?这道题着重用到它的两个性质。

  1. 我们要求的是期望,和这个概率生成函数有什么关系呢?想一想,上面的式子想出现期望,是不是需要在 \mathrm{P}(X=i) 上乘一个 i 呀?如何让这个 i 出现呢?求导啊。于是这个式子也就可以理解了:f'(x)=\sum_{i=1}^{+\infty}i\mathrm{P}(X=i)x^{i-1}。为了全部加起来,我们令 x=1,得到 f'(1)=\sum_{i=1}^{+\infty}i\mathrm{P}(X=i)=\mathrm{E}(X)

总结一下,概率生成函数和它的导函数在 x=1 处的取值都很特殊,分别是 f(1)=1,f'(1)=\mathrm{E}(X)

引入概率生成函数

先设一个变量 Y 表示(任意一次随机的过程中)停止的时候 T 的长度,也就是随机了 Y 个字符的时候恰好随机出 S

引入两个概率生成函数 f(x)g(x),分别表示 Y=i 的概率和 Y>i 的概率。也就是 f(x)=\sum_{i=0}^{+\infty}\mathrm{P}(Y=i)x^ig(x)=\sum_{i=0}^{+\infty}\mathrm{P}(Y>i)x^i

方便起见,我们用 f_i 表示 f(x)x^i 项系数,g_i 同理。那么,f_i 就表示随机了 i 个字符恰好停止的概率,g_i 表示随机了 i 个字符还没有停止的概率。

第一个式子

接下来我们建立一个 fg 的递推式。

把这个式子整理成生成函数,那么就有 $xg(x)+1=f(x)+g(x)$。$+1$ 是处理边界项 $g_0=1$(因为不可能一个字符也没有就停止了,$Y$ 必定大于 $0$,因此 $g_0=1$)。 由于我们要求的是 $f'(1)$,因此上面的式子整理成 $f(x)=(x-1)g(x)+1$,再求导得 $f'(x)=g(x)+(x-1)g'(x)$,于是有 $f'(1)=g(1)$,这是不是非常好? ##### 第二个式子 接下来的过程就非常奇妙了。 我们设一个新的数列 $h_i\ (i\ge 0)$,表示同时满足以下两个条件(记为事件 $A$)的概率: 1. 随机了 $i$ 个字符还没有出现 $S
  1. 接着无条件随机 m 个字符(注意 mS 的长度),且这 m 个字符恰好是 S

什么叫“无条件”呢?就是随机这 m 个字符的过程中,有可能还没随机够 m 个,就已经出现 S 了。这种情况下我们强迫它一定要随机够 m 个。

相当于,h_i 表示的是这一个事件 A 的发生概率:

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

解释了 h_i 的含义,如何计算 h_i 呢?

第一种方法比较简单。上面两个条件是彼此独立的,因此可以把“随机了 i 个字符都没有停止”和“无条件随机 m 个字符,恰好得到 S”的概率乘起来,也就是 h_i=g_i n^{-m}(注意,n 是字符集大小)。

第二种方法比较神奇,运用了分类讨论(或者说全概率公式)。

我们注意到,如果事件 A 发生,那么第一次出现 S 的时刻(也就是上面定义的 Y)一定满足 i<Y\le i+mi+m 的时候一定已经出现了 S,所以 Y\le i+m)。因此我们讨论 Y 的每一个取值。

假设 Y=y,那么这个前提的概率就是 f_y。下面只需要求出在这个前提下事件 A 发生的概率,再把这个概率乘以 f_y 并全部加起来(根据全概率公式)就好了。

这个前提下事件 A 发生的概率是多少呢?

t=y-i\ (0<t\le m)。那么 y 时刻(也就是第一次出现 S 的时候),已经随机了这 m 个字符中的 t 个了。如果要发生事件 A,就得满足,这 t 个字符恰好是 S 的前 t 个(条件 a),并且后面再随机的 m-t 个字符恰好是 S 的剩余 m-t 个字符(条件 b)。

然而,由于 y 时刻恰好出现了 S,那也就意味着这 t 个新的字符也恰好是 S 的那个长度为 t 的后缀。也就是说,这 t 个字符是确定的。条件 a 能够满足,就等价于,这个后缀必须同时也是 S 的前缀——想到了什么?这就是说,这个后缀必须是 S 的 border!(注意,这里和 border 的一些定义有所不同的是,我们认为 S 本身也是 S 的一个 border。也就是说,只要一个串既是 S 的前缀,又是 S 的后缀,那么我们就把这个串叫做 S 的 border。)总结一下,条件 a 等价于这个长度为 t 的后缀是 S 的 border。

条件 b 是概率性的,剩下来的字符和前面的是独立的,因此这 m-t 个字符满足要求的概率就是 n^{-(m-t)}=n^{t-m}

因此,Y=y,\ t=y-i前提下,同时满足条件 a 和 b 的概率(也就是事件 A 发生的概率)是 \mathrm{isborder}(t)\times n^{t-m},其中 \mathrm{isborder}(t) 的取值为 10,表示长度为 t 的后缀是否是 S 的一个 border。

根据全概率公式,把所有可能情况的概率加起来,事件 A 发生的概率就是 \sum_{t=1}^{m}f_{i+t}n^{t-m}\mathrm{isborder}(t)(注意 t=y-i,\ y=i+t,因此 f_y=f_{i+t})。

上面的式子换一种写法就是 h_i=\sum_{S\text{的} t \text{长border}} f_{i+t}n^{t-m},这个求和号的意思是对所有 S 的 border 求和,求和的时候把这个 border 的长度带入到被求和式当中的 t 中。

整理一下,我们用两种方法计算 h_i,分别得到了 h_i=g_i n^{-m}h_i=\sum_{S\text{的} t \text{长border}} f_{i+t}n^{t-m},因此 g_i n^{-m}=\sum_{S\text{的} t \text{长border}} f_{i+t}n^{t-m}。这又是一个关于 gf 的式子!

上面这个式子两边乘以 n^m 得到 g_i=\sum_{S\text{的} t \text{长border}} f_{i+t}n^{t},看上去好多了。

接下来要把这个式子弄成生成函数的形式。注意到下标不一样,那肯定是移位了。

g(x) 乘以 x^m,第 i+m 项的系数变成了 g_i

f(x) 乘以 x^{m-t},第 i+m 项的系数变成了 f_{(i+m)-(m-t)}=f_{i+t}

因此,上面的式子整理成生成函数就是 x^mg(x)=\sum_{S\text{的} t \text{长border}} x^{m-t}f(x)n^{t}

得到答案

上面的式子看上去还是很丑是吧?别忘了我们只要求出 g(1) 就是答案了。

x=1 代入上面的式子得到 g(1)=\sum_{S\text{的} t \text{长border}} f(1)n^{t},又根据概率生成函数的性质,f(1)=1,因此 g(1)=\sum_{S\text{的} t \text{长border}} n^{t}。太神奇了!

根据这个式子,答案就是 \sum_{S\text{的} t \text{长border}} n^{t}。因此我们只需要找出 S 的所有 border,把 n^t 累加到答案里就可以了。这个可以用 KMP,或者由于我们可以枚举 border 长度,用哈希判断前后缀是否相等。

时间复杂度 O(m)

特别注意:如果用 KMP 求,一定记得这道题中 S 本身也算 S 的一个 border!

代码实现

式子推完了,代码应该非常容易写。提供我用 KMP 写的代码。

#include <cstdio>
#include <cctype>
#include <algorithm>
#define MAXIOLG 25
#define FILENAME(x) \
freopen(x".in","r",stdin); \
freopen(x".out","w",stdout);
#define MAXN 100005
#define MOD 10000
#define N 100000
#define MD(x) (((x)>=MOD)?((x)-=MOD):(0))
using namespace std;

typedef long long ll;
typedef long double ld;
typedef ll io_t;
io_t shin[MAXIOLG];
io_t seto(void); //快读,实现略去
void ayano(io_t x,char spliter='\n'); //快写,本题没用到

int spw[MAXN]; // 字符集大小(题目中的 n)的幂的预处理
int knxt[MAXN]; // KMP 的 next 数组
int arr[MAXN]; // 字符串

int main(void){
    int s,testdatas; //s 表示题目中的 n
    s=seto()%MOD,testdatas=seto();
    spw[0]=1;
    for (int i=1;i<=N;i++)
        spw[i]=spw[i-1]*s%MOD;
    while (testdatas--){
        int n=seto(); //表示题目中的 m
        // KMP 计算 next
        knxt[0]=-1;
        for (int i=1;i<=n;i++){
            int cur=seto();
            arr[i]=cur;
            int tnxt=knxt[i-1];
            while (tnxt>=0){
                if (arr[tnxt+1]==cur)
                    break;
                tnxt=knxt[tnxt];
            }
            knxt[i]=tnxt+1;
        }
        int t=n;
        int ans=0;
        // 在 next 数组上跳,枚举每一个 border
        while (t)
            ans+=spw[t],MD(ans),t=knxt[t];
        //输出答案
        putchar(ans/1000+'0'),putchar(ans/100%10+'0'),
        putchar(ans/10%10+'0'),putchar(ans%10+'0'),putchar('\n');
    }
    return 0;
}