Một lựa chọn ngẫu nhiên đúng chỗ có thể thay cả một cấu trúc dữ liệu phức tạp. Có hai kiểu bảo đảm: luôn đúng, thời gian ngẫu nhiên (lấy mẫu ứng viên rồi kiểm lại chính xác), và thời gian cố định, xác suất sai nhỏ (kiểm chứng ngẫu nhiên, băm). Viết lời giải ngẫu nhiên là phải ước lượng được xác suất thất bại, chọn số lần thử theo con số đó, và dùng bộ sinh tốt có hạt giống theo thời gian.
Lấy mẫu một nhân chứng
Nếu đáp án được xác định bởi một (hay hai) phần tử, và các phần tử “tốt” chiếm ít nhất tỉ lệ , thì chọn ngẫu nhiên một phần tử sẽ trúng với xác suất . Thử lần độc lập, xác suất trượt cả lần là . Mỗi ứng viên được kiểm lại chính xác nên không bao giờ in sai “có”; chỉ có thể sót.
Ví dụ: đường thẳng chứa ít nhất 20% số điểm. Một cặp điểm ngẫu nhiên cùng nằm trên đường ấy với xác suất khoảng ; thử 300 cặp thì xác suất trượt là .
Khi cộng dồn trên nhiều truy vấn, cộng các xác suất sai (chặn hợp): truy vấn, mỗi truy vấn sai với xác suất , tổng không quá . Chọn sao cho con số này rất nhỏ, ví dụ .
Dùng mt19937 (hoặc mt19937_64) với hạt giống chrono::steady_clock::now(). Đừng dùng rand(): trên một số trình biên dịch nó chỉ cho tối đa 32767, và hạt giống cố định dễ bị dựng test phá.
Hỏi nhanh
Đáp án là một giá trị chiếm hơn một nửa đoạn. Lấy 30 vị trí ngẫu nhiên làm ứng viên thì xác suất bỏ sót khoảng bao nhiêu?
Mỗi lần lấy trúng với xác suất hơn , trượt dưới ; 30 lần độc lập đều trượt với xác suất dưới .
Ví dụĐường Thẳng Đủ Đông
Đề. điểm phân biệt, tọa độ tới , và . Có đường thẳng nào đi qua ít nhất điểm không?
Lời giải. Nếu có đường đi qua ít nhất 20% số điểm, một cặp điểm ngẫu nhiên nằm trên nó với xác suất khoảng . Thử 300 cặp, mỗi cặp đếm số điểm thẳng hàng; cả 300 lần đều trượt với xác suất .
Bước 1: trường hợp nhỏ. need = (n * p + 99) / 100 là tính bằng số nguyên. Cần không quá 2 điểm thì luôn có đường (qua hai điểm bất kỳ, hoặc qua điểm duy nhất).
ll need = (n * p + 99) / 100;
if (need <= 2) {
cout << "possible\n";
return 0;
}
Bước 2: thử một cặp. Đếm mọi điểm thẳng hàng với , bằng cách so hai tích chéo thay cho chia lấy hệ số góc. Hiệu tọa độ tới nên mỗi tích tới , vẫn vừa long long.
ll i = rng() % n, j = rng() % n;
if (i == j) continue;
ll cnt = 0;
for (ll k = 0; k < n; k++)
if ((x[j] - x[i]) * (y[k] - y[i]) == (y[j] - y[i]) * (x[k] - x[i])) cnt++;
mt19937 gieo bằng đồng hồ, để test cố định không thể nhắm vào một dãy số ngẫu nhiên biết trước.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 1e5 + 5;
ll n, p, x[N], y[N];
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> p;
for (ll i = 0; i < n; i++) cin >> x[i] >> y[i];
ll need = (n * p + 99) / 100;
if (need <= 2) {
cout << "possible\n";
return 0;
}
for (ll it = 0; it < 300; it++) {
ll i = rng() % n, j = rng() % n;
if (i == j) continue;
ll cnt = 0;
for (ll k = 0; k < n; k++)
if ((x[j] - x[i]) * (y[k] - y[i]) == (y[j] - y[i]) * (x[k] - x[i])) cnt++;
if (cnt >= need) {
cout << "possible\n";
return 0;
}
}
cout << "impossible\n";
}
6
50
0 1
2 5
4 9
0 0
1 0
5 2
possible
phép.
Ví dụGiá Trị Nổi Bật Trên Đoạn
Đề. Mảng , truy vấn : in tăng dần mọi giá trị xuất hiện nhiều hơn lần, hoặc .
Lời giải. Có nhiều nhất hai giá trị như vậy, mỗi giá trị chiếm hơn đoạn. Lấy 60 vị trí ngẫu nhiên làm ứng viên: mỗi giá trị đáp án bị sót với xác suất dưới . Đếm chính xác từng ứng viên, nên không bao giờ in sai, chỉ có thể sót với xác suất rất nhỏ đó.
Bước 1: nén giá trị, danh sách vị trí. Mỗi bộ test nén lại và dựng pos[v], các vị trí của giá trị theo thứ tự tăng.
for (int i = 1; i <= n; i++) {
id[i] = lower_bound(vals.begin(), vals.end(), a[i]) - vals.begin();
pos[id[i]].push_back(i);
}
Bước 2: chọn ứng viên. Đoạn không dài hơn 60 thì lấy hết, khỏi phụ thuộc may rủi; dài hơn thì lấy 60 vị trí ngẫu nhiên.
if (len <= S)
for (int i = l; i <= r; i++) c.push_back(id[i]);
else
for (int k = 0; k < S; k++) c.push_back(id[l + rng() % len]);
Bước 3: đếm chính xác. Số lần xuất hiện trong là upper_bound(r) - lower_bound(l) trên pos[v]. Ứng viên đã sắp theo chỉ số nén, mà chỉ số nén theo giá trị tăng, nên ans ra đúng thứ tự cần in.
for (int v : c) {
int cnt = upper_bound(pos[v].begin(), pos[v].end(), r) - lower_bound(pos[v].begin(), pos[v].end(), l);
if (cnt > len / 3) ans.push_back(vals[v]);
}
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 5, S = 60;
int t, n, q, a[N], id[N];
vector<int> vals, pos[N];
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
cin >> n >> q;
vals.clear();
for (int i = 1; i <= n; i++) cin >> a[i], vals.push_back(a[i]);
sort(vals.begin(), vals.end());
vals.erase(unique(vals.begin(), vals.end()), vals.end());
for (int v = 0; v < (int)vals.size(); v++) pos[v].clear();
for (int i = 1; i <= n; i++) {
id[i] = lower_bound(vals.begin(), vals.end(), a[i]) - vals.begin();
pos[id[i]].push_back(i);
}
while (q--) {
int l, r;
cin >> l >> r;
int len = r - l + 1;
vector<int> c;
if (len <= S)
for (int i = l; i <= r; i++) c.push_back(id[i]);
else
for (int k = 0; k < S; k++) c.push_back(id[l + rng() % len]);
sort(c.begin(), c.end());
c.erase(unique(c.begin(), c.end()), c.end());
vector<int> ans;
for (int v : c) {
int cnt = upper_bound(pos[v].begin(), pos[v].end(), r) - lower_bound(pos[v].begin(), pos[v].end(), l);
if (cnt > len / 3) ans.push_back(vals[v]);
}
if (ans.empty()) cout << -1;
for (int x : ans) cout << x << ' ';
cout << '\n';
}
}
}
1
9 5
4 7 4 9 7 4 7 8 10
1 9
1 7
2 6
4 9
8 8
-1
4 7
4 7
-1
8
Mỗi truy vấn .
Kiểm chứng ngẫu nhiên
Kiểm bằng cách nhân thật tốn . Freivalds chọn vector ngẫu nhiên rồi so với , chỉ ba phép nhân ma trận với vector, . Nếu thì hai vế luôn bằng nhau. Nếu , có hàng của khác 0, và là một phương trình tuyến tính không tầm thường theo : với chọn đều trong , xác suất thỏa chỉ là .
Cùng ý tưởng: so hai đa thức bằng giá trị tại một điểm ngẫu nhiên (đa thức bậc khác 0 có không quá nghiệm), so hai xâu bằng băm.
Ví dụKiểm Tra Phép Nhân Ma Trận
Đề. Ba ma trận , , phần tử dưới . Kiểm .
Lời giải. Tính tốn phép. Freivalds: chọn vector ngẫu nhiên và so với , chỉ cần ba phép nhân ma trận với vector. Nếu thì một lần thử bị lừa với xác suất không quá ; hai lần thử độc lập là .
Bước 1: nhân ma trận với vector. , lấy dư sau mỗi phép cộng; tích hai số dưới nhỏ hơn , vừa long long.
void mulvec(ll M[N][N], ll *in, ll *out) {
for (ll i = 0; i < n; i++) {
ll s = 0;
for (ll j = 0; j < n; j++) s = (s + M[i][j] * in[j]) % MOD;
out[i] = s;
}
}
Bước 2: một lần thử. bv , abv , cv .
for (ll i = 0; i < n; i++) v[i] = rng() % MOD;
mulvec(B, v, bv);
mulvec(A, bv, abv);
mulvec(C, v, cv);
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 1005, MOD = 1e9 + 7;
ll n, A[N][N], B[N][N], C[N][N], v[N], bv[N], abv[N], cv[N];
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
void mulvec(ll M[N][N], ll *in, ll *out) {
for (ll i = 0; i < n; i++) {
ll s = 0;
for (ll j = 0; j < n; j++) s = (s + M[i][j] * in[j]) % MOD;
out[i] = s;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 0; i < n; i++)
for (ll j = 0; j < n; j++) cin >> A[i][j];
for (ll i = 0; i < n; i++)
for (ll j = 0; j < n; j++) cin >> B[i][j];
for (ll i = 0; i < n; i++)
for (ll j = 0; j < n; j++) cin >> C[i][j];
for (ll it = 0; it < 2; it++) {
for (ll i = 0; i < n; i++) v[i] = rng() % MOD;
mulvec(B, v, bv);
mulvec(A, bv, abv);
mulvec(C, v, cv);
for (ll i = 0; i < n; i++)
if (abv[i] != cv[i]) {
cout << "NO\n";
return 0;
}
}
cout << "YES\n";
}
2
1 0
0 1
2 3
4 5
2 3
4 5
YES
Hai lần thử, mỗi lần phép.
Kiểm tra nguyên tố Miller-Rabin
Với lẻ, viết với lẻ. Nếu nguyên tố thì với mọi không chia hết cho , dãy theo mô-đun hoặc bắt đầu bằng 1, hoặc chứa trước khi tới 1 (vì chỉ có nghiệm theo mô-đun nguyên tố). Cơ số vi phạm điều này là nhân chứng cho là hợp số. Với hợp số, ít nhất số là nhân chứng, nên vài cơ số ngẫu nhiên đã đủ; với , 12 cơ số nguyên tố đầu tiên đã được kiểm là đủ chắc chắn.
Phép nhân theo mô-đun tới cần __int128. Mỗi cơ số tốn phép nhân.
Ví dụNhận Diện Số Nguyên Tố
Đề. số , in YES nếu nguyên tố.
Lời giải. Miller-Rabin với 12 số nguyên tố đầu làm cơ số. Bộ cơ số này đã được kiểm là đúng cho mọi , nên kết quả chắc chắn đúng chứ không chỉ đúng với xác suất cao.
Bước 1: nhân theo mô-đun lớn. Hai số dưới có tích tới , nên nhân trong __int128.
ll mul(ll a, ll b, ll m) {
return (__int128)a * b % m;
}
Bước 2: chia thử. Gặp ước nhỏ thì nguyên tố chỉ khi chính nó là số đó. Bước này cũng loại mọi số chẵn, nên phần sau luôn có lẻ. Rồi tách với lẻ.
for (ll p : bases)
if (n % p == 0) return n == p;
ll d = n - 1, s = 0;
while (d % 2 == 0) d /= 2, s++;
Bước 3: kiểm một cơ số. . Bằng 1 hoặc thì không phải nhân chứng. Ngược lại bình phương tối đa lần; gặp thì thôi. Không gặp thì là nhân chứng, là hợp số.
ll x = power(a, d, n);
if (x == 1 || x == n - 1) continue;
bool comp = 1;
for (ll r = 1; r < s && comp; r++) {
x = mul(x, x, n);
if (x == n - 1) comp = 0;
}
if (comp) return 0;
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll bases[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
ll mul(ll a, ll b, ll m) {
return (__int128)a * b % m;
}
ll power(ll a, ll b, ll m) {
ll r = 1;
a %= m;
while (b) {
if (b & 1) r = mul(r, a, m);
a = mul(a, a, m);
b >>= 1;
}
return r;
}
bool isPrime(ll n) {
if (n < 2) return 0;
for (ll p : bases)
if (n % p == 0) return n == p;
ll d = n - 1, s = 0;
while (d % 2 == 0) d /= 2, s++;
for (ll a : bases) {
ll x = power(a, d, n);
if (x == 1 || x == n - 1) continue;
bool comp = 1;
for (ll r = 1; r < s && comp; r++) {
x = mul(x, x, n);
if (x == n - 1) comp = 0;
}
if (comp) return 0;
}
return 1;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
ll t;
cin >> t;
while (t--) {
ll n;
cin >> n;
cout << (isPrime(n) ? "YES" : "NO") << '\n';
}
}
6
2
29
91
97
221
1000000
YES
YES
NO
YES
NO
NO
Mỗi số phép nhân __int128.
Băm bằng trọng số ngẫu nhiên
Muốn biết hai đoạn có cùng đa tập giá trị không, gán mỗi giá trị một số ngẫu nhiên 64 bit và lấy trên đoạn (tràn số tự nhiên của unsigned long long chính là cộng theo mô-đun ). Hai đa tập bằng nhau thì bằng nhau; khác nhau thì bằng nhau với xác suất rất nhỏ, vì hiệu là một tổ hợp tuyến tính khác 0 của các độc lập. Tổng tiền tố cho phép tính của mọi đoạn trong .
Biến thể: XOR trọng số để kiểm “mỗi giá trị xuất hiện chẵn lần”; trọng số theo mô-đun 3 để kiểm số mũ chia hết cho 3; cộng trọng số vào cả hình chữ nhật để đánh dấu vùng.
Hỏi nhanh
Vì sao không băm đa tập bằng tổng các giá trị ?
Tổng thật là một hàm tuyến tính cố định, nên dựng được test đụng độ chủ ý rất dễ. Trọng số ngẫu nhiên làm đụng độ phụ thuộc vào lựa chọn lúc chạy, người ra test không đoán trước được.
Ví dụHai Đoạn Cùng Đa Tập
Đề. Hai dãy dài , giá trị trong . truy vấn: đoạn có hoán vị được thành không?
Lời giải. Gán cho mỗi giá trị một số ngẫu nhiên 64 bit h[v]. Hai đa tập bằng nhau thì tổng trọng số bằng nhau; khác nhau thì tổng trùng nhau chỉ với xác suất cỡ . Tổng trên một đoạn lấy từ tổng tiền tố.
Bước 1: tổng tiền tố trọng số. Kiểu ull nên tràn số chính là cộng theo mô-đun , không cần lấy dư.
for (ll v = 1; v <= n; v++) h[v] = rng();
for (ll i = 1; i <= n; i++) {
ll x;
cin >> x;
pa[i] = pa[i - 1] + h[x];
}
Bước 2: so. So độ dài và so hiệu hai tổng tiền tố, . So độ dài là cần thiết: hai đoạn khác độ dài không bao giờ hoán vị được thành nhau, dù tổng trọng số có tình cờ bằng nhau.
bool ok = r - l == R - L && pa[r] - pa[l - 1] == pb[R] - pb[L - 1];
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const ll N = 2e5 + 5;
ll n, q;
ull h[N], pa[N], pb[N];
mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> q;
for (ll v = 1; v <= n; v++) h[v] = rng();
for (ll i = 1; i <= n; i++) {
ll x;
cin >> x;
pa[i] = pa[i - 1] + h[x];
}
for (ll i = 1; i <= n; i++) {
ll x;
cin >> x;
pb[i] = pb[i - 1] + h[x];
}
while (q--) {
ll l, r, L, R;
cin >> l >> r >> L >> R;
bool ok = r - l == R - L && pa[r] - pa[l - 1] == pb[R] - pb[L - 1];
cout << (ok ? "Yes" : "No") << '\n';
}
}
8 6
1 2 2 3 4 1 5 3
2 1 3 2 4 5 1 3
1 4 1 4
2 5 2 5
5 7 5 7
1 3 6 8
6 8 6 8
1 2 1 3
Yes
No
Yes
No
Yes
No
.
Khi đi thi
- Đáp án do một phần tử chiếm tỉ lệ quyết định: lấy mẫu lần với nhỏ, kiểm lại chính xác từng ứng viên. Cộng xác suất sai trên mọi truy vấn trước khi chọn .
- So sánh đắt (tích ma trận, đa thức, đa tập): kiểm tại điểm hay trọng số ngẫu nhiên. Kiểm nguyên tố tới : Miller-Rabin 12 cơ số, nhân
__int128. - Dùng
mt19937hạt giống theo thời gian; không dùngrand()hay hạt giống cố định, nhất là ở nơi có thể bị hack.
Tóm tắt
- Lấy mẫu nhân chứng: xác suất trượt , ứng viên luôn được kiểm lại.
- Freivalds: trong , sai với xác suất mỗi lần.
- Miller-Rabin: phân tích , mỗi cơ số kiểm dãy bình phương; 12 cơ số đầu là chắc chắn cho 64 bit.
- Trọng số ngẫu nhiên biến so sánh đa tập thành so sánh một số.
Bài tập
rand7() thành số đều trong , chỉ nhận khi không vượt 40 rồi lấy dư 10.map lưu các ô đã bị đổi chỗ, mỗi lần lật chọn ngẫu nhiên trong phần còn lại.next.>; hỏi các vị trí ngẫu nhiên, công sai là các hiệu với phần tử lớn nhất.Đọc thêm
- Kiểm tra số nguyên tốVNOI WikiFermat và Miller-Rabin bằng tiếng Việt.
- Primality testscp-algorithmsFermat, Miller-Rabin và tập cơ số chắc chắn cho 64 bit.
- Randomized AlgorithmsUSACO GuideBộ sinh ngẫu nhiên đúng cách và bài luyện.
- Freivalds' algorithmWikipediaChứng minh xác suất sai.