跳转至

排队上电梯

$n$个人排队上电梯,每一秒钟都有一个人站在电梯前,以$p$的概率上电梯,$1-p$的概率停滞不前。

求$t$秒钟之后,上电梯人数的期望值。

条件概率递推

显然,如果$t\le n$,一共只有$t$秒钟不够$n$个人都走完,那么每一秒都有人在电梯前等待。那么上电梯的人数就是一个二项分布$B(t, p)$,这时候上电梯的人数期望为:$pt$。

下面我们只考虑$t\gt n$的情况,这时候就不一定每一秒钟都有人在电梯前等待了。我们设随机变量$X_t$表示$t$秒钟上电梯的人数。

那么对于$1\le k \le n-1$显然有条件概率公式: $$ \begin{aligned} &P(X_t = k) \\ = &P(X_t = k \mid X_{t-1}=k)P(X_{t-1}=k) \\ &+ P(X_t = k \mid X_{t-1}=k-1)P(X_{t-1}=k-1)\\ = &(1-p)P(X_{t-1}=k) + pP(X_{t-1}=k-1) \end{aligned} $$

注意上述公式对$k=0,n$的情况不成立

$k=0$的情况比较简单: $$ P(X_t = 0) = (1-p)^t $$ $k=n$的情况较为特殊,因为后继无人,所以不需要$1-p$这个因子了: $$ P(X_t = n) = P(X_{t-1}=n) + pP(X_{t-1}=n-1) $$

代码

上面的条件概率公式用dp数组写出来就是:

n, p, t = input().split()
n = int(n)
p = float(p)
t = int(t)
if t <= n:
    print(t * p)
else:
    dp = [[0] * (n + 1) for _ in range(t + 1)]
    dp[0][0] = 1.0
    for i in range(1, t + 1):
        dp[i][0] = dp[i - 1][0] * (1 - p)
        for j in range(1, n):
            dp[i][j] = dp[i - 1][j - 1] * p + dp[i - 1][j] * (1 - p)
        dp[i][n] = dp[i - 1][n - 1] * p + dp[i - 1][n]
    # 求期望
    print(sum(k * dp[t][k] for k in range(n + 1)))

其中dp[i][j]代表$j$秒钟有$i$个人上电梯的概率。

直接求期望

利用前面的概率递推式我们可以求一求期望:

$$ \begin{aligned} &E(X_t)\\ = & \sum_{k=0}^n kP(X_t=k)\\ = & \sum_{k=1}^n kP(X_t=k)\\ = & \sum_{k=1}^n k\left[(1-p)P(X_{t-1}=k)+pP(X_{t-1}=k-1)\right] + npP(X_{t-1}=n)\\ = & {\color{red}(1-p)\sum_{k=1}^n kP(X_{t-1}=k)} + p\sum_{k=1}^n kP(X_{t-1}=k-1) + npP(X_{t-1}=n)\\ = & {\color{red}(1-p)E(X_{t-1})} + p\sum_{k=1}^n (k-1)P(X_{t-1}=k-1)+ npP(X_{t-1}=n)\\ &+ p\sum_{k=1}^n P(X_{t-1}=k-1)\\ = &(1-p)E(X_{t-1})+p\sum_{k=1}^{n+1} (k-1)P(X_{t-1}=k-1) + p\sum_{k=1}^n P(X_{t-1}=k-1)\\ = & (1-p)E(X_{t-1}) + pE(X_{t-1}) + p (1-P(X_{t-1}= n))\\ = & E(X_{t-1}) + p(1-P(X_{t-1}= n)) \end{aligned} $$

实际上,这个公式不必如此麻烦也可以得到。

不难发现: $$ X_t = X_{t-1} + \mathcal{1}(X_{t-1}\ne n) \mathcal{1}(Y=1) $$ 其中$Y$是参数为$p$的0-1随机变量,代表$t-1$时候之后的那一秒是否有人进入电梯, $\mathcal{1}(\cdot)$是示性函数。

两侧求期望即可得到: $$ E(X_t) = E(X_{t-1}) + E(\mathcal{1}(X_{t-1}\ne n) \mathcal{1}(Y=1)) $$

其中 $$ \begin{aligned} &E(\mathcal{1}(X_{t-1}\ne n) \mathcal{1}(Y=1)) \\ = &P(Y=1, X_{t-1}\ne n)\\ = &P(Y=1 \mid X_{t-1}\ne n)P(X_{t-1}\ne n)\\ = &p (1-P(X_{t-1}= n)) \end{aligned} $$

所以想要求$E(X_t)$只需要求$P(X_{t-1}= n)$即可:

$$ E(X_t) = E(X_{t-1}) +p (1-P(X_{t-1}= n)) $$

不断递推得到: $$ E(X_t) = p (1-P(X_{t-1}= n)) + p(1-P(X_{t-2}=n)) + \cdots + p (1-P(X_{1}= n)) + p (1-P(X_{0}= n)) + E(X_0) $$

所以: $$ E(X_t) = pt - p\sum_{i=0}^{t-1} P(X_{i}=n) $$

显然 $$ P(X_i=n) = 0 \quad \forall i\lt n $$ 所以: $$ E(X_t) = pt - p\sum_{i=n}^{t-1} P(X_{i}=n) $$

这里面: $$ P(X_i = n) = P(X_{i-1}=n) + pP(X_{i-1}=n-1) \quad i\ge n $$

涉及到了$P(X_{i-1}=n-1)$,比较难处理。如果递归求解那么计算量和第一种方法差不多。

所以我们考虑直接求解$P(X_i = n)$,也就是$i$秒钟$n$个人全部走掉的概率。

只需要条件于最后一个走掉的人的时间即可:

$$ P(X_i = n) = \sum_{s=0}^{i-n} C_{i-s-1}^{n-1} p^{n}(1-p)^{i-s-n} $$

前$i-s-1$秒走掉了$n-1$个人,第$i-s$秒走掉了最后一个人,后面的$s$秒无事发生。

双重求和代码

同样的,我们写出代码:

from math import comb
n, p, t = input().split()
n = int(n)
p = float(p)
t = int(t)
if t <= n:
    print(t * p)
else:
    print(
        t * p
        - p
        * sum(
            sum(
                comb(i - s - 1, n - 1) * p**n * (1 - p) ** (i - s - n)
                for s in range(i - n + 1)
            )
            for i in range(n, t)
        )
    )

不过这个代码慢得很。

单重求和

没关系,我们可以用一点点数学等价变形把双重求和变成单层: $$ E(X_t) = pt - p\sum_{k=0}^{d-1} (d-k)C_{n+k-1}^{n-1} p^{n}(1-p)^{k} $$ 其中$d=t-n$

咋变的?

我们的期望是

$$ E(X_t) = pt - p\sum_{i=n}^{t-1} \sum_{s=0}^{i-n} C_{i-s-1}^{n-1} p^{n}(1-p)^{i-s-n} $$

这里面有一个双重求和: $$ S = \sum_{i=n}^{t-1} \sum_{s=0}^{i-n} C_{i-s-1}^{n-1} p^{n}(1-p)^{i-s-n} $$

注意到后面的求和只和$i-s$相关。为了简化记号,我们做一些换元。

首先我们做变量代换:$d = t-n$,那么: $$ S = \sum_{i=n}^{n+d-1} \sum_{s=0}^{i-n} C_{i-s-1}^{n-1} p^{n}(1-p)^{i-s-n} $$

再令$j = i-n$得到: $$ S = \sum_{j=0}^{d-1} \sum_{s=0}^{j} C_{j+n-s-1}^{n-1} p^{n}(1-p)^{j-s} $$ 观察到后面的求和实际上只和$j-s$有关系,所以我们可以把双重求和变为关于$k = j-s$的单重求和。

$k$的取值为$0,1,\cdots, d-1$,重数分别为$d, d-1, \cdots, 1$。

重数的计算很简单,观察一下:

j 0 1 2 ... d-1
s 0 0,1 0,1,2 ... 0,1,...,d-1
j-s 0 1,0 2,1,0 ... d-1,d-2,...,0

就知道,$j-s=k$出现的次数是$d-k$

所以:

$$ S = \sum_{k=0}^{d-1} (d-k)C_{n+k-1}^{n-1} p^{n}(1-p)^{k} $$

这就把双重求和变成了单重。

from math import comb
n, p, t = input().split()
n = int(n)
p = float(p)
t = int(t)
if t <= n:
    print(t * p)
else:
    d = t - n
    print(
        t * p
        - p
        * sum(
            (d - k) * comb(n + k - 1, n-1) * p**n * (1 - p) ** k
            for k in range(d)
        )
    )

这下代码很快了,但是数字很大的时候会有OverflowError,一个超大的整数乘以浮点数是会有问题的,恼。

数值安全

但是没有关系,我们再加入一点点数值计算的小技巧,先取对数再做指数。这样就不会overflow了:

from math import comb, log, exp
n, p, t = input().split()
n = int(n)
p = float(p)
t = int(t)
if t <= n:
    print(t * p)
else:
    d = t - n
    # 要取对数了,所以特殊情况排除一下
    if p == 1:
        print(n)
    elif p == 0:
        print(0)
    else:
        print(
            t * p
            - p
            * sum(
                exp(
                    log(d - k)
                    + log(comb(n + k - 1, n - 1))
                    + n * log(p)
                    + k * log(1 - p)
                )
                for k in range(d)
            )
        )

AC

终于,dp算法和直接用组合数的解法都AC了😭

前者是常规算法,时间复杂度和空间复杂度还可以。后者没有空间复杂度,时间复杂度也很低✌️

该睡觉了😴


最后更新: 2025-05-29 02:42:18
创建日期: 2025-04-17 21:54:37

评论