Đoạn mã ví dụ này minh họa rằng đó std::rand
là một trường hợp của balderdash sùng bái hàng hóa cũ sẽ khiến bạn nhướng mày mỗi khi nhìn thấy nó.
Có một số vấn đề ở đây:
Hợp đồng mà mọi người thường cho rằng — ngay cả những linh hồn nghèo khó, những người không biết gì tốt hơn và sẽ không nghĩ ra nó một cách chính xác những điều khoản này — là rand
các mẫu từ phân phối đồng đều trên các số nguyên trong 0, 1, 2,… RAND_MAX
, và mỗi cuộc gọi mang lại một mẫu độc lập .
Vấn đề đầu tiên là hợp đồng giả định, các mẫu ngẫu nhiên đồng nhất độc lập trong mỗi cuộc gọi, không thực sự như những gì tài liệu nói — và trên thực tế, việc triển khai trong lịch sử đã không cung cấp ngay cả sự độc lập đơn giản nhất. Ví dụ, C99 §7.20.2.1 ' rand
Hàm' nói mà không cần giải thích:
Các rand
chức năng tính toán một chuỗi các số nguyên giả ngẫu nhiên trong khoảng từ 0 đến RAND_MAX
.
Đây là một câu vô nghĩa, bởi vì tính ngẫu nhiên giả là thuộc tính của một hàm (hoặc họ hàm ), không phải của một số nguyên, nhưng điều đó không ngăn được ngay cả các quan chức ISO lạm dụng ngôn ngữ này. Rốt cuộc, những độc giả duy nhất sẽ khó chịu vì nó biết tốt hơn là đọc tài liệu rand
vì sợ tế bào não của họ bị phân hủy.
Một triển khai lịch sử điển hình trong C hoạt động như thế này:
static unsigned int seed = 1;
static void
srand(unsigned int s)
{
seed = s;
}
static unsigned int
rand(void)
{
seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
return (int)seed;
}
Điều này có đặc tính đáng tiếc là mặc dù một mẫu đơn lẻ có thể được phân phối đồng đều dưới một hạt ngẫu nhiên đồng nhất (phụ thuộc vào giá trị cụ thể của RAND_MAX
), nó sẽ luân phiên giữa các số nguyên chẵn và lẻ trong các lần gọi liên tiếp — sau
int a = rand();
int b = rand();
biểu thức (a & 1) ^ (b & 1)
cho kết quả 1 với xác suất 100%, điều này không xảy ra đối với các mẫu ngẫu nhiên độc lập trên bất kỳ phân phối nào được hỗ trợ trên số nguyên chẵn và lẻ. Do đó, một giáo phái về hàng hóa xuất hiện rằng người ta nên loại bỏ các bit bậc thấp để đuổi theo con thú khó nắm bắt về 'tính ngẫu nhiên tốt hơn'. (Cảnh báo spoiler: Đây không phải là một thuật ngữ chuyên môn. Đây là một dấu hiệu cho thấy bất kỳ ai mà bạn đang đọc văn xuôi đều không biết họ đang nói về điều gì hoặc nghĩ rằng bạn không biết gì và cần phải hạ mình.)
Vấn đề thứ hai là ngay cả khi mỗi lệnh gọi lấy mẫu độc lập với phân phối ngẫu nhiên đồng nhất trên 0, 1, 2,…, RAND_MAX
thì kết quả của rand() % 6
sẽ không được phân phối đồng nhất trong 0, 1, 2, 3, 4, 5 giống như một con súc sắc cuộn, trừ khi RAND_MAX
đồng dư với -1 modulo 6. Ví dụ đếm đơn giản: Nếu RAND_MAX
= 6, thì từ rand()
, tất cả các kết quả có xác suất bằng nhau 1/7, nhưng từ rand() % 6
, kết quả 0 có xác suất 2/7 trong khi tất cả các kết quả khác có xác suất 1/7 .
Cách thích hợp để làm điều này là lấy mẫu từ chối: liên tục lấy mẫu ngẫu nhiên đồng nhất độc lập s
từ 0, 1, 2,… RAND_MAX
, và loại bỏ (ví dụ) các kết quả 0, 1, 2,…, ((RAND_MAX + 1) % 6) - 1
—nếu bạn nhận được một trong các những, bắt đầu lại; nếu không, hãy nhường s % 6
.
unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
continue;
return s % 6;
Bằng cách này, tập hợp các kết quả rand()
mà chúng ta chấp nhận sẽ chia đều cho 6 và mỗi kết quả có thể xảy ra từ đó s % 6
nhận được bởi cùng một số kết quả được chấp nhận từ đó rand()
, vì vậy nếu rand()
được phân phối đồng đều thì cũng vậy s
. Không có ràng buộc về số lần thử nghiệm, nhưng số lượng dự kiến nhỏ hơn 2 và xác suất thành công tăng theo cấp số nhân với số lần thử nghiệm.
Việc lựa chọn kết quả nào màrand()
bạn từ chối là không quan trọng, miễn là bạn ánh xạ một số lượng bằng nhau với mỗi số nguyên dưới 6. Mã tại cppreference.com đưa ra một lựa chọn khác , vì vấn đề đầu tiên ở trên — rằng không có gì được đảm bảo về phân phối hoặc sự độc lập của các đầu ra rand()
và trên thực tế, các bit bậc thấp thể hiện các mẫu không 'trông đủ ngẫu nhiên' (đừng nhớ rằng đầu ra tiếp theo là một hàm xác định của đầu ra trước đó).
Bài tập dành cho người đọc: Chứng minh rằng mã tại cppreference.com mang lại phân bố đồng đều trên các cuộn khuôn nếu rand()
mang lại phân phối đồng đều trên 0, 1, 2,… , RAND_MAX
.
Bài tập cho người đọc: Tại sao bạn có thể thích một hoặc các tập hợp con khác từ chối? Tính toán nào là cần thiết cho mỗi thử nghiệm trong hai trường hợp?
Một vấn đề thứ ba là không gian hạt giống quá nhỏ nên ngay cả khi hạt giống được phân phối đồng đều, một kẻ thù được trang bị kiến thức về chương trình của bạn và một kết quả nhưng không hạt giống có thể dễ dàng dự đoán hạt giống và các kết quả tiếp theo, điều này khiến chúng có vẻ không như vậy ngẫu nhiên sau cùng. Vì vậy, đừng nghĩ đến việc sử dụng điều này cho mật mã.
Bạn có thể đi theo con đường ưa thích và std::uniform_int_distribution
lớp học của C ++ 11 với một thiết bị ngẫu nhiên thích hợp và công cụ ngẫu nhiên yêu thích của bạn như twister Mersenne phổ biến từng được yêu thích std::mt19937
để chơi xúc xắc với người em họ bốn tuổi của bạn, nhưng ngay cả điều đó sẽ không có phù hợp để tạo ra mật mã chủ chốt vật liệu và Mersenne twister là một không gian con heo khủng khiếp quá với một đa kilobyte tình trạng tàn phá trên bộ nhớ cache của CPU của bạn với một thời gian thiết lập khiêu dâm, vì vậy nó là xấu ngay cả đối với, ví dụ như , song song mô phỏng Monte Carlo với cây tái tạo của các máy tính con; sự phổ biến của nó có lẽ chủ yếu xuất phát từ cái tên hấp dẫn của nó. Nhưng bạn có thể sử dụng nó để lăn xúc xắc đồ chơi như ví dụ này!
Một cách tiếp cận khác là sử dụng trình tạo số giả ngẫu nhiên mật mã đơn giản với trạng thái nhỏ, chẳng hạn như PRNG xóa khóa nhanh đơn giản hoặc chỉ một mật mã dòng như AES-CTR hoặc ChaCha20 nếu bạn tự tin ( ví dụ: trong mô phỏng Monte Carlo cho nghiên cứu trong khoa học tự nhiên) rằng không có hậu quả bất lợi nào đối với việc dự đoán kết quả trong quá khứ nếu trạng thái bị tổn hại.
std::uniform_int_distribution
cho xúc xắc