Suppose that you want to output 0 with probability \(1/2\) and 1 with probability \(1/2\). At your disposal is a procedure \(\textsc{Biased-Random}\), that outputs either 0 or 1. It outputs 1 with some probability \(p\) and 0 with probability \(1 - p\), where \(0 < p < 1\), but you do not know what \(p\) is. Give an algorithm that uses \(\textsc{Biased-Random}\) as a subroutine, and returns an unbiased answer, returning 0 with probability \(1/2\) and 1 with probability \(1/2\). What is the expected running time of your algorithm as a function of \(p\)?
The key insight is to call \(\textsc{Biased-Random}\) twice and observe the pattern of results.
The Clever Trick
If we call \(\textsc{Biased-Random}\) twice:
\[\begin {align*}
P(00) &= (1-p) \cdot (1-p) = (1-p)^2 \\
P(01) &= (1-p) \cdot p = p(1-p) \\
P(10) &= p \cdot (1-p) = p(1-p) \\
P(11) &= p \cdot p = p^2
\end {align*}\]
Note that probabilities of \(P(01)\) and \(P(10)\) are equal, regardless of the value of \(p\). This gives us our unbiased source:
If we get \((0,1)\), return 0
If we get \((1,0)\), return 1
If we get \((0,0)\) or \((1,1)\), discard and try again
Therefore, the expected running time is \(\Theta(1/[p(1-p)])\).
Understanding the Running Time
The function \(p(1-p)\) is maximized when \(p = 1/2\) (giving \(p(1-p) = 1/4\)), and approaches 0 as \(p\) approaches 0 or 1.
Best case: When \(p = 1/2\), \(E[\text{calls}] = 4\)
Worst case: When \(p\) is very close to 0 or 1, we need many iterations
For example:
If \(p = 0.9\), then \(E[\text{calls}] = 1/(0.9 \cdot 0.1) = 11.11\)
If \(p = 0.99\), then \(E[\text{calls}] = 1/(0.99 \cdot 0.01) = 101.01\)
Python Implementation
import random
def BiasedRandom(p):
"""
Simulates BIASED-RANDOM with probability p of returning 1
Args:
p: probability of returning 1 (between 0 and 1)
Returns:
1 with probability p, 0 with probability (1-p)
"""
return 1 if random.random() < p else 0
def UnbiasedRandom(p):
"""
Generates an unbiased random bit using a biased source
Args:
p: bias of the source (probability of 1)
Returns:
0 or 1, each with probability 1/2
"""
while True:
x = BiasedRandom(p)
y = BiasedRandom(p)
if x == 0 and y == 1:
return 0
if x == 1 and y == 0:
return 1
# If (0,0) or (1,1), discard and try again
# Test with different bias values
print("Testing Unbiased-Random with various bias values:\n")
for p in [0.1, 0.3, 0.5, 0.7, 0.9]:
print(f"Bias p = {p}")
# Count number of calls to BiasedRandom
call_count = 0
original_biased = BiasedRandom
def counted_biased(bias):
global call_count
call_count += 1
return original_biased(bias)
# Generate samples and count calls
BiasedRandom = counted_biased
samples = 1000
zeros = 0
ones = 0
for _ in range(samples):
result = UnbiasedRandom(p)
if result == 0:
zeros += 1
else:
ones += 1
BiasedRandom = original_biased
avg_calls = call_count / samples
theoretical = 1 / (p * (1 - p))
print(f" Results: 0's = {zeros}, 1's = {ones}")
print(f" Average calls per output: {avg_calls:.2f}")
print(f" Theoretical expectation: {theoretical:.2f}")
print()
print("As expected, when p = 0.5, we need the fewest calls (~4).")
print("When p is extreme (close to 0 or 1), we need many more calls.")