Given a function which produces a random integer in the range 1 to 5, write a function which produces a random integer in the range 1 to 7.
- What is a simple solution?
- What is an effective solution to reduce memory usage or run on a slower CPU?
Given a function which produces a random integer in the range 1 to 5, write a function which produces a random integer in the range 1 to 7.
(I have stolen Adam Rosenfeld's answer and made it run about 7% faster.)
Assume that rand5() returns one of {0,1,2,3,4} with equal distribution and the goal is return {0,1,2,3,4,5,6} with equal distribution.
We're keeping track of the largest value that the loop can make in the variable
max
. If the reult so far is between max%7 and max-1 then the result will be uniformly distrubuted in that range. If not, we use the remainder, which is random between 0 and max%7-1, and another call to rand() to make a new number and a new max. Then we start again.Edit: Expect number of times to call rand5() is x in this equation:
Algorithm:
7 can be represented in a sequence of 3 bits
Use rand(5) to randomly fill each bit with 0 or 1.
For e.g: call rand(5) and
if the result is 1 or 2, fill the bit with 0
if the result is 4 or 5, fill the bit with 1
if the result is 3 , then ignore and do it again (rejection)
This way we can fill 3 bits randomly with 0/1 and thus get a number from 1-7.
EDIT: This seems like the simplest and most efficient answer, so here's some code for it:
The premise behind Adam Rosenfield's correct answer is:
When n equals 2, you have 4 throw-away possibilities: y = {22, 23, 24, 25}. If you use n equals 6, you only have 1 throw-away: y = {15625}.
5^6 = 15625
7 * 2232 = 15624
You call rand5 more times. However, you have a much lower chance of getting a throw-away value (or an infinite loop). If there is a way to get no possible throw-away value for y, I haven't found it yet.
I'd like to add another answer, in addition to my first answer. This answer attempts to minimize the number of calls to
rand5()
per call torand7()
, to maximize the usage of randomness. That is, if you consider randomness to be a precious resource, we want to use as much of it as possible, without throwing away any random bits. This answer also has some similarities with the logic presented in Ivan's answer.The entropy of a random variable is a well-defined quantity. For a random variable which takes on N states with equal probabilities (a uniform distribution), the entropy is log2 N. Thus,
rand5()
has approximately 2.32193 bits of entropy, andrand7()
has about 2.80735 bits of entropy. If we hope to maximize our use of randomness, we need to use all 2.32193 bits of entropy from each call torand5()
, and apply them to generating 2.80735 bits of entropy needed for each call torand7()
. The fundamental limit, then, is that we can do no better than log(7)/log(5) = 1.20906 calls torand5()
per call torand7()
.Side notes: all logarithms in this answer will be base 2 unless specified otherwise.
rand5()
will be assumed to return numbers in the range [0, 4], andrand7()
will be assumed to return numbers in the range [0, 6]. Adjusting the ranges to [1, 5] and [1, 7] respectively is trivial.So how do we do it? We generate an infinitely precise random real number between 0 and 1 (pretend for the moment that we could actually compute and store such an infinitely precise number -- we'll fix this later). We can generate such a number by generating its digits in base 5: we pick the random number 0.
a
1a
2a
3..., where each digit ai
is chosen by a call torand5()
. For example, if our RNG chose ai
= 1 for alli
, then ignoring the fact that that isn't very random, that would correspond to the real number 1/5 + 1/52 + 1/53 + ... = 1/4 (sum of a geometric series).Ok, so we've picked a random real number between 0 and 1. I now claim that such a random number is uniformly distributed. Intuitively, this is easy to understand, since each digit was picked uniformly, and the number is infinitely precise. However, a formal proof of this is somewhat more involved, since now we're dealing with a continuous distribution instead of a discrete distribution, so we need to prove that the probability that our number lies in an interval [
a
,b
] equals the length of that interval,b - a
. The proof is left as an exercise for the reader =).Now that we have a random real number selected uniformly from the range [0, 1], we need to convert it to a series of uniformly random numbers in the range [0, 6] to generate the output of
rand7()
. How do we do this? Just the reverse of what we just did -- we convert it to an infinitely precise decimal in base 7, and then each base 7 digit will correspond to one output ofrand7()
.Taking the example from earlier, if our
rand5()
produces an infinite stream of 1's, then our random real number will be 1/4. Converting 1/4 to base 7, we get the infinite decimal 0.15151515..., so we will produce as output 1, 5, 1, 5, 1, 5, etc.Ok, so we have the main idea, but we have two problems left: we can't actually compute or store an infinitely precise real number, so how do we deal with only a finite portion of it? Secondly, how do we actually convert it to base 7?
One way we can convert a number between 0 and 1 to base 7 is as follows:
To deal with the problem of infinite precision, we compute a partial result, and we also store an upper bound on what the result could be. That is, suppose we've called
rand5()
twice and it returned 1 both times. The number we've generated so far is 0.11 (base 5). Whatever the rest of the infinite series of calls torand5()
produce, the random real number we're generating will never be larger than 0.12: it is always true that 0.11 ≤ 0.11xyz... < 0.12.So, keeping track of the current number so far, and the maximum value it could ever take, we convert both numbers to base 7. If they agree on the first
k
digits, then we can safely output the nextk
digits -- regardless of what the infinite stream of base 5 digits are, they will never affect the nextk
digits of the base 7 representation!And that's the algorithm -- to generate the next output of
rand7()
, we generate only as many digits ofrand5()
as we need to ensure that we know with certainty the value of the next digit in the conversion of the random real number to base 7. Here is a Python implementation, with a test harness:Note that
rand7_gen()
returns a generator, since it has internal state involving the conversion of the number to base 7. The test harness callsnext(r7)
10000 times to produce 10000 random numbers, and then it measures their distribution. Only integer math is used, so the results are exactly correct.Also note that the numbers here get very big, very fast. Powers of 5 and 7 grow quickly. Hence, performance will start to degrade noticeably after generating lots of random numbers, due to bignum arithmetic. But remember here, my goal was to maximize the usage of random bits, not to maximize performance (although that is a secondary goal).
In one run of this, I made 12091 calls to
rand5()
for 10000 calls torand7()
, achieving the minimum of log(7)/log(5) calls on average to 4 significant figures, and the resulting output was uniform.In order to port this code to a language that doesn't have arbitrarily large integers built-in, you'll have to cap the values of
pow5
andpow7
to the maximum value of your native integral type -- if they get too big, then reset everything and start over. This will increase the average number of calls torand5()
per call torand7()
very slightly, but hopefully it shouldn't increase too much even for 32- or 64-bit integers.Here's a solution that fits entirely within integers and is within about 4% of optimal (i.e. uses 1.26 random numbers in {0..4} for every one in {0..6}). The code's in Scala, but the math should be reasonably clear in any language: you take advantage of the fact that 7^9 + 7^8 is very close to 5^11. So you pick an 11 digit number in base 5, and then interpret it as a 9 digit number in base 7 if it's in range (giving 9 base 7 numbers), or as an 8 digit number if it's over the 9 digit number, etc.:
If you paste a test into the interpreter (REPL actually), you get:
The distribution is nice and flat (within about 10k of 1/7 of 10^8 in each bin, as expected from an approximately-Gaussian distribution).