Là java.util.Random thực sự ngẫu nhiên? Làm thế nào tôi có thể tạo 52! (giai thừa) trình tự có thể?


202

Tôi đã sử dụng Random (java.util.Random)để xáo trộn một bộ bài gồm 52 lá bài. Có 52! (8.0658175e + 67) khả năng. Tuy nhiên, tôi đã phát hiện ra rằng hạt giống cho java.util.Randomlà một long, nhỏ hơn nhiều ở 2 ^ 64 (1.8446744e + 19).

Từ đây, tôi nghi ngờ liệu java.util.Random có thực sự là ngẫu nhiên hay không ; nó thực sự có khả năng tạo ra tất cả 52! khả năng?

Nếu không, làm thế nào tôi có thể tạo ra một chuỗi ngẫu nhiên tốt hơn có thể tạo ra tất cả 52! khả năng?


21
"làm thế nào tôi chắc chắn có thể tạo ra một số ngẫu nhiên thực sự trên 52!" Các số từ Randomkhông bao giờ là số ngẫu nhiên thực sự . Đó là một PRNG, trong đó P là viết tắt của "giả". Đối với các số ngẫu nhiên thực , bạn cần một nguồn ngẫu nhiên (chẳng hạn như Random.org).
TJ Crowder

7
@JimGarrison Đó không phải là những gì sau OP. Anh ấy đang nói về 10 ^ 68 trình tự có thể. Vì mỗi chuỗi giả ngẫu nhiên được xác định bởi hạt giống của nó, OP cho biết có thể có nhiều nhất 2 ^ 64 chuỗi khác nhau.
dasblinkenlight

6
Tôi nghĩ đó là một câu hỏi thú vị và đáng để suy nghĩ. Nhưng tôi không thể tự hỏi về bối cảnh vấn đề của bạn: chính xác điều đó dẫn đến yêu cầu có thể tạo ra tất cả 52! hoán vị? Ví dụ, trong cây cầu trong thế giới thực, chúng ta có thể xáo trộn bộ bài và giải quyết một lá bài cùng một lúc, tuy nhiên chỉ có ~ 6e11 bàn tay khác nhau do nhiều hoán vị khác nhau dẫn đến cùng một bàn tay. Suy nghĩ theo hướng khác, bạn có cần một giải pháp cụ thể cho 52!, Hay bạn cần một giải pháp khái quát hóa, hai sàn được xáo trộn với nhau (104! / (2 ** 52), hoặc ~ 2e150)?
NPE

9
@NPE - Lấy Solitaire (Klondike) chẳng hạn, 52! chính xác là số bàn tay có thể ..
Serj Ardovic

3
Tôi nghĩ rằng đây là một bài đọc thú vị: superuser.com/a/712583
Dennis_E

Câu trả lời:


153

Chọn một hoán vị ngẫu nhiên đòi hỏi đồng thời nhiều hơn và ít ngẫu nhiên hơn những gì câu hỏi của bạn ngụ ý. Hãy để tôi giải thích.

Tin xấu: cần nhiều sự ngẫu nhiên.

Lỗ hổng cơ bản trong cách tiếp cận của bạn là nó đang cố gắng chọn giữa ~ 2 226 khả năng sử dụng 64 bit entropy (hạt giống ngẫu nhiên). Để lựa chọn một cách công bằng giữa ~ 2 226 khả năng bạn sẽ phải tìm cách tạo ra 226 bit entropy thay vì 64.

Có một số cách để tạo ra các bit ngẫu nhiên: phần cứng chuyên dụng , hướng dẫn CPU , giao diện hệ điều hành , dịch vụ trực tuyến . Đã có một giả định ngầm định trong câu hỏi của bạn rằng bằng cách nào đó bạn có thể tạo ra 64 bit, do đó, chỉ cần làm bất cứ điều gì bạn sẽ làm, chỉ bốn lần và quyên tặng các bit thừa cho tổ chức từ thiện. :)

Tin tốt: cần ít ngẫu nhiên.

Khi bạn có 226 bit ngẫu nhiên đó, phần còn lại có thể được thực hiện một cách xác định và do đó các thuộc tính của java.util.Randomcó thể được thực hiện không liên quan . Đây là cách làm.

Hãy nói rằng chúng tôi tạo ra tất cả 52! hoán vị (chịu đựng với tôi) và sắp xếp chúng theo từ vựng.

Để chọn một trong những hoán vị, tất cả những gì chúng ta cần là một số nguyên ngẫu nhiên duy nhất giữa 052!-1. Số nguyên đó là 226 bit của entropy. Chúng tôi sẽ sử dụng nó như một chỉ mục vào danh sách hoán vị được sắp xếp của chúng tôi. Nếu chỉ số ngẫu nhiên được phân bố đều, không chỉ là bạn đảm bảo rằng tất cả các hoán vị có thể được lựa chọn, họ sẽ được chọn equiprobably (mà là một đảm bảo mạnh hơn những gì các câu hỏi được hỏi).

Bây giờ, bạn thực sự không cần phải tạo ra tất cả những hoán vị đó. Bạn có thể sản xuất trực tiếp, với vị trí được chọn ngẫu nhiên trong danh sách được sắp xếp giả thuyết của chúng tôi. Điều này có thể được thực hiện trong thời gian O (n 2 ) thời gian sử dụng Lehmer [1] đang (xem thêm hoán vị đánh sốhệ thống số factoriadic ). N ở đây là kích thước của bộ bài của bạn, tức là 52.

Có một triển khai C trong câu trả lời StackOverflow này . Có một số biến số nguyên ở đó sẽ tràn cho n = 52, nhưng may mắn là trong Java bạn có thể sử dụng java.math.BigInteger. Phần còn lại của các tính toán có thể được phiên mã gần như là:

public static int[] shuffle(int n, BigInteger random_index) {
    int[] perm = new int[n];
    BigInteger[] fact = new BigInteger[n];
    fact[0] = BigInteger.ONE;
    for (int k = 1; k < n; ++k) {
        fact[k] = fact[k - 1].multiply(BigInteger.valueOf(k));
    }

    // compute factorial code
    for (int k = 0; k < n; ++k) {
        BigInteger[] divmod = random_index.divideAndRemainder(fact[n - 1 - k]);
        perm[k] = divmod[0].intValue();
        random_index = divmod[1];
    }

    // readjust values to obtain the permutation
    // start from the end and check if preceding values are lower
    for (int k = n - 1; k > 0; --k) {
        for (int j = k - 1; j >= 0; --j) {
            if (perm[j] <= perm[k]) {
                perm[k]++;
            }
        }
    }

    return perm;
}

public static void main (String[] args) {
    System.out.printf("%s\n", Arrays.toString(
        shuffle(52, new BigInteger(
            "7890123456789012345678901234567890123456789012345678901234567890"))));
}

[1] Đừng nhầm lẫn với Lehrer . :)


7
Heh, và tôi chắc chắn rằng liên kết ở cuối sẽ là New Math . :-)
TJ Crowder

5
@TJCrowder: Nó gần như là vậy! Đó là đa tạp Riemannian vô cùng khác biệt đã vung nó. :-)
NPE

2
Thật tốt khi thấy mọi người đánh giá cao kinh điển. :-)
TJ Crowder

3
Nơi nào bạn nhận được 226 bit ngẫu nhiên trong Java ? Xin lỗi, mã của bạn không trả lời điều đó.
Thorsten S.

5
Tôi không hiểu ý của bạn, Java Random () sẽ không cung cấp 64 bit entropy. OP ngụ ý một nguồn không xác định có thể tạo ra 64 bit để tạo PRNG. Thật hợp lý khi giả định rằng bạn có thể yêu cầu cùng một nguồn cho 226 bit.
Ngừng làm hại Monica

60

Phân tích của bạn là chính xác: gieo một trình tạo số giả ngẫu nhiên với bất kỳ hạt giống cụ thể nào cũng phải mang lại trình tự tương tự sau khi xáo trộn, giới hạn số lượng hoán vị mà bạn có thể đạt được đến 2 64 . Khẳng định này dễ dàng xác minh bằng thực nghiệm bằng cách gọi Collection.shufflehai lần, chuyển một Randomđối tượng được khởi tạo với cùng một hạt giống và quan sát thấy hai lần xáo trộn ngẫu nhiên giống hệt nhau.

Sau đó, một giải pháp cho vấn đề này là sử dụng một trình tạo số ngẫu nhiên cho phép hạt giống lớn hơn. Java cung cấp SecureRandomlớp có thể được khởi tạo với byte[]mảng có kích thước hầu như không giới hạn. Sau đó bạn có thể vượt qua một thể hiện của SecureRandomđể Collections.shufflehoàn thành nhiệm vụ:

byte seed[] = new byte[...];
Random rnd = new SecureRandom(seed);
Collections.shuffle(deck, rnd);

8
Chắc chắn, một hạt giống lớn không phải là một đảm bảo rằng tất cả 52! khả năng sẽ được tạo ra (đó là những gì câu hỏi này cụ thể về)? Là một thử nghiệm tư duy, hãy xem xét một PRNG bệnh lý lấy một hạt lớn tùy ý và tạo ra một chuỗi số không dài vô tận. Có vẻ như khá rõ ràng rằng PRNG cần phải đáp ứng nhiều yêu cầu hơn là chỉ lấy một hạt giống đủ lớn.
NPE

2
@SerjArdovic Có, mọi tài liệu hạt giống được truyền cho đối tượng SecureRandom phải không thể đoán trước được, theo tài liệu Java.
dasblinkenlight

10
@NPE Bạn nói đúng, mặc dù hạt giống quá nhỏ là sự đảm bảo cho giới hạn trên, nhưng hạt giống đủ lớn không được đảm bảo ở giới hạn dưới. Tất cả điều này là loại bỏ giới hạn trên lý thuyết, khiến RNG có thể tạo ra tất cả 52! kết hợp.
dasblinkenlight

5
@SerjArdovic Số byte nhỏ nhất cần có là 29 (bạn cần 226 bit để biểu diễn 52! Các tổ hợp bit có thể, là 28,25 byte, vì vậy chúng ta phải làm tròn nó). Lưu ý rằng việc sử dụng 29 byte vật liệu hạt giống sẽ loại bỏ giới hạn trên lý thuyết về số lượng xáo trộn bạn có thể nhận được, mà không thiết lập giới hạn dưới (xem nhận xét của NPE về RNG xảo quyệt lấy hạt rất lớn và tạo ra một chuỗi các số không).
dasblinkenlight

8
Việc SecureRandomthực hiện gần như chắc chắn sẽ sử dụng một PRNG cơ bản. Và nó phụ thuộc vào thời kỳ của PRNG (và ở mức độ thấp hơn, độ dài trạng thái) liệu nó có khả năng lựa chọn trong số 52 hoán vị giai thừa hay không. (Lưu ý rằng tài liệu nói rằng việc SecureRandomtriển khai "tuân thủ tối thiểu" một số thử nghiệm thống kê nhất định và tạo ra kết quả đầu ra "phải mạnh về mặt mật mã", nhưng không đặt giới hạn thấp hơn rõ ràng về độ dài trạng thái của PRNG hoặc trong giai đoạn của nó.)
Peter O.

26

Nói chung, trình tạo số giả ngẫu nhiên (PRNG) không thể chọn trong số tất cả các hoán vị của danh sách 52 mục nếu độ dài trạng thái của nó nhỏ hơn 226 bit.

java.util.Randomthực hiện một thuật toán với mô đun 2 48 ; do đó, độ dài trạng thái của nó chỉ là 48 bit, ít hơn nhiều so với 226 bit mà tôi đã đề cập. Bạn sẽ cần sử dụng một PRNG khác có độ dài trạng thái lớn hơn - cụ thể là một PRNG có thời gian từ 52 giai đoạn trở lên.

Xem thêm "Xáo trộn" trong bài viết của tôi về trình tạo số ngẫu nhiên .

Việc xem xét này độc lập với bản chất của PRNG; nó áp dụng như nhau cho các PRNG mã hóa và phi mã hóa (tất nhiên, các PRNG không mã hóa là không phù hợp mỗi khi có bảo mật thông tin).


Mặc dù java.security.SecureRandomcho phép các hạt có chiều dài không giới hạn được truyền vào, việc SecureRandomtriển khai có thể sử dụng PRNG cơ bản (ví dụ: "SHA1PRNG" hoặc "DRBG"). Và nó phụ thuộc vào thời kỳ của PRNG (và ở mức độ thấp hơn, độ dài trạng thái) liệu nó có khả năng lựa chọn trong số 52 hoán vị giai thừa hay không. (Lưu ý rằng tôi định nghĩa "độ dài trạng thái" là "kích thước tối đa của hạt giống mà PRNG có thể thực hiện để khởi tạo trạng thái của nó mà không cần rút ngắn hoặc nén hạt giống đó ").


18

Hãy để tôi xin lỗi trước, bởi vì điều này hơi khó hiểu ...

Trước hết, bạn đã biết rằng java.util.Randomhoàn toàn không phải là ngẫu nhiên. Nó tạo ra các chuỗi theo một cách hoàn toàn có thể dự đoán được từ hạt giống. Bạn hoàn toàn chính xác rằng, vì hạt giống chỉ dài 64 bit, nên nó chỉ có thể tạo ra 2 ^ 64 chuỗi khác nhau. Nếu bạn bằng cách nào đó tạo ra 64 bit ngẫu nhiên thực và sử dụng chúng để chọn một hạt giống, bạn không thể sử dụng hạt giống đó để chọn ngẫu nhiên giữa tất cả 52! trình tự có thể với xác suất bằng nhau.

Tuy nhiên, thực tế này không có kết quả, miễn là bạn không thực sự tạo ra nhiều hơn 2 ^ 64 chuỗi, miễn là không có gì 'đặc biệt' hoặc 'đặc biệt đáng chú ý' về chuỗi 2 ^ 64 mà nó có thể tạo ra .

Hãy nói rằng bạn có PRNG tốt hơn nhiều, sử dụng hạt 1000 bit. Hãy tưởng tượng bạn có hai cách để khởi tạo nó - một cách sẽ khởi tạo nó bằng cách sử dụng toàn bộ hạt giống và một cách sẽ băm hạt giống xuống 64 bit trước khi khởi tạo nó.

Nếu bạn không biết trình khởi tạo nào, bạn có thể viết bất kỳ loại thử nghiệm nào để phân biệt chúng không? Trừ khi bạn (chưa) đủ may mắn để kết thúc việc khởi tạo cái xấu với cùng 64 bit hai lần, thì câu trả lời là không. Bạn không thể phân biệt giữa hai trình khởi tạo mà không có một số kiến ​​thức chi tiết về một số điểm yếu trong việc triển khai PRNG cụ thể.

Ngoài ra, hãy tưởng tượng rằng Randomlớp có một mảng gồm 2 ^ 64 chuỗi được chọn hoàn toàn và ngẫu nhiên tại một thời điểm trong quá khứ xa xôi và rằng hạt giống chỉ là một chỉ mục trong mảng này.

Vì vậy, việc Randomchỉ sử dụng 64 bit cho hạt giống của nó thực sự không nhất thiết là một vấn đề thống kê, miễn là không có khả năng đáng kể rằng bạn sẽ sử dụng cùng một hạt giống hai lần.

Tất nhiên, đối với các mục đích mã hóa , một hạt giống 64 bit là không đủ, bởi vì việc một hệ thống sử dụng cùng một hạt giống hai lần là khả thi về mặt tính toán.

BIÊN TẬP:

Tôi nên thêm rằng, mặc dù tất cả những điều trên là chính xác, rằng việc thực hiện thực tế java.util.Randomkhông phải là tuyệt vời. Nếu bạn đang viết một trò chơi bài, có thể sử dụng MessageDigestAPI để tạo hàm băm SHA-256 "MyGameName"+System.currentTimeMillis()và sử dụng các bit đó để xáo trộn bộ bài. Theo lập luận trên, miễn là người dùng của bạn không thực sự đánh bạc, bạn không phải lo lắng currentTimeMillisvề việc trả lại lâu. Nếu người dùng của bạn đang thực sự đánh bạc, sau đó sử dụng SecureRandomkhông có hạt.


6
@ThorstenS, làm thế nào bạn có thể viết bất kỳ loại bài kiểm tra nào có thể xác định rằng có những kết hợp thẻ không bao giờ có thể đưa ra?
Matt Timmermans

2
Có một số bộ thử nghiệm số ngẫu nhiên như Diehard từ George Marsaglia hoặc TestU01 từ Pierre Bencuyer / Richard Simard dễ dàng tìm thấy sự bất thường về thống kê trong đầu ra ngẫu nhiên. Để kiểm tra thẻ bạn có thể sử dụng hai hình vuông. Bạn xác định thứ tự thẻ. Hình vuông đầu tiên hiển thị vị trí của hai thẻ đầu tiên là cặp xy: Thẻ đầu tiên là x và vị trí chênh lệch (!) (-26-25) của thẻ thứ hai là y. Hình vuông thứ hai hiển thị thẻ thứ 3 và thứ 4 với (-25-25) so với thứ 2/3. Điều này sẽ hiển thị ngay các khoảng trống và cụm trong phân phối của bạn nếu bạn chạy nó trong một thời gian.
Thorsten S.

4
Chà, đó không phải là bài kiểm tra mà bạn nói bạn có thể viết, nhưng nó cũng không áp dụng. Tại sao bạn cho rằng có những khoảng trống và cụm trong phân phối mà các thử nghiệm như vậy sẽ phát hiện ra? Điều đó có nghĩa là "điểm yếu cụ thể trong việc thực hiện PRNG" như tôi đã đề cập, và không liên quan gì đến số lượng hạt giống có thể. Các thử nghiệm như vậy thậm chí không yêu cầu bạn đặt lại máy phát điện. Tôi đã cảnh báo ngay từ đầu rằng điều này thật khó hiểu.
Matt Timmermans

3
@ThorstenS. Các bộ kiểm tra đó hoàn toàn sẽ không xác định xem nguồn của bạn là PRNG có mã hóa được bảo mật bằng 64 bit hay RNG thực sự. (Rốt cuộc, kiểm tra PRNG là những gì các bộ đó dành cho.) Ngay cả khi bạn biết thuật toán được sử dụng, một PRNG tốt sẽ không thể xác định trạng thái mà không cần tìm kiếm vũ trụ của không gian trạng thái.
Sneftel

1
@ThorstenS.: Trong một bộ bài thực sự, phần lớn các kết hợp sẽ không bao giờ xuất hiện. Bạn chỉ không biết đó là những gì. Đối với một PRNG nửa vời, nó cũng tương tự - nếu bạn có thể kiểm tra xem một chuỗi đầu ra nhất định có dài trong hình ảnh của nó hay không, đó là một lỗ hổng trong PRNG. Vô cùng lớn nhà nước / thời kỳ như 52! là không cần thiết; 128-bit nên đủ.
R .. GitHub DỪNG GIÚP ICE

10

Tôi sẽ có một chút khác biệt về vấn đề này. Bạn đúng với giả định của mình - PRNG của bạn sẽ không thể đạt được tất cả 52! khả năng.

Câu hỏi là: quy mô của trò chơi bài của bạn là gì?

Nếu bạn đang làm một trò chơi kiểu klondike đơn giản? Sau đó, bạn chắc chắn không cần tất cả 52! khả năng. Thay vào đó, hãy nhìn nó như thế này: một người chơi sẽ có 18 triệu trò chơi riêng biệt. Ngay cả khi tính toán cho 'Vấn đề sinh nhật', họ sẽ phải chơi hàng tỷ tay trước khi họ tham gia vào trò chơi trùng lặp đầu tiên.

Nếu bạn đang thực hiện một mô phỏng monte-carlo? Thì có lẽ bạn ổn. Bạn có thể phải đối phó với các vật phẩm do 'P' trong PRNG, nhưng có lẽ bạn sẽ không gặp phải vấn đề chỉ vì không gian hạt giống thấp (một lần nữa, bạn đang xem xét các khả năng độc đáo.) mặt trái, nếu bạn đang làm việc với số lần lặp lớn, thì, vâng, không gian hạt giống thấp của bạn có thể là một công cụ giải quyết.

Nếu bạn đang thực hiện một trò chơi nhiều người chơi, đặc biệt là nếu có tiền trên mạng? Sau đó, bạn sẽ cần phải làm một số điều về cách các trang web poker trực tuyến xử lý cùng một vấn đề mà bạn đang hỏi về. Bởi vì trong khi vấn đề không gian hạt giống thấp không đáng chú ý đối với người chơi trung bình, thì có thể khai thác nếu nó đáng để đầu tư thời gian. (Tất cả các trang web poker đều trải qua giai đoạn mà PRNG của họ bị 'hack', cho phép ai đó nhìn thấy các thẻ lỗ của tất cả những người chơi khác, chỉ bằng cách suy ra hạt giống từ các thẻ bị lộ.) Nếu đây là tình huống bạn gặp phải, đừng 't chỉ cần tìm một PRNG tốt hơn - bạn sẽ cần phải đối xử với nó một cách nghiêm túc như một vấn đề Crypto.


9

Giải pháp ngắn mà về cơ bản là giống nhau của dasblinkenlight:

// Java 7
SecureRandom random = new SecureRandom();
// Java 8
SecureRandom random = SecureRandom.getInstanceStrong();

Collections.shuffle(deck, random);

Bạn không cần phải lo lắng về trạng thái nội bộ. Giải thích dài dòng tại sao:

Khi bạn tạo một SecureRandomcá thể theo cách này, nó sẽ truy cập vào một trình tạo số ngẫu nhiên thực sự cụ thể của hệ điều hành. Đây là một nhóm entropy nơi các giá trị được truy cập có chứa các bit ngẫu nhiên (ví dụ: đối với bộ định thời nano giây, độ chính xác của nano giây về cơ bản là ngẫu nhiên) hoặc bộ tạo số phần cứng bên trong.

Đầu vào này (!) Vẫn có thể chứa dấu vết giả được đưa vào hàm băm mật mã mạnh để loại bỏ các dấu vết đó. Đó là lý do những CSPRNG đó được sử dụng, không phải để tự tạo ra những con số đó! Các SecureRandomcó một bộ đếm mà dấu vết bao nhiêu bit được sử dụng ( getBytes(), getLong()vv) và nạp các SecureRandomvới bit entropy khi cần thiết .

Tóm lại: Đơn giản chỉ cần quên phản đối và sử dụng SecureRandomnhư trình tạo số ngẫu nhiên thực sự.


4

Nếu bạn coi số đó chỉ là một mảng bit (hoặc byte) thì có lẽ bạn có thể sử dụng các Random.nextBytesgiải pháp (Bảo mật) được đề xuất trong câu hỏi Stack Overflow này , sau đó ánh xạ mảng thành a new BigInteger(byte[]).


3

Một thuật toán rất đơn giản là áp dụng SHA-256 cho một chuỗi các số nguyên tăng dần từ 0 trở lên. (Một muối có thể được thêm vào nếu muốn "lấy một chuỗi khác".) Nếu chúng ta giả sử rằng đầu ra của SHA-256 là "tốt như" các số nguyên phân bố đồng đều giữa 0 và 2 256 - 1 thì chúng ta có đủ entropy cho bài tập.

Để có được hoán vị từ đầu ra của SHA256 (khi được biểu thị dưới dạng số nguyên), người ta chỉ cần giảm modulo 52, 51, 50 ... như trong mã giả này:

deck = [0..52]
shuffled = []
r = SHA256(i)

while deck.size > 0:
    pick = r % deck.size
    r = floor(r / deck.size)

    shuffled.append(deck[pick])
    delete deck[pick]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.