01 · Punto de partida
Cuando la discreta se muestrea millones de veces
Buscar en acumuladas cuesta O(n) lineal u O(log n) binaria por muestra. Con n = 10 000 categorías y N = 10⁸, eso es prohibitivo. El alias paga O(n) una vez y luego muestrea en O(1): un entero uniforme + una comparación.
La idea visual: rebanar toda la masa en n columnas de altura exacta 1/n, cada una con a lo sumo dos valores (el propio y su alias). Muestrear es elegir columna y decidir entre sus dos habitantes.
- ¿Cómo se parte una masa arbitraria en n columnas de 1/n?
- ¿Qué guardan las tablas Prob y Alias?
- ¿Por qué cada muestra cuesta solo dos uniformes discretos?
- ¿Cuándo conviene pagar el preproceso?
02 · Definición
Columnas de 1/n con a lo sumo dos valores
Escalar pᵢ por n: qᵢ = n·pᵢ promedia 1. Los qᵢ < 1 (chicos) ceden su faltante a qⱼ > 1 (grandes). Al final cada columna i tiene Prob[i] = fracción propia y Alias[i] = donante del resto.
Prob[i]
Umbral en [0,1] de la columna i. Con prob. Prob[i] sale i; con 1 − Prob[i] sale su alias. Si Prob[i] = 1 no hay alias.
Alias[i]
El donante que completa la columna hasta 1/n. Cada valor aparece como alias en pocas columnas; la masa total se conserva.
O(1) real
Un entero + un uniforme + dos accesos a tabla. Sin bucles, sin búsquedas. Ideal para N ≫ n con p fija.
| i | qᵢ = 6·pᵢ | Prob[i] | Alias[i] |
|---|---|---|---|
| 1 | 0,60 | 0,600 | 3 |
| 2 | 1,20 | 1,000 | — |
| 3 | 1,80 | 1,000 | — |
| 4 | 1,50 | 1,000 | — |
| 5 | 0,60 | 0,600 | 4 |
| 6 | 0,30 | 0,300 | 4 |
03 · Anatomía
Construcción de Walker en O(n)
- Pilas chicas y grandes
- qᵢ < 1 van a “small”, qᵢ ≥ 1 a “large”. Sacar un chico s y un grande g: Prob[s] = qₛ, Alias[s] = g; descontar (1 − qₛ) de q_g y reclasificar g.
- Invariante
- La masa total de pilas + columnas cerradas siempre suma n. Al vaciar pilas, cada columna mide 1/n exacto (salvo redondeo).
- Memoria 2n
- Dos arreglos de n (Prob flotante + Alias entero). Para n = 10⁶ son ~12 MB: aceptable frente al ahorro en N.
- Muestreo sin sesgo
- P(X = k) = Σᵢ (1/n)·P(devolver k|columna i) = pₖ por construcción. El redondeo se absorbe forzando Prob = 1 en restos.
Alias O(1)
N millonario, p fija
Preproceso O(n) amortizado. Sin búsquedas: el costo por muestra no crece con n.
Bisect O(log n)
p cambiante o N chico
Sin preproceso. Si p cambia en cada llamada o N ≈ n, construir alias no se amortiza.
04 · Cuándo pagar el preproceso
Regla N ≫ n
- 1Vocabularios y grillas.
Miles de categorías fijas (lenguaje, grillas espaciales) con miles de millones de sorteos.
- 2Motores de simulación.
Demanda/discretas empíricas fijas que se muestrean en cada réplica y paso.
- 3No usar si…
p cambia por contexto, N ≈ n, o n ≤ 10 (lineal ya es O(1) práctico).
- 4Detalle en Tema 14.
Construcción paso a paso con pilas y verificación de columnas.
05 · Representación en Python
Walker + sampler en 25 líneas
Construcción clásica y muestreo con dos uniformes. La verificación compara frecuencias contra p.
Python en tu navegador. Cambiá a la justa y verificá que Prob queda todo en 1.
import random
from collections import deque
random.seed(100)
probs = [0.10, 0.20, 0.30, 0.25, 0.10, 0.05]
n = len(probs)
def construir_alias(probs):
n = len(probs)
q = [p * n for p in probs]
prob = [0.0] * n
alias = [0] * n
small = deque([i for i, v in enumerate(q) if v < 1 - 1e-12])
large = deque([i for i, v in enumerate(q) if v >= 1 - 1e-12])
while small and large:
s = small.popleft()
g = large.popleft()
prob[s] = q[s]
alias[s] = g
q[g] = q[g] - (1 - q[s])
if q[g] < 1 - 1e-12:
small.append(g)
else:
large.append(g)
for i in list(small) + list(large):
prob[i] = 1.0
alias[i] = i
return prob, alias
def alias_sample(prob, alias):
n = len(prob)
i = int(random.random() * n)
return i if random.random() < prob[i] else alias[i]
prob, alias = construir_alias(probs)
print("Prob:", [round(v, 3) for v in prob])
print("Alias:", alias)
N = 20000
m = [alias_sample(prob, alias) + 1 for _ in range(N)]
for v in range(1, 7):
print(v, round(m.count(v) / N, 3))
Frecuencias ≈ tabla original. Con la justa, Prob = [1,1,1,1,1,1]: cada columna es pura.
06 · Exploración
Laboratorio: alias que calza en O(1)
Elegí distribución y N. El muestreo usa alias real en el navegador (dos uniformes por muestra). Las tablas Prob/Alias se muestran debajo; el histograma debe pegarse a los rombos teóricos.
Alias O(1)
I ~ U{1..6} · U < Prob[I]
Tablas construidas una vez; N muestras en O(1) cada una.
Prob: — · Alias: —
Seis barras vs rombos teóricos. Cambiá a justa para ver Prob todo en 1.
Preguntas para explorar
- Con cargada, ¿qué columnas tienen alias? Leé la línea Prob/Alias.
- Con justa, ¿por qué Prob = 1 en todas? ¿Qué significa “columna pura”?
- Si N = 500, ¿el error máx. es del método o del ruido? ¿Cómo lo distinguís?
Ver respuestas sugeridas
- Las chicas (1, 5, 6) con alias 3, 4, 4: ceden su faltante a las grandes.
- qᵢ = 1 exacto: cada columna ya mide 1/6 sin donantes.
- Ruido: el chi-cuadrado escala con N; el sesgo de método no se achica al subir N.
07 · Comprensión
Confusiones frecuentes
«Alias aproxima»
No: es exacto (salvo flotante). El histograma se desvía solo por ruido Monte Carlo, igual que bisect.
«Uso un solo uniforme para columna y umbral»
Partir los bits correlaciona I y U₂. Dos uniformes independientes: uno elige columna, otro decide.
«Reconstruyo en cada muestra»
Destruye la ganancia: O(n) por muestra. Construir una vez, muestrear N veces.
«Alias sirve con p cambiante»
No sin reconstruir. Con pesos por contexto, bisect o tabla por contexto; alias solo con p fija.
08 · Práctica guiada
Ejercicios con Python
Ejercicio 1: verificar columnas
Con la tabla del dado cargado, comprobá que cada pᵢ se recupera desde (Prob, Alias) con la fórmula de la buena práctica.
probs = [0.10, 0.20, 0.30, 0.25, 0.10, 0.05]
prob = [0.6, 1.0, 1.0, 1.0, 0.6, 0.3]
alias = [2, 1, 2, 3, 3, 3] # 0-based: [2,1,2,3,3,3]-1
n = 6
for k in range(n):
pk = prob[k] / n + sum((1 - prob[j]) / n
for j in range(n) if alias[j] == k and prob[j] < 1)
print(k + 1, round(pk, 3), "vs", probs[k])
Ver solución razonada
Todas deben coincidir a 1e−9. Si alguna difiere, el alias de esa columna está mal asignado.
Ejercicio 2: justa trivial
Construí alias para 6 × 1/6 y verificá Prob todo 1. Muestreá 6000 y compará contra randint.
import random
random.seed(101)
prob = [1.0] * 6
alias = list(range(6))
m = [(int(random.random() * 6) if random.random() < prob[int(random.random()*6)] else 0) for _ in range(6)]
# Muestreo correcto simple:
random.seed(101)
m = []
for _ in range(6000):
i = int(random.random() * 6)
m.append(i if random.random() < prob[i] else alias[i])
print([round(m.count(v) / len(m), 3) for v in range(6)])
Ver solución
Seis ≈ 0,167. Con Prob = 1 el alias nunca se usa: el método colapsa a “columna al azar”.
Ejercicio 3: medir la ganancia
Compará tiempo de 200 000 muestras por bisect vs alias con n = 100 categorías. Alias debe ganar claro.
Ver una posible respuesta
import random
import bisect
import time
from collections import deque
random.seed(102)
n = 100
probs = [random.random() for _ in range(n)]
s = sum(probs)
probs = [p / s for p in probs]
acum = []
a = 0.0
for p in probs:
a += p
acum.append(a)
acum[-1] = 1.0
def construir(probs):
n = len(probs)
q = [p * n for p in probs]
prob = [0.0] * n
alias = [0] * n
small = deque([i for i, v in enumerate(q) if v < 1])
large = deque([i for i, v in enumerate(q) if v >= 1])
while small and large:
st = small.popleft()
lg = large.popleft()
prob[st] = q[st]
alias[st] = lg
q[lg] = q[lg] - (1 - q[st])
(small if q[lg] < 1 else large).append(lg)
for i in list(small) + list(large):
prob[i] = 1.0
return prob, alias
prob, alias = construir(probs)
N = 200000
t0 = time.perf_counter()
for _ in range(N):
_ = bisect.bisect_left(acum, random.random())
t1 = time.perf_counter()
for _ in range(N):
i = int(random.random() * n)
_ = i if random.random() < prob[i] else alias[i]
t2 = time.perf_counter()
print(f"bisect: {t1-t0:.3f}s alias: {t2-t1:.3f}s")
Alias suele ser 2–5× más rápido con n = 100. La brecha crece con n.
09 · Síntesis
Ideas para recordar
- Alias = n columnas de 1/n con primario + alias; muestreo O(1).
- Construcción Walker O(n) con pilas chicas/grandes, una vez.
- Dos uniformes por muestra: columna y umbral, independientes.
- Gana si N ≫ n con p fija; pierde si p cambia o N ≈ n.
- Validar por columnas (fórmula exacta), no solo histograma.
En el próximo tema construiremos esas tablas a mano: construcción de tablas de alias.