[java] java.util.Random이 실제로 무작위입니까? 52를 어떻게 생성 할 수 있습니까! (계통적) 가능한 순서?

Random (java.util.Random)52 장의 카드 덱을 섞는 데 사용 하고 있습니다. 52가 있습니다! (8.0658175e + 67) 가능성. 그러나, 나는 씨앗 java.util.Randomlong2 ^ 64 (1.8446744e + 19)에서 훨씬 작은 것을 발견했습니다 .

여기에서 나는 java.util.Random 정말 그 랜덤 인지 의심 스럽다 . 실제로 모든 52 개를 생성 할 수 있습니까? 가능성?

그렇지 않은 경우 52 개를 모두 생성 할 수있는 더 나은 무작위 시퀀스를 어떻게 안정적으로 생성 할 수 있습니까? 가능성?



답변

랜덤 순열을 선택하려면 질문에서 의미하는 것보다 더 많은 랜덤이 필요합니다. 설명하겠습니다.

나쁜 소식은 더 많은 무작위성이 필요하다는 것입니다.

귀하의 접근 방식의 근본적인 결함은 64 비트의 엔트로피 (임의의 시드)를 사용하여 ~ 2 226 가능성 중에서 선택하려고한다는 것 입니다. ~ 2 226 개의 가능성 을 공정하게 선택하려면 64 대신 226 비트의 엔트로피를 생성하는 방법을 찾아야합니다.

랜덤 비트를 생성하는 방법에는 전용 하드웨어 , CPU 명령어 , OS 인터페이스 , 온라인 서비스 등 여러 가지가 있습니다 . 당신의 질문에는 이미 64 비트를 생성 할 수 있다는 암시 적 가정이 이미 있습니다. 따라서 4 번만 수행하고 초과 비트를 자선 단체에 기부하십시오. 🙂

좋은 소식은 적은 무작위성이 필요하다는 것입니다.

226 개의 임의 비트가 있으면 나머지를 결정 론적으로 수행 할 수 있으므로 속성을 java.util.Random무의미하게 만들 수 있습니다 . 방법은 다음과 같습니다.

52 개를 모두 생성한다고 가정 해 봅시다! 순열 (나를 지니고)을 사전 식으로 정렬합니다.

순열 중 하나를 선택 모든 우리의 필요 사이에 단일 무작위 정수 052!-1. 그 정수는 우리의 226 비트 엔트로피입니다. 정렬 된 순열 목록에 대한 색인으로 사용합니다. 랜덤 인덱스가 균일하게 분포 된 경우 모든 순열을 선택할 수 있다고 보장 할뿐만 아니라 동등 하게 선택됩니다 (질문이 요구하는 것보다 더 강력한 보증 임).

이제 실제로 이러한 순열을 모두 생성 할 필요는 없습니다. 가상 정렬 목록에서 임의로 선택된 위치를 고려하여 직접 하나를 생성 할 수 있습니다. 이는 Lehmer [1] 코드를 사용하여 O (n 2 ) 시간 내에 수행 할 수 있습니다 ( 번호 매기기 순열계승 수 시스템 참조 ). 여기서 n은 갑판의 크기, 즉 52입니다.

StackOverflow 답변 에는 C 구현이 있습니다. n = 52에 오버플로되는 몇 가지 정수 변수가 있지만 운 좋게도 Java에서는을 사용할 수 있습니다 java.math.BigInteger. 나머지 계산은 거의 그대로 기록 될 수 있습니다.

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] Lehrer 와 혼동하지 마십시오 . 🙂


답변

분석이 정확합니다. 특정 시드가있는 의사 난수 생성기를 시드하면 셔플 후 동일한 시퀀스를 생성해야하므로 얻을 수있는 순열의 수는 2 64로 제한 됩니다. 이 어설 션은 두 번 호출 하고 동일한 시드로 초기화 된 객체를 전달 하고 두 개의 임의 셔플이 동일한 지 관찰하여 실험적 으로 쉽게 확인할 수 있습니다.Collection.shuffleRandom

따라서 이에 대한 해결책은 더 큰 시드를 허용하는 난수 생성기를 사용하는 것입니다. Java는 사실상 무제한 크기의 배열 SecureRandom로 초기화 될 수있는 클래스를 제공합니다 byte[]. 당신은 인스턴스를 전달할 수 SecureRandom에 대한 Collections.shuffle작업을 완료하려면 :

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


답변

일반적으로 의사 난수 생성기 (PRNG)는 상태 길이가 226 비트보다 작은 경우 52 개 항목 목록의 모든 순열 중에서 선택할 수 없습니다.

java.util.Random모듈러스가 2 48 인 알고리즘을 구현하고 ; 따라서 상태 길이는 48 비트이므로 내가 언급 한 226 비트보다 훨씬 적습니다. 상태 길이가 더 큰 다른 PRNG를 사용해야합니다. 특히주기가 52 배 이상인 PRNG를 사용해야합니다.

난수 생성기에 대한기사의 “셔플 링”도 참조하십시오 .

이 고려 사항은 PRNG의 특성과 무관합니다. 이는 암호화 및 비 암호화 PRNG에 동일하게 적용됩니다 (물론 정보 암호화가 관련 될 때마다 비 암호화 PRNG는 부적절합니다).


java.security.SecureRandom길이가 무제한 인 시드를 전달할 수 있지만 SecureRandom구현시 기본 PRNG (예 : “SHA1PRNG”또는 “DRBG”)를 사용할 수 있습니다. 그리고 그것은 PRNG의 기간 (그리고 그보다 작은 경우, 상태 길이)에 따라 52 개의 요인 순열 중에서 선택할 수 있는지에 달려 있습니다. ( “상태 길이” 를 “PRNG가 해당 시드를 줄이거 나 압축하지 않고 상태를 초기화하기 위해 취할 수있는 시드의 최대 크기 정의 합니다.)


답변

이해하기 조금 어렵 기 때문에 미리 사과 드리겠습니다 …

우선, 당신은 이미 java.util.Random완전히 무작위가 아님을 알고 있습니다. 종자에서 완벽하게 예측 가능한 방식으로 서열을 생성합니다. 시드의 길이는 64 비트이므로 2 ^ 64 개의 서로 다른 시퀀스 만 생성 할 수 있습니다. 어떻게 든 64 개의 실제 랜덤 비트를 생성하고이를 사용하여 시드를 선택했다면 해당 시드를 사용 하여 52 개 모두 를 임의로 선택할 수 없었습니다 ! 동일한 확률로 가능한 서열.

그러나이 사실은 중요하지의 당신이 실제로 한 것도 ‘특별한’또는 존재하는 한, 2 개 이상의 ^ 64 개 시퀀스를 생성하지 않을거야만큼 ‘눈에 띄게 특별한’2 ^ 64 시퀀스에 대해이 있다고 할 수 생성 .

1000 비트 시드를 사용하는 PRNG가 훨씬 우수하다고 가정 해 보겠습니다. 초기화하는 두 가지 방법이 있다고 가정하십시오. 한 가지 방법은 전체 시드를 사용하여 초기화하고 다른 방법은 시드를 초기화하기 전에 64 비트로 해시합니다.

어떤 이니셜 라이저가 어떤 것인지 모르는 경우 구별 할 수있는 테스트를 작성할 수 있습니까? 같은 64 비트로 나쁜 것을 초기화 할 정도로 운이 좋지 않으면 대답은 ‘아니요’입니다. 특정 PRNG 구현의 약점에 대한 자세한 지식이 없으면 두 이니셜 라이저를 구별 할 수 없습니다.

또는 Random클래스에 2 ^ 64 시퀀스의 배열이 있으며 먼 과거에 어느 시점에서 완전히 무작위로 선택되었으며 시드 가이 배열에 대한 색인 일뿐이라고 상상해보십시오 .

따라서 동일한 시드를 두 번 사용할 가능성이 크지 않다면Random 시드에 64 비트 만 사용 한다는 사실이 통계적으로 문제가되지는 않습니다.

물론, 암호화 목적 상, 시스템이 동일한 시드를 두 번 사용하도록하는 것은 계산 상 가능하기 때문에 64 비트 시드로는 충분하지 않습니다.

편집하다:

위의 모든 내용이 정확하더라도 실제 구현이 java.util.Random훌륭하지 않다는 것을 추가해야합니다 . 카드 게임을 작성하는 경우 MessageDigestAPI를 사용하여 의 SHA-256 해시를 생성하고 "MyGameName"+System.currentTimeMillis()해당 비트를 사용하여 덱을 섞습니다. 위의 주장에 따르면, 사용자가 실제로 도박을하지 않는 currentTimeMillis한 오래 반환 한다고 걱정할 필요가 없습니다 . 사용자 실제로 도박을하는 SecureRandom경우 시드없이 사용하십시오 .


답변

나는 이것에 대해 약간 다른 압정을 취할 것입니다. 당신은 당신의 가정에 맞습니다-당신의 PRNG는 52를 모두 칠 수 없을 것입니다! 가능성.

문제는 카드 게임의 규모는 무엇입니까?

간단한 클론 다이크 스타일 게임을 만들고 있다면? 그럼 당신은 확실히 모든 52 필요 하지 않습니다 ! 가능성. 대신, 다음과 같이보십시오 : 플레이어는 18 개의 quintillion 별개의 게임을 갖게됩니다 . 심지어 ‘생일 문제’를 설명하더라도, 그들은 첫 번째 복제 게임을 시작하기 전에 수십억의 손을 쳐야했습니다.

몬테카를로 시뮬레이션을하고 있다면? 그렇다면 당신은 아마 괜찮을 것입니다. PRNG의 ‘P’로 인해 인공물을 처리해야 할 수도 있지만 낮은 시드 공간으로 인해 문제가 발생하지 않을 수도 있습니다 (다시 말해서 고유 한 가능성의 5 분의 1을보고 있습니다). 반대로, 반복 횟수가 큰 작업을 수행하는 경우 시드 공간이 적을 때 거래가 중단 될 수 있습니다.

멀티 플레이어 카드 게임을하고 있다면, 특히 돈이 많이 있다면? 그런 다음 온라인 포커 사이트가 요청한 것과 동일한 문제를 어떻게 처리했는지에 대해 인터넷 검색을해야합니다. 낮은 시드 공간 문제는 일반적인 플레이어 에게는 눈에 띄지 않지만 시간 투자 가치가 있다면 악용 될 수 있습니다. (포커 사이트는 모두 PRNG가 ‘해킹’된 단계를 거치며 노출 된 카드에서 시드를 추론하여 다른 플레이어의 홀 카드를 볼 수있게합니다.) 이것이 현재 상황이라면, 돈 ‘t는 당신은 암호화 문제로 심각하게 취급해야합니다 – 단순히 더 나은 PRNG를 찾을 수 있습니다.


답변

본질적으로 dasblinkenlight와 동일한 짧은 솔루션 :

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

Collections.shuffle(deck, random);

내부 상태에 대해 걱정할 필요가 없습니다. 왜 긴 설명 :

SecureRandom이 방법으로 인스턴스 를 만들면 OS 별 실제 난수 생성기에 액세스합니다. 임의의 비트를 포함하는 값에 액세스하는 엔트로피 풀 (예 : 나노초 타이머의 경우 나노초 정밀도는 본질적으로 임의 임) 또는 내부 하드웨어 번호 생성기입니다.

여전히 가짜 트레이스를 포함 할 수있는이 입력 (!)은 해당 트레이스를 제거하는 강력한 암호화 해시로 공급됩니다. 그것이 바로 CSPRNG가 사용되는 이유입니다. 는 SecureRandom많은 비트가 (사용 된 방법을 추적 카운터 갖고 getBytes(), getLong()등) 및 리필 SecureRandom엔트로피 비트를 필요 .

한마디로 : 이의 제기를 잊고 SecureRandom진정한 난수 생성기로 사용하십시오.


답변

숫자를 비트 (또는 바이트)의 배열로 간주하면 Random.nextBytes스택 오버플로 질문 에서 제안 된 (보안) 솔루션 을 사용한 다음 배열을에 매핑 할 수 new BigInteger(byte[])있습니다.