LeetCode 470 - Implement Rand10() Using Rand7()
The problem provides access to a single API, rand7(), which returns a uniformly random integer from 1 to 7. The task is to implement another function, rand10(), which must return a uniformly random integer from 1 to 10.
Difficulty: 🟡 Medium
Topics: Math, Rejection Sampling, Randomized, Probability and Statistics
Solution
Problem Understanding
The problem provides access to a single API, rand7(), which returns a uniformly random integer from 1 to 7. The task is to implement another function, rand10(), which must return a uniformly random integer from 1 to 10.
The important constraint is that we are not allowed to use any built-in random number generators. Every piece of randomness must ultimately come from repeated calls to rand7().
The challenge is not simply generating numbers in the range [1, 10]. The difficult part is ensuring that every number from 1 through 10 has exactly the same probability of being chosen. A naive implementation can easily introduce bias.
The hidden parameter n in the test cases only indicates how many times the online judge will call rand10(). It is not passed directly into the function and does not affect the implementation logic.
Because n can be as large as 10^5, the solution must be efficient. A poor implementation that repeatedly retries too often could become unnecessarily slow.
A major observation is that 7 does not divide evenly into 10, which means we cannot simply map one call to rand7() directly into one result from 1 to 10. For example, using something like:
return rand7() % 10 + 1
would never generate values greater than 8. Similarly, combining multiple calls incorrectly can make some outputs more likely than others.
The problem guarantees that rand7() itself is perfectly uniform, meaning each value from 1 through 7 occurs with probability 1/7. Our implementation must preserve uniformity while transforming the output space from size 7 into size 10.
Important edge cases include situations where the generated intermediate random value does not map evenly into [1, 10]. A naive implementation might incorrectly compress ranges and create probability bias. The correct solution must explicitly reject invalid ranges to maintain equal probability.
Approaches
Brute Force Approach
A brute force idea is to repeatedly call rand7() and try to directly map its outputs into the range [1, 10]. One attempt could be to combine two calls:
(rand7() + rand7()) % 10 + 1
This does produce numbers from 1 to 10, but the distribution is not uniform. Some sums occur more frequently than others. For example, the sum 2 can only occur one way, while the sum 8 can occur many different ways.
Another brute force attempt is to keep generating numbers until we somehow land in the desired range. However, without careful probability management, this still introduces uneven probabilities.
The brute force approach fails because it ignores a critical requirement: every output must have identical probability.
Key Insight
The key insight is that two independent calls to rand7() can generate a uniform distribution over 49 equally likely outcomes.
Since:
7 × 7 = 49
we can think of the pair (a, b) as representing numbers from 1 to 49.
Because 49 is larger than 10, we can use rejection sampling. Specifically:
- Use the first
40values because40is divisible by10 - Reject values
41through49 - Map the remaining values evenly into
1through10
This works because each of the 40 accepted states occurs with equal probability.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(1) | O(1) | Produces biased probabilities, therefore incorrect |
| Optimal | O(1) expected | O(1) | Uses rejection sampling to guarantee uniform distribution |
Algorithm Walkthrough
- Call
rand7()twice to generate two independent random values.
Let:
row = rand7()
col = rand7()
Since each call is independent and uniformly distributed, every pair (row, col) is equally likely.
2. Convert the two-dimensional pair into a single number from 1 to 49.
We use:
index = (row - 1) * 7 + col
This effectively lays out all possible pairs in a 7 × 7 grid.
For example:
| row | col | index |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 7 | 7 |
| 2 | 1 | 8 |
| 7 | 7 | 49 |
| 3. Check whether the generated number falls within the usable range. |
We only accept values from 1 through 40.
Why 40?
Because:
40 = 10 × 4
This means every output from 1 to 10 can appear exactly four times.
4. Reject invalid values.
If the generated value is between 41 and 49, discard it and repeat the process.
This is the core idea behind rejection sampling. 5. Convert the accepted value into the final answer.
Once we have a valid number from 1 to 40, map it into [1, 10] using:
result = (index - 1) % 10 + 1
This evenly distributes the outcomes.
Why it works
Each pair of rand7() calls produces one of 49 equally likely outcomes. By rejecting values 41 through 49, we are left with 40 equally likely outcomes. Since 40 divides evenly into 10, each final value from 1 through 10 appears exactly four times. Therefore, the probability distribution is perfectly uniform.
Python Solution
# The rand7() API is already defined for you.
# def rand7():
# # returns a random integer in the range 1 to 7
class Solution:
def rand10(self) -> int:
while True:
row = rand7()
col = rand7()
index = (row - 1) * 7 + col
if index <= 40:
return (index - 1) % 10 + 1
The implementation continuously generates candidate values until a valid one is found.
The variables row and col represent two independent random values from 1 to 7. These are combined into a single uniformly distributed value from 1 to 49.
The expression:
(row - 1) * 7 + col
maps the 7 × 7 grid into a linear sequence.
The condition:
if index <= 40:
implements rejection sampling. Values above 40 are discarded because they would create uneven probabilities.
Finally, modulo arithmetic converts the accepted range into values from 1 to 10.
Go Solution
func rand10() int {
for {
row := rand7()
col := rand7()
index := (row-1)*7 + col
if index <= 40 {
return (index-1)%10 + 1
}
}
}
The Go implementation follows exactly the same logic as the Python version.
Go uses explicit variable declarations with :=. Since integers in this problem remain very small, integer overflow is not a concern.
The infinite for loop is the Go equivalent of Python's while True: loop.
Worked Examples
Example 1
Input: n = 1
Possible Output: [2]
Suppose the following calls occur:
| Step | rand7() #1 | rand7() #2 | index | Accepted? | Final Result |
|---|---|---|---|---|---|
| 1 | 1 | 2 | 2 | Yes | 2 |
Calculation:
index = (1 - 1) * 7 + 2
= 2
result = (2 - 1) % 10 + 1
= 2
Example 2
Input: n = 2
Possible Output: [2, 8]
First call to rand10():
| Step | rand7() #1 | rand7() #2 | index | Accepted? | Result |
|---|---|---|---|---|---|
| 1 | 1 | 2 | 2 | Yes | 2 |
Second call to rand10():
| Step | rand7() #1 | rand7() #2 | index | Accepted? | Result |
|---|---|---|---|---|---|
| 1 | 2 | 1 | 8 | Yes | 8 |
Calculation:
index = (2 - 1) * 7 + 1
= 8
result = (8 - 1) % 10 + 1
= 8
Example 3
Input: n = 3
Possible Output: [3, 8, 10]
Third generated value:
| Step | rand7() #1 | rand7() #2 | index | Accepted? | Result |
|---|---|---|---|---|---|
| 1 | 2 | 3 | 10 | Yes | 10 |
Calculation:
index = (2 - 1) * 7 + 3
= 10
result = (10 - 1) % 10 + 1
= 10
Example Showing Rejection
Suppose:
| Step | rand7() #1 | rand7() #2 | index | Accepted? |
|---|---|---|---|---|
| 1 | 6 | 7 | 42 | No |
| 2 | 2 | 5 | 12 | Yes |
The first generated value is rejected because 42 > 40.
The algorithm retries until a valid value is produced.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) expected | Rejection sampling retries only occasionally |
| Space | O(1) | Uses only a few integer variables |
The algorithm has constant expected time complexity because each attempt succeeds with probability:
40 / 49
The expected number of attempts is therefore:
49 / 40
Since each attempt uses two calls to rand7(), the expected number of rand7() calls is:
2 × (49 / 40) = 2.45
This remains constant regardless of input size.
Test Cases
# Mock rand7 implementation for testing purposes only
from collections import Counter
import random
def rand7():
return random.randint(1, 7)
class Solution:
def rand10(self) -> int:
while True:
row = rand7()
col = rand7()
index = (row - 1) * 7 + col
if index <= 40:
return (index - 1) % 10 + 1
sol = Solution()
# Test 1: output always within valid range
for _ in range(1000):
value = sol.rand10()
assert 1 <= value <= 10 # verifies range correctness
# Test 2: every number appears eventually
results = [sol.rand10() for _ in range(10000)]
counts = Counter(results)
for num in range(1, 11):
assert num in counts # verifies all outputs are reachable
# Test 3: approximate uniformity
# each number should appear reasonably often
for count in counts.values():
assert count > 700 # rough lower bound for randomness sanity
# Test 4: repeated calls do not crash
for _ in range(100000):
sol.rand10() # stress test for stability
# Test 5: verify no invalid values produced
results = [sol.rand10() for _ in range(5000)]
assert all(1 <= x <= 10 for x in results) # boundary validation
| Test | Why |
|---|---|
| Generate 1000 values | Verifies all outputs remain within [1, 10] |
| Check all numbers appear | Ensures every value is reachable |
| Approximate uniformity test | Validates probability distribution is reasonably balanced |
| Stress test with 100000 calls | Confirms stability under heavy usage |
| Boundary validation | Ensures no invalid outputs are produced |
Edge Cases
One important edge case occurs when the generated intermediate value falls between 41 and 49. These values cannot be evenly mapped into 1 through 10. A buggy implementation might still apply modulo arithmetic directly, which would make some outputs more likely than others. The rejection sampling step prevents this by discarding those values entirely.
Another important case is repeated rejection. In theory, the algorithm could repeatedly generate invalid values. A poor implementation might accidentally enter an infinite loop because of incorrect retry logic. This implementation handles retries safely using a simple infinite loop that continues until a valid value is produced.
A third edge case involves incorrect index construction. If the formula converting two rand7() calls into a value from 1 to 49 is wrong, some numbers may become unreachable or duplicated. The expression:
(row - 1) * 7 + col
guarantees that every value from 1 through 49 appears exactly once, preserving uniform probability across the entire space.