Chciałbym dodać kolejną odpowiedź, oprócz mojej pierwszej odpowiedzi . Ta odpowiedź próbuje zminimalizować liczbę połączeń do rand5()
każdego połączenia rand7()
, aby zmaksymalizować wykorzystanie losowości. Oznacza to, że jeśli uważasz przypadkowość za cenny zasób, chcemy wykorzystać jego jak najwięcej, bez wyrzucania losowych elementów. Ta odpowiedź ma również pewne podobieństwa z logiką przedstawioną w odpowiedzi Iwana .
Entropia zmiennej losowej jest dobrze określona wielkość. Dla zmiennej losowej, która przyjmuje N stanów z jednakowymi prawdopodobieństwami (rozkład równomierny), entropia wynosi log 2 N. Zatem rand5()
ma około 2,32193 bitów entropii i rand7()
około 2,80735 bitów entropii. Jeśli mamy nadzieję zmaksymalizować wykorzystanie przypadkowości, musimy użyć wszystkich 2,32193 bitów entropii z każdego wywołania rand5()
i zastosować je do wygenerowania 2.80735 bitów entropii potrzebnych dla każdego wywołania do rand7()
. Podstawowym ograniczeniem jest zatem to, że nie możemy zrobić nic lepszego niż log (7) / log (5) = 1,20906 wywołań rand5()
na połączenie z rand7()
.
Dodatkowe uwagi: wszystkie logarytmy w tej odpowiedzi będą podstawą 2, chyba że określono inaczej. rand5()
zakłada się, że zwracają liczby z zakresu [0, 4] i rand7()
przyjmowane są, że zwracają liczby z zakresu [0, 6]. Dostosowanie zakresów odpowiednio do [1, 5] i [1, 7] jest banalne.
Więc jak to zrobimy? Generujemy nieskończenie precyzyjną losową liczbę rzeczywistą z przedziału od 0 do 1 (udawajmy, że możemy faktycznie obliczyć i zapisać tak nieskończenie dokładną liczbę - naprawimy to później). Możemy wygenerować taki numer, generując jego cyfry w bazie 5: wybieramy losową liczbę 0. a
1 a
2 a
3 ..., gdzie każda cyfra a i
jest wybierana przez wywołanie do rand5()
. Na przykład, jeśli nasz RNG wybrał i
dla wszystkich wartość a = 1 i
, to ignorując fakt, że nie jest to zbyt losowe, odpowiadałoby to rzeczywistej liczbie 1/5 + 1/5 2 + 1/5 3 + ... = 1/4 (suma szeregu geometrycznego).
Ok, więc wybraliśmy losową liczbę rzeczywistą od 0 do 1. Teraz twierdzę, że taka losowa liczba jest równomiernie rozłożona. Intuicyjnie jest to łatwe do zrozumienia, ponieważ każda cyfra została wybrana jednolicie, a liczba jest nieskończenie dokładna. Jednak formalny dowód na to jest nieco bardziej zaangażowany, ponieważ teraz mamy do czynienia z rozkładem ciągłym zamiast rozkładem dyskretnym, więc musimy udowodnić, że prawdopodobieństwo, że nasza liczba leży w przedziale [ a
, b
] jest równe długości że interwał b - a
. Dowód pozostaje jako ćwiczenie dla czytelnika =).
Teraz, gdy mamy losową liczbę rzeczywistą wybraną równomiernie z zakresu [0, 1], musimy przekonwertować ją na serię równomiernie losowych liczb z zakresu [0, 6], aby wygenerować wynik rand7()
. Jak to robimy? Po prostu odwrotność tego, co właśnie zrobiliśmy - konwertujemy go na nieskończenie dokładny dziesiętny w podstawie 7, a następnie każda podstawowa cyfra 7 odpowiada jednemu wyjściu rand7()
.
Biorąc przykład z wcześniejszego, jeśli nasz rand5()
wytworzy nieskończony strumień 1, nasza losowa liczba rzeczywista wyniesie 1/4. Konwertując 1/4 na podstawę 7, otrzymujemy nieskończoną liczbę dziesiętną 0,15151515 ..., więc będziemy produkować jako dane wyjściowe 1, 5, 1, 5, 1, 5 itd.
Ok, więc mamy główny pomysł, ale pozostały nam dwa problemy: nie jesteśmy w stanie obliczyć ani zapisać nieskończenie dokładnej liczby rzeczywistej, więc jak sobie z tym poradzić? Po drugie, jak faktycznie przekonwertować go na bazę 7?
Jednym ze sposobów konwersji liczby od 0 do 1 na bazę 7 jest:
- Pomnóż przez 7
- Integralną częścią wyniku jest następna podstawowa 7 cyfra
- Odejmij część integralną, pozostawiając tylko część ułamkową
- Idź do kroku 1
Aby poradzić sobie z problemem nieskończonej precyzji, obliczamy wynik częściowy, a także przechowujemy górną granicę możliwego wyniku. To znaczy, załóżmy, że zadzwoniliśmy rand5()
dwa razy i wrócił 1 razy. Dotychczas wygenerowaliśmy liczbę 0,11 (podstawa 5). Niezależnie od reszty nieskończonej serii wywołań do rand5()
wygenerowania, losowa liczba rzeczywista, którą generujemy, nigdy nie będzie większa niż 0,12: zawsze jest prawdą, że 0,11 ≤ 0,11xyz ... <0,12.
Tak więc, śledząc do tej pory bieżącą liczbę i maksymalną wartość, jaką kiedykolwiek mogła przyjąć, konwertujemy obie liczby na bazę 7. Jeśli zgadzają się co do pierwszych k
cyfr, możemy bezpiecznie wyprowadzić kolejne k
cyfry - niezależnie od tego, co nieskończony strumień podstawowych cyfr 5, nigdy nie wpłyną one na kolejne k
cyfry reprezentacji podstawowej 7!
I to jest algorytm - aby wygenerować następny wynik rand7()
, generujemy tylko tyle cyfr, rand5()
ile potrzebujemy, aby upewnić się, że znamy z pewnością wartość następnej cyfry w przeliczeniu losowej liczby rzeczywistej na bazę 7. Oto implementacja Python z testową wiązką:
import random
rand5_calls = 0
def rand5():
global rand5_calls
rand5_calls += 1
return random.randint(0, 4)
def rand7_gen():
state = 0
pow5 = 1
pow7 = 7
while True:
if state / pow5 == (state + pow7) / pow5:
result = state / pow5
state = (state - result * pow5) * 7
pow7 *= 7
yield result
else:
state = 5 * state + pow7 * rand5()
pow5 *= 5
if __name__ == '__main__':
r7 = rand7_gen()
N = 10000
x = list(next(r7) for i in range(N))
distr = [x.count(i) for i in range(7)]
expmean = N / 7.0
expstddev = math.sqrt(N * (1.0/7.0) * (6.0/7.0))
print '%d TRIALS' % N
print 'Expected mean: %.1f' % expmean
print 'Expected standard deviation: %.1f' % expstddev
print
print 'DISTRIBUTION:'
for i in range(7):
print '%d: %d (%+.3f stddevs)' % (i, distr[i], (distr[i] - expmean) / expstddev)
print
print 'Calls to rand5: %d (average of %f per call to rand7)' % (rand5_calls, float(rand5_calls) / N)
Zwróć uwagę, że rand7_gen()
zwraca generator, ponieważ ma stan wewnętrzny polegający na konwersji liczby na bazę 7. Wiązka testowa wywołuje next(r7)
10000 razy w celu wygenerowania 10000 liczb losowych, a następnie mierzy ich rozkład. Używana jest tylko matematyka liczb całkowitych, więc wyniki są dokładnie poprawne.
Zauważ też, że liczby tutaj stają się bardzo duże, bardzo szybkie. Moce 5 i 7 rosną szybko. Dlatego wydajność zacznie się zauważalnie obniżać po wygenerowaniu wielu liczb losowych z powodu arytmetyki bignum. Pamiętaj jednak, że moim celem było maksymalne wykorzystanie losowych bitów, a nie maksymalizacja wydajności (chociaż jest to cel drugorzędny).
W jednym z nich wykonałem 12091 wywołań rand5()
dla 10000 wywołań rand7()
, osiągając minimum wywołań log (7) / log (5) średnio do 4 cyfr znaczących, a wynikowy wynik był jednolity.
W celu portu to kod języka, który nie posiada dowolnie duże liczby całkowite wbudowany, musisz cap wartości pow5
i pow7
maksymalnej wartości swojej rodzimej zintegrowanym typu - jeśli staną się zbyt duże, a następnie zresetować wszystko i zacznij od nowa. Zwiększy rand5()
to rand7()
bardzo nieznacznie średnią liczbę wywołań na połączenie , ale mam nadzieję, że nie powinno to zbytnio wzrosnąć nawet dla liczb całkowitych 32- lub 64-bitowych.