Chương 5·Bài 55

Probability

Tung một đồng xu 10 lần, xác suất được đúng 3 mặt ngửa là bao nhiêu? Mỗi lần tung thêm vào, xác suất của mọi kết quả cũ tách làm hai phần. Cách làm đó là cả bài học này.

Xác suất trong thi đấu hầu như luôn giải bằng một trong ba cách: DP trên phân phối (xác suất của từng trạng thái sau từng bước), tuyến tính kỳ vọng (cộng đóng góp của từng phần, kể cả khi các phần phụ thuộc nhau), và DP kỳ vọng (kỳ vọng số bước còn lại từ mỗi trạng thái). Đề thường bắt in số thực với sai số cho phép, hoặc theo modulo một số nguyên tố.

DP phân phối

Có nn phép thử độc lập, cần xác suất của một sự kiện về kết quả gộp (số lần thành công, tổng điểm). Gọi dpi[j]dp_i[j] là xác suất sau ii phép thử, đại lượng gộp bằng jj. Phép thử thứ i+1i + 1 chỉ phụ thuộc chính nó, nên

dpi+1[j]=dpi[j]⋅(1−p)+dpi[j−1]⋅p.dp_{i+1}[j] = dp_i[j] \cdot (1 - p) + dp_i[j - 1] \cdot p.

Chỉ cần một mảng, duyệt jj giảm dần để không dùng giá trị vừa cập nhật. Cuối cùng cộng dpn[j]dp_n[j] trên các jj thỏa sự kiện.

Trong hình, mỗi cột là xác suất có đúng jj lần thành công. Mỗi phép thử tách mỗi cột thành hai phần: một phần ở lại, một phần dịch sang phải.

Phân phối số lần thành công sau từng phép thử.

Ví dụĐa Số Tín Hiệu

Đề. N≤2999N \le 2999 (lẻ) cảm biến độc lập, cảm biến ii cho tín hiệu dương với xác suất pip_i. Tính xác suất số tín hiệu dương nhiều hơn số tín hiệu âm, sai số 10−910^{-9}.

Lời giải. dp[j] là xác suất có đúng jj tín hiệu dương trong các cảm biến đã xét. Hết cảm biến, cộng dp[j] với j>N/2j > N/2.

Bước 1: thêm một cảm biến. jj dương có được từ jj dương cũ và cảm biến này âm, hoặc j−1j - 1 dương cũ và cảm biến này dương. Duyệt jj giảm dần để dp[j - 1] vẫn là giá trị cũ khi dùng, như cái túi một chiều. dp[0] xử lý riêng vì không có dp[-1].

C++
for (ll j = i; j >= 1; j--) dp[j] = dp[j] * (1 - p) + dp[j - 1] * p;
dp[0] *= 1 - p;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 3005;
ll n;
double dp[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    dp[0] = 1;
    for (ll i = 1; i <= n; i++) {
        double p;
        cin >> p;
        for (ll j = i; j >= 1; j--) dp[j] = dp[j] * (1 - p) + dp[j - 1] * p;
        dp[0] *= 1 - p;
    }
    double ans = 0;
    for (ll j = n / 2 + 1; j <= n; j++) ans += dp[j];
    cout << fixed << setprecision(12) << ans << '\n';
}
3
0.20 0.50 0.90

N2/2≈4,5⋅106N^2 / 2 \approx 4{,}5 \cdot 10^6 phép; double đủ chính xác cho 10−910^{-9}. long double chính xác hơn nhưng phải trả giá:

DP phân phối với hai mươi nghìn phép thử, trung vị 5 lần chạy, i7-10850H, g++ -O2.double217 mslong double620 ms · gấp 2,9 lần
DP phân phối với hai mươi nghìn phép thử, hai kiểu số thực, đo trên i7-10850H, g++ -O2.

double nhanh hơn long double khoảng 2,7 lần. Quy tắc: dùng double khi sai số đề cho là 10−610^{-6} đến 10−910^{-9}; chỉ đổi sang long double khi kết quả sai thật.

Kỳ vọng của max

Với biến XX nhận giá trị nguyên không âm, E[X]=∑k≥1P(X≥k)E[X] = \sum_{k \ge 1} P(X \ge k): mỗi giá trị xx được đếm đúng xx lần, ở k=1,…,xk = 1, \dots, x.

Công thức hợp với giá trị lớn nhất, vì P(max⁡≤k)P(\max \le k) dễ tính: mọi phép thử đều không vượt kk, tức (k/m)n(k/m)^n khi mỗi lần chọn đều trong 1..m1..m. Vậy:

E[max⁡]=∑k=1m(1−(k−1m)n).E[\max] = \sum_{k=1}^{m} \left(1 - \left(\frac{k - 1}{m}\right)^n\right).

Lũy thừa số thực tính bằng pow. Vì (k−1)/m<1(k-1)/m < 1 nên không tràn, chỉ có thể rất nhỏ.

Hỏi nhanh

Gieo hai lần một xúc xắc 4 mặt. Kỳ vọng của số lớn hơn là bao nhiêu?

Biến chỉ báo

E[X+Y]=E[X]+E[Y]E[X + Y] = E[X] + E[Y] luôn đúng, kể cả khi XX và YY phụ thuộc nhau. Muốn tính kỳ vọng của một đại lượng đếm, viết nó thành tổng các biến chỉ báo IeI_e (bằng 1 nếu sự kiện ee xảy ra, 0 nếu không). Khi đó E[∑Ie]=∑P(e)E[\sum I_e] = \sum P(e), và việc còn lại là chọn sự kiện ee sao cho P(e)P(e) dễ tính.

Đại lượng không phải tổng đếm, như bình phương độ dài, có thể viết thành tổng các phần tăng: nối thêm một phần tử vào đoạn liên tiếp dài ll thì l2l^2 tăng 2l+12l + 1. Kỳ vọng phần tăng chỉ cần E[l]E[l], mà E[l]E[l] có công thức truy hồi đơn giản.

Mỗi đỉnh đóng góp xác suất được chọn trực tiếp vào kỳ vọng tổng số lượt.245311/11/21/21/31/3E[số lượt] = Σ xác suất được chọn trực tiếp
Mỗi đỉnh đóng góp xác suất nó được chọn trực tiếp, bằng nghịch đảo độ sâu tính cả gốc.

Hỏi nhanh

Xáo đều một hoán vị của 1..n1..n. Kỳ vọng số điểm cố định (vị trí ii chứa đúng số ii) là bao nhiêu?

Ví dụCây Con Biến Mất

Đề. Cây n≤105n \le 10^5 đỉnh, gốc 1. Mỗi lượt chọn đều một đỉnh còn lại và xóa nó cùng cây con của nó. Tính kỳ vọng số lượt.

Lời giải. Số lượt bằng số đỉnh được chọn trực tiếp. Đỉnh vv được chọn trực tiếp khi nó được chọn trước mọi tổ tiên của nó. Xét dvd_v đỉnh trên đường từ gốc tới vv: mỗi đỉnh có khả năng như nhau là đỉnh đầu tiên bị chọn, nên xác suất là 1/dv1/d_v. Tuyến tính kỳ vọng cho phép cộng xác suất từng đỉnh dù các sự kiện phụ thuộc nhau: đáp án ∑1/dv\sum 1/d_v, không cần mô phỏng quá trình xóa.

Bước 1: BFS tính độ sâu. dep[1] = 1: độ sâu tính cả gốc, đúng là số đỉnh trên đường từ gốc. Cộng 1.0 / dep[u] khi lấy mỗi đỉnh ra.

C++
while (!q.empty()) {
    ll u = q.front();
    q.pop();
    ans += 1.0 / dep[u];
    for (ll v : adj[u])
        if (!dep[v]) dep[v] = dep[u] + 1, q.push(v);
}
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 1e5 + 5;
ll n, dep[N];
vector<ll> adj[N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i < n; i++) {
        ll u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    queue<ll> q;
    q.push(1);
    dep[1] = 1;
    double ans = 0;
    while (!q.empty()) {
        ll u = q.front();
        q.pop();
        ans += 1.0 / dep[u];
        for (ll v : adj[u])
            if (!dep[v]) dep[v] = dep[u] + 1, q.push(v);
    }
    cout << fixed << setprecision(12) << ans << '\n';
}
5
1 2
1 3
3 4
3 5

O(n)O(n).

Ví dụNhịp Thành Công

Đề. n≤105n \le 10^5 phép thử độc lập, phép thứ ii thành công với xác suất pip_i. Điểm là tổng bình phương độ dài các đoạn thành công liên tiếp cực đại. Tính kỳ vọng điểm.

Lời giải. Bình phương không cộng tuyến tính được, nên tính phần tăng: một đoạn dài ll thêm một phép thành công thì điểm tăng (l+1)2−l2=2l+1(l + 1)^2 - l^2 = 2l + 1. Gọi ee là kỳ vọng độ dài đoạn thành công kết thúc tại phép thử trước. Phép ii thành công với xác suất pip_i, nên kỳ vọng phần tăng là pi(2e+1)p_i(2e + 1).

Bước 1: cộng trước, cập nhật sau. Phần tăng dùng ee cũ. Rồi e←(e+1)pie \leftarrow (e + 1) p_i: thành công thì đoạn dài thêm 1, thất bại thì về 0.

C++
ans += p * (2 * e + 1);
e = (e + 1) * p;
Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

ll n;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    double e = 0, ans = 0;
    for (ll i = 1; i <= n; i++) {
        double p;
        cin >> p;
        ans += p * (2 * e + 1);
        e = (e + 1) * p;
    }
    cout << fixed << setprecision(12) << ans << '\n';
}
4
1 0.5 0.5 0

O(n)O(n).

DP kỳ vọng

Gọi E(s)E(s) là kỳ vọng số bước còn lại từ trạng thái ss. Một bước tốn 1 rồi chuyển sang s′s' với xác suất P(s→s′)P(s \to s'):

E(s)=1+∑s′P(s→s′) E(s′).E(s) = 1 + \sum_{s'} P(s \to s')\, E(s').

Nếu có xác suất qq ở lại ss, chuyển vế: E(s)(1−q)=1+∑s′≠sP(s→s′)E(s′)E(s)(1 - q) = 1 + \sum_{s' \ne s} P(s \to s') E(s'), rồi chia cho 1−q1 - q. Thứ tự tính ngược với quá trình, thường theo một đại lượng chỉ giảm.

Để giảm số chiều, gom các phần tử giống nhau: trạng thái chỉ cần số lượng mỗi loại, không cần biết phần tử nào.

Ví dụBàn Ăn Ngẫu Nhiên

Đề. N≤300N \le 300 đĩa, đĩa ii có ai∈{1,2,3}a_i \in \{1, 2, 3\} miếng. Mỗi lượt chọn đều một đĩa, còn miếng thì ăn một miếng. Tính kỳ vọng số lượt tới khi hết.

Lời giải. Các đĩa cùng số miếng như nhau, nên trạng thái chỉ cần (c1,c2,c3)(c_1, c_2, c_3): số đĩa còn 1, 2, 3 miếng. Với s=c1+c2+c3s = c_1 + c_2 + c_3, xác suất chọn đĩa rỗng là (N−s)/N(N - s)/N; chuyển vế phần ở lại đó được

E=N+c1E(c1−1,c2,c3)+c2E(c1+1,c2−1,c3)+c3E(c1,c2+1,c3−1)s.E = \frac{N + c_1 E(c_1 - 1, c_2, c_3) + c_2 E(c_1 + 1, c_2 - 1, c_3) + c_3 E(c_1, c_2 + 1, c_3 - 1)}{s}.

Bước 1: hai tầng. Mảng 3013301^3 số double quá lớn, nhưng tầng c3c_3 chỉ dùng tầng c3−1c_3 - 1. cur[c1][c2] là tầng hiện tại, pre là tầng trước.

Bước 2: thứ tự tính trong một tầng. Duyệt c2c_2 rồi c1c_1 tăng dần: cur[c1 - 1][c2] và cur[c1 + 1][c2 - 1] đều đã tính, vì c1+c2c_1 + c_2 hoặc c2c_2 nhỏ hơn.

C++
double e = n;
if (c1) e += c1 * cur[c1 - 1][c2];
if (c2) e += c2 * cur[c1 + 1][c2 - 1];
if (c3) e += c3 * pre[c1][c2 + 1];
cur[c1][c2] = e / s;

Bước 3: sang tầng sau. memcpy chép cả tầng sang pre. Hai bảng 3052305^2 số double, chưa tới 1,5 MB.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll N = 305;
ll n, cnt[4];
double pre[N][N], cur[N][N];

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    for (ll i = 1; i <= n; i++) {
        ll a;
        cin >> a;
        cnt[a]++;
    }
    for (ll c3 = 0; c3 <= cnt[3]; c3++) {
        for (ll c2 = 0; c2 + c3 <= n; c2++)
            for (ll c1 = 0; c1 + c2 + c3 <= n; c1++) {
                ll s = c1 + c2 + c3;
                if (!s) {
                    cur[c1][c2] = 0;
                    continue;
                }
                double e = n;
                if (c1) e += c1 * cur[c1 - 1][c2];
                if (c2) e += c2 * cur[c1 + 1][c2 - 1];
                if (c3) e += c3 * pre[c1][c2 + 1];
                cur[c1][c2] = e / s;
            }
        memcpy(pre, cur, sizeof(cur));
    }
    cout << fixed << setprecision(12) << pre[cnt[1]][cnt[2]] << '\n';
}
2
1 2

O(N3/6)O(N^3 / 6), khoảng 4,5⋅1064{,}5 \cdot 10^6 trạng thái.

Kỳ vọng modulo

Đề in “X⋅Y−1 mod pX \cdot Y^{-1} \bmod p” khi kỳ vọng là phân số X/YX/Y. Mọi phép cộng, nhân, chia trong công thức đều làm được theo modulo: xác suất a/100a/100 thành a⋅100−1a \cdot 100^{-1}, phép chia thành nhân nghịch đảo bằng lũy thừa nhanh. Cứ suy ra công thức như với số thực, rồi tính theo modulo; không so sánh hay làm tròn giữa chừng.

Ví dụChuỗi Cổng Khởi Động Lại

Đề. n≤2⋅105n \le 2 \cdot 10^5 cổng; ở cổng ii thành công với xác suất pi/100p_i/100 thì sang cổng tiếp theo (hoặc kết thúc ở cổng nn), thất bại thì quay về cổng 1. Mỗi ngày kiểm một cổng. In kỳ vọng số ngày theo modulo 998244353998244353.

Lời giải. Gọi qi=p1p2⋯piq_i = p_1 p_2 \cdots p_i (theo xác suất). Một lần chạy từ cổng 1 tới được cổng ii (và tốn thêm ngày thứ ii) với xác suất qi−1q_{i-1}, nên kỳ vọng số ngày của một lần chạy là 1+q1+⋯+qn−11 + q_1 + \dots + q_{n-1}. Mỗi lần chạy thành công với xác suất qnq_n, số lần chạy kỳ vọng là 1/qn1/q_n. Đáp án (1+q1+⋯+qn−1)/qn(1 + q_1 + \dots + q_{n-1}) / q_n.

Bước 1: tích lũy. inv100 là nghịch đảo của 100, để pi/100p_i / 100 thành phép nhân theo mô-đun. num cộng qi−1q_{i-1} trước rồi mới nhân qq với pip_i; hết vòng num =1+q1+⋯+qn−1= 1 + q_1 + \dots + q_{n-1} và q =qn= q_n.

C++
for (ll i = 1; i <= n; i++) {
    ll p;
    cin >> p;
    num = (num + q) % MOD;
    q = q * p % MOD * inv100 % MOD;
}
cout << num * power(q, MOD - 2) % MOD << '\n';

Phép chia là nhân với nghịch đảo của q. Mọi bước chỉ là cộng, nhân, chia, nên tính theo mô-đun đúng như với số thực.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

const ll MOD = 998244353;
ll n;

ll power(ll a, ll b) {
    ll r = 1;
    a %= MOD;
    while (b) {
        if (b & 1) r = r * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return r;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n;
    ll inv100 = power(100, MOD - 2), q = 1, num = 0;
    for (ll i = 1; i <= n; i++) {
        ll p;
        cin >> p;
        num = (num + q) % MOD;
        q = q * p % MOD * inv100 % MOD;
    }
    cout << num * power(q, MOD - 2) % MOD << '\n';
}
3
100 50 100

O(n+log⁡p)O(n + \log p).

Khi đi thi

  • Kỳ vọng của đại lượng đếm: viết thành tổng biến chỉ báo trước, tìm sự kiện có xác suất đơn giản (thứ tự tương đối, cặp phần tử, phần tăng).
  • Phân phối sau nhiều phép thử độc lập: DP một mảng, duyệt ngược. Kỳ vọng số bước: DP ngược quá trình, chuyển vế xác suất ở lại.
  • In số thực với fixed và 9 đến 12 chữ số; in modulo thì thay mọi phép chia bằng nghịch đảo.

Tóm tắt

  • dp[j]←dp[j](1−p)+dp[j−1]pdp[j] \leftarrow dp[j](1 - p) + dp[j - 1]p cho phân phối số lần thành công.
  • E[X]=∑k≥1P(X≥k)E[X] = \sum_{k \ge 1} P(X \ge k); với giá trị lớn nhất, P(max⁡≤k)P(\max \le k) là tích.
  • Tuyến tính kỳ vọng không cần độc lập; bình phương độ dài tách thành tổng phần tăng 2l+12l + 1.
  • E(s)=1+∑P(s→s′)E(s′)E(s) = 1 + \sum P(s \to s')E(s'), giải ra khi có vòng lặp tự thân.

Bài tập

Đọc thêm