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ó 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 là xác suất sau phép thử, đại lượng gộp bằng . Phép thử thứ chỉ phụ thuộc chính nó, nên
Chỉ cần một mảng, duyệt giảm dần để không dùng giá trị vừa cập nhật. Cuối cùng cộng trên các thỏa sự kiện.
Trong hình, mỗi cột là xác suất có đúng 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.
Ví dụĐa Số Tín Hiệu
Đề. (lẻ) cảm biến độc lập, cảm biến cho tín hiệu dương với xác suất . 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ố .
Lời giải. dp[j] là xác suất có đúng 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 .
Bước 1: thêm một cảm biến. dương có được từ dương cũ và cảm biến này âm, hoặc dương cũ và cảm biến này dương. Duyệt 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].
for (ll j = i; j >= 1; j--) dp[j] = dp[j] * (1 - p) + dp[j - 1] * p;
dp[0] *= 1 - p;
#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
0.550000000000
phép; double đủ chính xác cho . long double chính xác hơn nhưng phải trả giá:
double nhanh hơn long double khoảng 2,7 lần. Quy tắc: dùng double khi sai số đề cho là đến ; chỉ đổi sang long double khi kết quả sai thật.
Kỳ vọng của max
Với biến nhận giá trị nguyên không âm, : mỗi giá trị được đếm đúng lần, ở .
Công thức hợp với giá trị lớn nhất, vì dễ tính: mọi phép thử đều không vượt , tức khi mỗi lần chọn đều trong . Vậy:
Lũy thừa số thực tính bằng pow. Vì 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?
, , , . Tổng là .
Biến chỉ báo
luôn đúng, kể cả khi và 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 (bằng 1 nếu sự kiện xảy ra, 0 nếu không). Khi đó , và việc còn lại là chọn sự kiện sao cho 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 thì tăng . Kỳ vọng phần tăng chỉ cần , mà có công thức truy hồi đơn giản.
Hỏi nhanh
Xáo đều một hoán vị của . Kỳ vọng số điểm cố định (vị trí chứa đúng số ) là bao nhiêu?
Mỗi vị trí là một biến chỉ báo với xác suất ; có vị trí nên tổng kỳ vọng là . Các biến này phụ thuộc nhau, nhưng tuyến tính kỳ vọng không cần độc lập.
Ví dụCây Con Biến Mất
Đề. Cây đỉ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 được chọn trực tiếp khi nó được chọn trước mọi tổ tiên của nó. Xét đỉnh trên đường từ gốc tới : mỗi đỉnh có khả năng như nhau là đỉnh đầu tiên bị chọn, nên xác suất là . 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 , 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.
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);
}
#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
2.666666666667
.
Ví dụNhịp Thành Công
Đề. phép thử độc lập, phép thứ thành công với xác suất . Đ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 thêm một phép thành công thì điểm tăng . Gọi 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 thành công với xác suất , nên kỳ vọng phần tăng là .
Bước 1: cộng trước, cập nhật sau. Phần tăng dùng cũ. Rồi : thành công thì đoạn dài thêm 1, thất bại thì về 0.
ans += p * (2 * e + 1);
e = (e + 1) * p;
#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
4.000000000000
.
DP kỳ vọng
Gọi là kỳ vọng số bước còn lại từ trạng thái . Một bước tốn 1 rồi chuyển sang với xác suất :
Nếu có xác suất ở lại , chuyển vế: , rồi chia cho . 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
Đề. đĩa, đĩa có 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 : số đĩa còn 1, 2, 3 miếng. Với , xác suất chọn đĩa rỗng là ; chuyển vế phần ở lại đó được
Bước 1: hai tầng. Mảng số double quá lớn, nhưng tầng chỉ dùng tầng . 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 rồi tăng dần: cur[c1 - 1][c2] và cur[c1 + 1][c2 - 1] đều đã tính, vì hoặc nhỏ hơn.
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 số double, chưa tới 1,5 MB.
#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
4.500000000000
, khoảng trạng thái.
Kỳ vọng modulo
Đề in “” khi kỳ vọng là phân số . Mọi phép cộng, nhân, chia trong công thức đều làm được theo modulo: xác suất thành , 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
Đề. cổng; ở cổng thành công với xác suất thì sang cổng tiếp theo (hoặc kết thúc ở cổng ), 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 .
Lời giải. Gọi (theo xác suất). Một lần chạy từ cổng 1 tới được cổng (và tốn thêm ngày thứ ) với xác suất , nên kỳ vọng số ngày của một lần chạy là . Mỗi lần chạy thành công với xác suất , số lần chạy kỳ vọng là . Đáp án .
Bước 1: tích lũy. inv100 là nghịch đảo của 100, để thành phép nhân theo mô-đun. num cộng trước rồi mới nhân với ; hết vòng num và q .
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.
#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
5
.
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
fixedvà 9 đến 12 chữ số; in modulo thì thay mọi phép chia bằng nghịch đảo.
Tóm tắt
- cho phân phối số lần thành công.
- ; với giá trị lớn nhất, 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 .
- , giải ra khi có vòng lặp tự thân.