概率期望杂记

概率期望杂记

概率

定义:

  • 样本点:随机现象中可能发生的不能再细分的结果。
  • 样本空间 \(\Omega\) :全体样本点构成的集合。
  • 随机事件:若干个样本点构成的集合,是样本空间 \(\Omega\) 的一个子集。
  • 随机事件的运算:并(和)、交(积)、差、补。
  • 随机事件域:样本空间 \(\Omega\) 的某些子集所组成的子集类,满足事件关于并、交、差、补封闭。

定义概率为每一个事件发生的可能性大小。若一个随机现象,其样本空间有限,且每个样本点出现的可能性是一样的,则对于事件 \(A\) ,有 \(P(A) = \frac{n(A)}{n(\Omega)}\)

概率的简单性质:

  • \(P(A) \in [0, 1]\)
  • \(P(\Omega) = 1\)\(P(\empty) = 0\)
  • \(P(A) + P(\overline{A}) = 1\)
  • \(A \subseteq B\) ,则 \(P(A) \leq P(B)\)
  • \(P(A + B) = P(A) + P(B) - P(AB)\)
  • \(P(A \setminus B) = P(A) - P(AB)\)

定义条件概率为事件 \(B\) 发生的情况下事件 \(A\) 发生的概率,记为 \(P(A \mid B)\)

条件概率的性质:

  • 概率乘法公式:

    \[P(AB) = P(A) P(B \mid A) = P(B) P(A \mid B) \]

  • 全概率公式:若一组事件 \(A_{1 \sim n}\)\(\Omega\) 的不交覆盖,则:

    \[P(B) = \sum_{i = 1}^n P(A_i) P(B \mid A_i) \]

    一般来说,设可能导致事件 \(B\) 发生的原因为 \(A_{1 \sim n}\) ,则在 \(P(A_i)\)\(P(B \mid A_i)\) 已知时可以通过全概率公式计算事件 \(B\) 发生的概率。

  • 贝叶斯公式:若一组事件 \(A_{1 \sim n}\)\(\Omega\) 的不交覆盖,则:

    \[P(A_i \mid B) = \frac{P(A_i B)}{P(B)} = \frac{P(A_i B)}{\sum_{i = 1}^n P(A_i) P(B \mid A_i)} \]

    一般来说,设可能导致事件 \(B\) 发生的原因为 \(A_{1 \sim n}\) ,则贝叶斯公式通常用于反推 \(B\) 的各个原因事件的发生概率。

定义独立事件:

  • 若事件 \(A, B\) 满足 \(P(AB) = P(A) P(B)\) ,则称 \(A, B\) 独立。

  • 扩展到多个事件 \(A_{1 \sim n}\) 的情况,称这些事件独立当且仅当对于其任意子集 \(\{ A_{i_k} : 1 \leq i_1 < i_2 < \cdots < i_k \leq n \}\) 均满足 \(P(A_{i_1} A_{i_2} \cdots A_{i_k}) = \prod_{k = 1}^r P(A_{i_k})\)

期望

定义:对于随机变量 \(X\) ,有 \(E(X) = \sum_x P(X = x) \times x\)

性质:

  • 对于随机变量 \(X\) ,有 \(E(cX) = c E(X)\)
  • 若随机变量 \(X, Y\) 独立,则 \(E(XY) = E(X) E(Y)\)
  • 对于随机变量 \(X, Y\) ,有 \(E(X + Y) = E(X) + E(Y)\)

条件期望:在随机变量 \(Y = y\) 的情况下随机变量 \(X\) 的期望,记作 \(E(X \mid Y = y)\)

全期望公式:\(E(X) = \sum_y P(Y = y) \times E(X \mid Y = y)\)

Tricks

  • \(n\)\([0, 1]\) 之间的随机变量 \(x_{1 \sim n}\)\(k\) 小值期望为 \(\frac{k}{n + 1}\)
  • 每次等概率随机从 \(1 \sim n\) 中删除数字,\(m\) 轮后 \(i\) 仍然存在的概率为 \(\frac{n - m}{n}\)
  • 前缀最大值相关。
    • 对于随机排列 \(p\)\(p_i\) 作为前缀最大值的概率为 \(\frac{1}{i}\)
    • 对于随机排列 \(p\) ,前缀最大值的期望个数为 \(O(\ln n)\)
  • 生日悖论:在 \([1, n]\) 中随机选数,选出两个相同数的期望轮数为 \(2 \sqrt{n}\)
  • 每种情况出现概率相同时,可以转化为求所有情况的贡献和,最后除以总方案数。
  • 一些前缀/差分类型的转化往往能够简化问题。
    • \(E(X) = \sum_i P(X = i) \times i = \sum_{i = 1}^{+\infty} P(X \geq i)\)
  • 利用期望的线性性,期望操作次数可以转化为每个操作被操作的概率。
    • 一些题目中具有“达成某条件后操作停止”的限制,此时可以求某个操作不为最后一次操作的概率,最后将答案 \(+1\) ,注意此时需要判定无需操作的情况。
  • 允许操作非法操作,但是不统计其贡献。
  • 等概率删点问题可以随机一个排列,然后按顺序删点,忽略无效操作,需要满足可删点集是不断减少的。

应用

P6835 [Cnoi2020] 线形生物

有一个人要从 \(1\) 走到 \(n + 1\) ,有 \(n\)\(i \to i + 1\) 的边,以及 \(m\) 条返祖边 \(u_i \to v_i (u_i \geq v_i)\) 。每次这个人等概率选择一条出边行动,求走到 \(n + 1\) 的期望步数。

\(n, m \leq 10^6\)

根据期望的线性性,有:

\[E(x \to y) = E(x \to x + 1) + E(x + 1 \to x + 2) + \cdots + E(y - 1 \to y) \]

因此 \(E(1 \to n + 1) = E(1 \to 2) + E(2 \to 3) + \cdots + E(n \to n + 1)\) ,问题转化为求 \(E(x \to x + 1)\) ,设 \(x\) 的度数为 \(d_x\) ,按定义列出式子:

\[E(x \to x + 1) = 1 + \frac{1}{d_x} \sum_{(x, y), y \neq x} E(y \to x + 1) \]

\(f_x = E(x \to x + 1), s_x = \sum_{i \leq x} f_i\) ,则:

\[f_x = 1 + \frac{1}{d_x} \sum_{(x, y), y \neq x} (s_x - s_{y - 1}) \\ \frac{1}{d_x} f_x = 1 - \frac{1}{d_x} \sum_{(x, y), y \neq x} (s_{x - 1} - s_{y - 1}) \\ f_x = d_x - \sum_{(x, y), y \neq x} (s_{x - 1} - s_{y - 1}) \]

不难前缀和优化转移做到线性。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e6 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int f[N], s[N];

int testid, n, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%d%d%d", &testid, &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v);
    }

    for (int u = 1; u <= n; ++u) {
        f[u] = G.e[u].size() + 1;

        for (int v : G.e[u])
            f[u] = add(f[u], dec(s[u - 1], s[v - 1]));

        s[u] = add(s[u - 1], f[u]);
    }
    
    printf("%d", s[n]);
    return 0;
}

[AGC020F] Arcs on a Circle

有一个周长为 \(C\) 的圆,有 \(n\) 条弧,第 \(i\) 条长度为 \(l_i\)

每一条弧 \(i\) 均匀随机地放置在圆上:选择圆上的一个随机实点,然后放置一条以该点为中心的长度为 \(l_i\) 的弧。

求圆上所有实点至少被一个弧覆盖的概率,注意一条弧覆盖了它的两个端点。

\(n \leq 6\)\(C \leq 50\)\(1 \leq l_i < C\)

由于坐标可以为实数,不好放入 DP 状态中。而每条弧的长度均为整数,则 \(A\) 能通过长 \(l\) 的弧覆盖到 \(B\) 当且仅当 \(\lfloor B \rfloor - \lfloor A \rfloor < l\)\(\lfloor B \rfloor - \lfloor A \rfloor = l \and (B - \lfloor B \rfloor) \leq (A - \lfloor A \rfloor)\)

由于每条弧的端点的小数部分是 \([0, 1)\) 之间的随机变量,因此两条弧端点小数部分相同的概率为 \(0\)

考虑暴力枚举弧的端点的小数部分之间的相对大小,一共 \(n!\) 种,算出它们的答案之后除以 \(n!\) 即可。

这样就可以离散化坐标了,一共有 \(nc\) 个坐标。考虑断环为链,覆盖到大于 \(C\) 的部分就忽略。选择最长的弧的端点作为链的起点,这样就不会出现一个更长的弧绕到前面一个弧覆盖不到的地方。

由于弧的数量较少,考虑状压 DP。设 \(f_{i, j, s}\) 表示左端点坐标不大于 \(i\) 的弧已经放置完毕,覆盖到的最大的前缀为 \([1, j]\) ,弧放置情况为 \(s\) 的方案数,最后除以总方案数 \(c^{n - 1}\) 即可。转移时枚举是否以 \(i\) 为左端点放置弧,由于限定了小数部分的相对大小,每一处可以放置的弧只有一条,故可以做到 \(O(1)\) 转移。

时间复杂度 \(O(n! \times 2^n \times (nc)^2)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 7, C = 51;

double f[N * C][1 << N];
int l[N], p[N];

int n, c;

signed main() {
    scanf("%d%d", &n, &c);
    
    for (int i = 0; i < n; ++i)
        scanf("%d", l + i);
    
    sort(l, l + n, greater<int>()), iota(p, p + n, 0);
    double ans = 0, cnt = 0;
    
    do {
        memset(f, 0, sizeof(f)), f[l[p[0]] * n][1] = 1;
        
        for (int i = 1; i < n * c; ++i)
            for (int j = i, k = i % n; j <= n * c; ++j)
                for (int s = 0; s < (1 << n); ++s)
                    if (~s >> k & 1)
                        f[min(n * c, max(j, i + l[p[k]] * n))][s | (1 << k)] += f[j][s];
        
        ans += f[n * c][(1 << n) - 1], ++cnt;
    } while (next_permutation(p + 1, p + n));
    
    printf("%.16lf", ans / cnt / pow(c, n - 1));
    return 0;
}

[ARC150D] Removing Gacha

给定一棵树,每次从满足 \(u\)\(u\) 的任意祖先未被染色的 \(u\) 中等概率选择一个点染色(可能会重复染色),求将所有点染色的期望操作次数。

\(n \leq 2 \times 10^5\)

不难发现 \(u\) 是否能被选中仅与 \(1 \to u\) 这条链上的染色情况有关,若这条链均被染色,则 \(u\) 无法被选中。

\(E(X_i)\) 表示 \(i\) 期望被选中的次数,则答案为 \(\sum_{i = 1}^n E(X_i)\)

考虑操作允许选中不能选的点,但是不统计其贡献。问题转化为每次从这条链上随机选一个点,选到 \(u\) 则贡献 \(+1\) ,求选过所有点时的期望贡献和。

假设这条长为 \(d\) 的链有 \(i\) 个点被染色,则选中一个未被染色的点的概率为 \(\frac{d - i}{d}\) ,因此期望操作 \(\frac{d}{d - i}\) 次染色一个未染色的点,因此所有点都被染色的期望操作次数为 \(\sum_{i = 0}^{d - 1} \frac{d}{d - i} = d \times \sum_{i = 1}^d \frac{1}{i}\) 。而每次选到 \(u\) 的概率为 \(\frac{1}{d}\) ,因此 \(u\) 期望被选中的操作次数即为 \(\sum_{i = 1}^d \frac{1}{i}\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int inv[N], dep[N];

int n, ans;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

void dfs(int u, int d, int s) {
    ans = add(ans, s);

    for (int v : G.e[u])
        dfs(v, d + 1, add(s, inv[d + 1]));
}

signed main() {
    scanf("%d", &n);

    for (int i = 2; i <= n; ++i) {
        int f;
        scanf("%d", &f);
        G.insert(f, i);
    }

    inv[0] = inv[1] = 1;

    for (int i = 2; i <= n; ++i)
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;

    dfs(1, 1, 1);
    printf("%d", ans);
    return 0;
}

CF1924E Paper Cutting Again

有一个 \(n \times m\) 的矩形,左下角为 \((0, 0)\) ,右上角为 \((n, m)\) ,每次均匀随机选择一条平行于坐标轴、经过的点坐标均为整数、穿过(不能在边界上)矩形的直线,并删去其上方或右方的部分。

求剩余面积 \(< k\) 的期望操作次数。

多测,\(\sum n, \sum m \leq 10^6\)

先将 \(k - 1\) 转化为剩余面积 \(\leq k\) 的期望操作次数,特判掉 \(nm \leq k\) 的情况,此时答案为 \(0\)

考虑计算每条直线不是最后一次被选的概率,相当于有一个 \(1 \sim n + m - 2\) 的排列,直线 \(x = i\) 会被选当且仅当其排在直线 \(x = 1 \sim i - 1\) 与直线 \(y = 1 \sim \lfloor \frac{k}{i} \rfloor\) 之前。记这样的直线有 \(q\) 条,则 \(x = i\) 排在这些直线之前的概率为 \(\frac{1}{q + 1}\)\(y = i\) 被选取的概率是类似的计算。

时间复杂度 \(O(n + m)\)

类似的题目:[ARC114E] Paper Cutting 2

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2e6 + 7;

int inv[N];

ll k;
int n, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

signed main() {
    inv[0] = inv[1] = 1;
    
    for (int i = 2; i < N; ++i)
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;

    int T;
    scanf("%d", &T);
    
    while (T--) {
        scanf("%d%d%lld", &n, &m, &k), --k;
        
        if (1ll * n * m <= k) {
            puts("0");
            continue;
        }
        
        int ans = 1;
        
        for (int i = 1; i < n; ++i)
            if (k / i < m)
                ans = add(ans, inv[i + k / i]);
        
        for (int i = 1; i < m; ++i)
            if (k / i < n)
                ans = add(ans, inv[i + k / i]);
        
        printf("%d\n", ans);
    }
    
    return 0;
}

[ARC165E] Random Isolation

给出一棵树和常数 \(k\) ,每次等概率删掉一个所在连通块大小 \(> k\) 的点以及其所连的边,求直至无法删除时的期望操作次数。

\(n \leq 100\)

考虑将操作转化为:随机一个排列,然后按顺序删点,忽略无效操作。

考虑将点的贡献放到连通块上,在最浅处统计连通块的出现概率,因为其若出现则一定会被删去一个点被破坏掉。对于一个 \(u\) 为根、大小为 \(i\) 的连通块,其能产生贡献当且仅当相邻的 \(j\) 个点全在这 \(i\) 个点之前操作,概率为 \(\frac{i! j!}{(i + j)!}\)

\(f_{u, i, j}\) 表示 \(u\) 为根、连通块大小为 \(i\) 、除了父亲以外相邻 \(j\) 个的方案数,转移即为树上背包,不难做到 \(O(n^4)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e2 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int f[N][N][N], g[N][N];
int fac[N], inv[N], invfac[N], siz[N];

int n, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

void dfs(int u, int fa) {
    siz[u] = 1, f[u][1][0] = 1;

    for (int v : G.e[u]) {
        if (v == fa)
            continue;

        dfs(v, u);

        for (int i = 0; i <= siz[u] + siz[v]; ++i)
            for (int j = 0; j <= siz[u] + siz[v]; ++j)
                g[i][j] = 0;

        for (int i = 0; i <= siz[u]; ++i)
            for (int j = 0; j <= siz[u]; ++j) {
                for (int p = 0; p <= siz[v]; ++p)
                    for (int q = 0; q <= siz[v]; ++q)
                        g[i + p][j + q] = add(g[i + p][j + q], 1ll * f[u][i][j] * f[v][p][q] % Mod);

                g[i][j + 1] = add(g[i][j + 1], f[u][i][j]);
            }

        siz[u] += siz[v];

        for (int i = 0; i <= siz[u]; ++i)
            for (int j = 0; j <= siz[u]; ++j)
                f[u][i][j] = g[i][j];
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    dfs(1, 0), prework(n);
    int ans = 0;

    for (int i = 1; i <= n; ++i)
        for (int j = m + 1; j <= siz[i]; ++j)
            for (int k = 0; k <= siz[i] - j; ++k)
                ans = add(ans, 1ll * f[i][j][k] * fac[j] % Mod * fac[k + (i != 1)] % Mod * invfac[j + k + (i != 1)] % Mod);

    printf("%d", ans);
    return 0;
}

CF1267G Game Relics

\(n\) 种物品,第 \(i\) 个圣物的价格为 \(c_i\) ,有两种购买方式:

  • 花费 \(c_i\) 元购买第 \(i\) 种物品。
  • 花费 \(x\) 元进行抽奖(\(x\) 为给定常数),可以等概率随机获得这 \(n\) 种物品中的一种,如果获得了已经获得过的物品,则会退还 \(\frac{x}{2}\) 元。

求最优策略下买到 \(n\) 种物品的期望最小花费。

\(n \leq 100\)\(x, \sum c \leq 10^4\)\(x \leq c_i\)

由于 \(x \le c_i\) ,显然最优策略一定是先抽奖,然后全买。

不妨将第一种购买方式转化为:随机买一件未拥有的物品,花费为该物品的花费。不难发现最优策略下两种购买方式是等价的。

由于每次抽奖是随机的,假设抽奖获得了 \(i\) 件物品,则这 \(i\) 件物品一定是等概率从全集中选出一个大小为 \(i\) 的子集。

考虑从已获得 \(i\) 件物品的答案转移到已获得 \(i + 1\) 件物品的答案,分类讨论购买方式:

  • 若直接购买,设剩余价格和为 \(c\) ,则期望花费为 \(\frac{c}{n - i}\)
  • 若使用抽奖操作,则期望花费为 \((\frac{n}{n - i} + 1) \times \frac{x}{2}\)

\(f_{i, j}\) 表示抽奖出 \(i\) 件物品,价格和为 \(j\) 的概率,不难 DP 求出。设 \(F_i\) 表示选了 \(i\) 件物品后再选一件的期望花费,答案即为 \(\sum_{i = 0}^{n - 1} F_i\) ,其中 \(F_i = \sum_{j = 0}^{\sum c} f_{i, j} \times \min ( ( \frac{n}{n - i} + 1 ) \times \frac{x}{2}, \frac{(\sum c) - j}{n - i} )\)

时间复杂度 \(O(n^2 \sum c)\)

#include <bits/stdc++.h>
typedef long double ld;
using namespace std;
const int N = 1e2 + 7, V = 1e4 + 7;

ld f[N][V];
int a[N];

int n, m, s;

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), s += a[i];

    f[0][0] = 1;

    for (int i = 1; i <= n; ++i)
        for (int j = i; j; --j)
            for (int k = a[i]; k <= s; ++k)
                f[j][k] += f[j - 1][k - a[i]] * j / (n - j + 1);

    ld ans = 0;

    for (int i = 0; i < n; ++i)
        for (int j = 0; j <= s; ++j)
            ans += f[i][j] * min(((ld)n / (n - i) + 1) * m / 2, (ld)(s - j) / (n - i));

    printf("%.10LF", ans);
    return 0;
}

QOJ5377. N 门问题

\(n\) 扇门,有且仅有一扇门后面后面有奖,主持人事先知道有奖的门。

当 A 选择一扇门后,若关着的门数量为 \(2\) ,则立刻结算结果,否则主持人打开一扇没有奖且 A 没选的门。

A 的策略为:通过已知信息,每次选择有奖概率最高的那扇门。注意在 A 的视角,A 认为主持人每次打开的门是等概率随机的。

若主持人采用最佳策略,求 A 结算时选中正确的门的概率。

\(n \leq 10^{18}\)

首先可以发现,无论 A 选择那一扇门,在 B 打开一扇门后,A 认为当前所选择的门有奖的概率变为所有门中唯一最小的。

证明:使用归纳法和反证法。考虑反证法,假设第 \(n\) 次被选择的门在开门后没有变为唯一最小,不妨设 A 选择的门是第 \(n - 1\) 次开门后概率最高的门。且根据假设,其相比第 \(n - 1\) 次开门后概率最小的门,主持人打开门后,前者仍比后者概率大。

考虑归纳法,后者一定是“第 \(n - 2\) 次开门后概率最大的门”,故在第 \(n - 2\) 次开门之后的时刻,后者的概率一定大于等于前者。

考虑从这个时刻起用式子来表示 A 认为这两扇门后有奖的概率经过两次开门行为后的变化,根据贝叶斯公式 \(P(A | B) = \frac{P(B | A) P(A)}{P(B)}\) ,可以发现二者的分母是一样的,而后者分子的系数比前者大。故两次开门后,后者概率理应大于前者概率,矛盾。

那么原问题可以转化为:有 \(n\) 个球,其中有一个是正确的。每次 A 选序列开头的球,然后 B 可以丢弃剩下的非正确球中的任意一个,然后 A 把自己刚刚选的球放到序列末尾。

可以发现 \(n\) 足够大时,B 可以在 \(\frac{n}{2}\) 次操作后控制奇偶性,导致 A 必败,考虑找到 \(n\) 的界。

写一个暴搜发现 \(n \geq 11\) 时 A 必败,因此只要打表 \(n \leq 10\) 即可做到 \(O(1)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const double ans[] = {0, 1, 1.0 / 2, 2.0 / 3, 5.0 / 8, 7.0 / 15, 5.0 / 12, 12.0 / 35, 
    14.0 / 48, 16.0 / 63, 18.0 / 80};

int n;

ll exgcd(ll a, ll b, ll &x, ll &y) {
    if (b) {
        ll g = exgcd(b, a % b, y, x);
        return y -= a / b * x, g;
    } else
        return x = 1, y = 0, a;
}

inline ll mul(ll a, ll b, ll p) {
    ll c = (long double) a / p * b;
    return (1ull* a * b - 1ull * c * p + p) % p;
}

inline pair<ll, ll> merge(pair<ll, ll> a, pair<ll, ll> b) {
    ll a1 = a.first, m1 = a.second, a2 = b.first, m2 = b.second,
        x, y, g = exgcd(m1, m2, x, y), m = m1 / g * m2, c = ((a2 - a1) % m + m) % m;
    return c % g ? make_pair(-1ll, -1ll) : make_pair((a1 + mul(mul(x, c / g, m), m1, m)) % m, m);
}

signed main() {
    scanf("%d", &n);
    pair<ll, ll> res;

    for (int i = 1; i <= n; ++i) {
        pair<ll, ll> now;
        scanf("%lld%lld", &now.first, &now.second);

        if (i == 1)
            res = now;
        else {
            res = merge(res, now);

            if (res.first == -1)
                return puts("error"), 0;
        }
    }

    if (res.first < 2)
        puts("error");
    else if (res.first > 10)
        puts("0.000000");
    else
        printf("%lf", ans[res.first]);

    return 0;
}

SP4060 KPGAME - A game with probability

\(n\) 个石子,两个人抛硬币,抛到正面则拿走一个石子,否则什么都不做。拿走最后一个石子的人胜利。

若双方都采用最优方案,两人抛到自己想要的那一面的概率分别为 \(p, q\) ,求先手的获胜概率。

\(n \leq 10^8\)\(p, q \in [0.5, 1)\) ,输出保留六位小数

\(f_{i, 0/1}\) 表示在投硬币前还剩 \(i\) 个石头,且现在是先/后手抛硬币,抛完硬币后先手的胜率。

初始状态为 \(f_{0, 0} = 0, f_{0, 1} = 1\) ,然后显然有两个分类讨论:

  • 若当前局面下取走石子更优,则:

    \[f_{i, 0} = p \times f_{i - 1, 1} + (1 - p) \times f_{i, 1} \\ f_{i, 1} = q \times f_{i - 1, 0} + (1 - q) \times f_{i, 0} \]

    解得:

    \[f_{i, 0} = \dfrac{p \times f_{i - 1, 1} + (1 - p) \times q \times f_{i - 1, 0}}{1 - (1 - p) \times (1 - q)} \\ f_{i, 1} = \dfrac{q \times f_{i - 1, 0} + (1 - q) \times p \times f_{i - 1, 1}}{1 - (1 - q) \times (1 - p)} \]

  • 若当前局面下不取石子更优,则:

    \[f_{i, 0} = p \times f_{i, 1} + (1 - p) \times f_{i - 1, 1} \\ f_{i, 1} = q \times f_{i, 0} + (1 - q) \times f_{i - 1, 0} \]

    解得:

    \[f_{i, 0} = \dfrac{p \times (1 - q) \times f_{i - 1, 0} + (1 - p) \times f_{i - 1, 1}}{1 - p \times q} \\ f_{i, 1} = \dfrac{q \times (1 - p) \times f_{i - 1, 1} + (1 - q) \times f_{i - 1, 0}}{1 - q \times p} \]

下面考虑决策,若从 \(i\) 个石头的状态转移到了 \(i - 1\) 个石头的状态,那么一定是某个人拿走了一个石子。若 \(f_{i - 1, 1} > f_{i - 1, 0}\) ,则当前玩家肯定希望自己取走石子,反之当前玩家肯定希望自己取走石子。因此只要比较 \(f_{i - 1, 0}\)\(f_{i - 1, 1}\) 即可决策。

但是这样时间复杂度 \(O(n)\) 不够优秀,注意到 \(n \geq 1000\) 时保留六位小数后的答案不变,因此只要令 \(n = \min(n, 1000)\) 即可。

#include <bits/stdc++.h>
using namespace std;

double p, q, f[2][2];
int T, n;

signed main() {
    scanf("%d", &T);

    while (T--) {
        scanf("%d%lf%lf", &n, &p, &q);
        n = min(n, 1000), f[0][0] = 0, f[0][1] = 1;

        for (int i = 1; i <= n; ++i) {
            if (f[~i & 1][1] > f[~i & 1][0]) {
                f[i & 1][0] = (p * f[~i & 1][1] + (1 - p) * q * f[~i & 1][0]) / (1 - (1 - p) * (1 - q));
                f[i & 1][1] = (q * f[~i & 1][0] + (1 - q) * p * f[~i & 1][1]) / (1 - (1 - q) * (1 - p));
            } else {
                f[i & 1][0] = (p * (1 - q) * f[~i & 1][0] + (1 - p) * f[~i & 1][1]) / (1 - p * q);
                f[i & 1][1] = (q * (1 - p) * f[~i & 1][1] + (1 - q) * f[~i & 1][0]) / (1 - q * p);
            }
        }

        printf("%.6lf\n", f[n & 1][0]);
    }

    return 0;
}

QOJ7754. Rolling For Days

\(m\) 种卡牌,第 \(i\) 种初始在牌堆中有 \(a_i\) 张,希望得到 \(b_i\) 张,牌的总数 \(n = \sum_{i = 1}^m a_i\)

每次抽卡会在目前牌堆的所有牌中随机抽取一张,若拥有的该类的张数已经达到 \(b_i\) 则放回牌堆,否则不放回。

求达到目标的期望抽卡次数。

\(n \leq 1000\)\(m \leq 12\)

发现放回这个操作导致每次抽卡的概率不好直接计算,而每次放回的牌下次再被抽取是一定仍会放回,因此考虑每次一直拿到一张没拿过的牌为止。

具体的,将抽卡转化为:有两个初始为空的牌堆 \(C, D\),每次从还没抽出的牌中随机抽一张,如果目前 \(C\) 中该种类卡牌达到 \(b_i\) 则放到 \(D\) 中,否则加入 \(C\) 中。每次抽卡对期望步数的贡献为 \(\frac{n - |C|}{n - |C| - |D|}\)

根据期望的线性性,转化为对于每个 \((|C|, |D|)\) 计算出现概率。设 \(f_{i, j, k}\) 表示前 \(i\) 个数、选了 \(j\) 个放在 \(C\) 中、选了 \(k\) 个放在 \(D\) 中的贡献和。转移时枚举第 \(i\) 选出 \(x\) 个,分配多少给 \(j\)\(k\) 即可,时间复杂度 \(O(n^3)\)

可以优化,由于 \(\frac{n - |C|}{n - |C| - |D|} = \frac{n}{n - |C| - |D|} - \frac{|C|}{n - |C| - |D|}\) ,因此考虑设:

  • \(f_{i, j, 0/1}\) 表示前 \(i\) 个数、选了 \(j\) 个放在 \(C \cup D\) 中、是否存在一种牌未达成目标时的 \(1\) 的贡献和(方案数)。
  • \(g_{i, j, 0/1}\) 表示前 \(i\) 个数、选了 \(j\) 个放在 \(C \cup D\) 中、是否存在一种牌未达成目标时 \(|C|\) 的贡献和。

不难做到 \(O(n^2)\) 转移,答案即为:

\[\sum_{i = 0}^n \frac{f_{m, i, 1} \times n - g_{m, i, 1}}{n - i} \times \frac{i! \times (n - i)!}{n!} \]

其中后者为概率,因为前 \(i\) 张牌和后 \(n - i\) 张牌可以任意排列。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e3 + 7;
 
int fac[N], inv[N], invfac[N];
int a[N], b[N], f[N][N][2], g[N][N][2];
 
int m, n;
 
inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}
 
inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}
 
inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}
 
inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}
 
signed main() {
    scanf("%d%d", &n, &m);
 
    for (int i = 1; i <= m; ++i)
        scanf("%d", a + i);
 
    for (int i = 1; i <= m; ++i)
        scanf("%d", b + i);
 
    prework(n), f[0][0][0] = 1;
 
    for (int i = 1, sa = 0; i <= n; sa += a[i++])
        for (int j = 0; j <= sa; ++j)
            for (int k = 0; k <= a[i]; ++k)
                for (int l = 0; l <= 1; ++l) {
                    f[i][j + k][l | (k < b[i])] = add(f[i][j + k][l | (k < b[i])], 
                        1ll * f[i - 1][j][l] * C(a[i], k) % Mod);
                    g[i][j + k][l | (k < b[i])] = add(g[i][j + k][l | (k < b[i])], 
                        1ll * add(1ll * f[i - 1][j][l] * min(k, b[i]) % Mod, g[i - 1][j][l]) * C(a[i], k) % Mod);
                }
 
    int ans = 0;
 
    for (int i = 0; i <= n; ++i)
        ans = add(ans, 1ll * dec(1ll * f[m][i][1] * n % Mod, g[m][i][1]) * inv[n - i] % Mod * 
            fac[i] % Mod * fac[n - i] % Mod * invfac[n] % Mod);
 
    printf("%d", ans);
    return 0;
}

P12061 [THUPC 2025 决赛] 喜爱之钥

\(l\) 个锁和 \(l + k\) 把钥匙,其中有 \(l\) 把真钥匙与锁一一对应,剩下 \(k\) 把假钥匙打不开任何锁。

\(n\) 个人,\(1 \sim n\) 轮流循环尝试开锁,每次会尝试未尝试的成功概率最大的钥匙-锁组合。

求所有锁都被打开时每个人成功开锁次数的期望 \(\bmod {10^9 + 7}\)

\(n, k \leq 50\)\(l \leq 5000\)

考虑添加 \(m\) 把假锁,与假钥匙一一对应,则每个 \(1 \sim n + m\) 的排列 \(p\) 都描述一个锁-钥匙的匹配关系。

初始时任意一把锁打开任意一把钥匙的概率均为 \(\frac{1}{n + m}\) ,若第一个人开锁失败,则说明 \(p_1 \neq 1\) ,考虑第二个人的决策:

  • 用第 \(x\) 把钥匙开第一把锁:成功概率为 \(P(p_1 = x) = \frac{1}{n + m - 1}\) ,有 \(n + m - 1\) 种选择方案。
  • 用第一把钥匙开第 \(y\) 把锁:成功概率为 \(P(p_y = 1) = \frac{1}{n + m - 1}\) ,有 \(n - 1\) 种选择方案。
  • 用第 \(x\) 把钥匙开第 \(y\) 把锁:成功概率为 \(P(p_y = x) = \frac{n + m - 2}{(n + m - 1)^2}\) ,显然不优。

若第二个人执行某策略失败,为了利用失败的信息,则后面的人都会采取相同的策略,直至成功打开锁为止。第一个人失败时第二个人的成功概率为 \(\frac{n + m - 1}{n + m} \times \frac{1}{n + m - 1} = \frac{1}{n + m}\) ,以此类推得到每个人成功的概率相等。

考虑一次成功的配对对后续操作的影响,由于每次都是多对一的尝试,因此不会给后面的操作留下有效信息,从而可以划分子问题处理。

\(f_{n, m, i}\) 表示剩余 \(n\) 个锁和 \(n + m\) 把钥匙时第 \(i\) 个人的期望成功次数,考虑转移:

  • 成功找到一对匹配关系:从 \(f_{n - 1, m, i - j}\) 转移而来,其中 \(i - j\) 为模 \(n\) 意义下的值。若选钥匙则 \(j \leq n\) ,若选锁则 \(j \leq n + m\) 。注意需要额外统计一下第 \(i\) 个人处匹配成功的概率。
  • 成功找到一个假钥匙:从 \(f_{n, m - 1, i - n}\) 转移而来。

前缀和优化转移即可做到 \(O(nlk)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e1 + 7, M = 5e3 + 7;

int inv[M * 2 + N], f[M][N][N];

int n, l, k;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%d%d%d", &n, &l, &k);
    inv[0] = inv[1] = 1;

    for (int i = 2; i <= l * 2 + k; ++i)
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;

    for (int a = 1; a <= l; ++a)
        for (int b = 0; b <= k; ++b) {
            int s1 = 0, s2 = 0;

            for (int i = 0; i < n; ++i) {
                s1 = add(s1, 1ll * (a / n + (i >= n - a % n)) * f[a - 1][b][i] % Mod);
                s2 = add(s2, 1ll * ((a + b) / n + (i >= n - (a + b) % n)) * f[a - 1][b][i] % Mod);
            }

            int p1 = (a == 1 && !b ? inv[a + b] : 
                    1ll * (a - 1) * inv[(a + b - 1) + (a - 1)] % Mod * inv[a + b] % Mod),
                p2 = (a == 1 && !b ? 0 : 1ll * (a + b - 1) * inv[(a + b - 1) + (a - 1)] % Mod * inv[a + b] % Mod);

            for (int i = 0; i < n; ++i) {
                f[a][b][i] = add(f[a][b][i], add(1ll * p1 * s1 % Mod, 1ll * p2 * s2 % Mod));

                f[a][b][i] = add(f[a][b][i], 1ll * p1 * (a / n + (i < a % n)) % Mod);
                f[a][b][i] = add(f[a][b][i], 1ll * p2 * ((a + b) / n + (i < (a + b) % n)) % Mod);

                f[a][b][i] = add(f[a][b][i], 1ll * p1 * b % Mod * f[a][b - 1][((i - a) % n + n) % n] % Mod);

                s1 = add(s1, dec(f[a - 1][b][i], f[a - 1][b][(i - a % n + n) % n]));
                s2 = add(s2, dec(f[a - 1][b][i], f[a - 1][b][(i - (a + b) % n + n) % n]));
            }
        }

    for (int i = 0; i < n; ++i)
        printf("%d ", f[l][k][i]);

    return 0;
}

CF1765C Card Guessing

有四种花色的牌,每种花色的牌均有 \(n\) 张,这 \(4n\) 张牌两两不同。

现在从牌堆中依次取出牌,每次会猜测当前牌花色为过去 \(k\) 次(若不足 \(k\) 次则为之前所有的抽卡结果)结果中出现次数最少的那一个花色(如有多个花色满足条件则随机猜一个)。

求期望猜对次数。

\(n \leq 500\)

根据期望线性性,则问题转化为每次猜对的概率。

\(i\) 次猜测时会根据过去 \(c = \min(i - 1, k)\) 次抽卡的结果猜测,设四种花色的出现次数为 \(c_{1 \sim 4}\) ,则猜对的概率为 \(\frac{n - (\min c_i)}{4n - c}\) (出现次数最少的花色数量与等概率选花色相互抵消)。

\(f_{i, j, k}\) 表示每种花色数量 \(\geq i\) 、考虑了 \(j\) 种花色、选了 \(k\) 张牌的贡献,不难 \(O(n^3)\) DP 求解,\(\min c_i\) 恰好为 \(i\) 的贡献即为 \(f_{i, 4, k} - f_{i + 1, 4, k}\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e3 + 7;

int fac[N], inv[N], invfac[N], f[N][5][N];

int n, k;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

inline int invC(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * invfac[n] * fac[m] % Mod * fac[n - m] % Mod;
}

inline int calc(int n, int k) {
    int res = 0;

    for (int i = 0; i <= min(n, k); ++i)
        res = add(res, 1ll * dec(f[i][4][k], f[i + 1][4][k]) * dec(n, i) % Mod * inv[n * 4 - k] % Mod);

    return 1ll * res * invC(n * 4, k) % Mod;
}

signed main() {
    scanf("%d%d", &n, &k);
    prework(n * 4), k = min(k, n * 4 - 1);

    for (int i = 0; i <= n; ++i) {
        f[i][0][0] = 1;

        for (int j = 1; j <= 4; ++j)
            for (int k = i; k <= n; ++k)
                for (int l = k; l <= n * 4; ++l)
                    f[i][j][l] = add(f[i][j][l], 1ll * C(n, k) * f[i][j - 1][l - k] % Mod);
    }

    int ans = 0;

    for (int i = 0; i < k; ++i)
        ans = add(ans, calc(n, i));

    printf("%d", add(ans, 1ll * calc(n, k) * (n * 4 - k) % Mod));
    return 0;
}
posted @ 2025-06-06 20:52  wshcl  阅读(49)  评论(0)    收藏  举报