Números Pseudoaleatorios · Tema 24

Permutaciones aleatorias

Mezclar con Fisher-Yates: n! órdenes con igual chance, en O(n), sin el sesgo del “swap con cualquiera”.

01 · Punto de partida

Sortear uno es fácil; mezclar todos, no

El Tema 23 sortea un ganador con choice. Pero repartir un mazo, ordenar un examen o rotar una playlist exigen todos los elementos en orden aleatorio: una permutación donde cada uno de los n! órdenes tenga prob. 1/n!. El atajo “recorro y cambio cada carta con otra al azar” parece funcionar y vicia el mazo: algunas órdenes salen el doble que otras.

La solución es de 1938 (Fisher-Yates, 1964 Knuth): ir fijando desde el final con un sorteo que se achica j ∈ 0…i. Tres líneas, O(n), parejo demostrable.

  • ¿Por qué j va de 0…i y no de 0…n−1?
  • ¿Cuántos órdenes parejos hay con n = 4?
  • ¿Qué tiene de malo ordenar por clave aleatoria?
  • ¿Por qué shuffle no devuelve nada?

02 · Definición

n! órdenes, una sola receta pareja

◈

Uniforme 1/n!

Con n = 3 hay 6 órdenes; con n = 52 hay 8e67. Parejo = cada uno con igual chance, verificable contando (sección 05).

⇄

Rango que se achica

En el paso i hay i+1 candidatos para el puesto i: j = floor(u·(i+1)). Achicar es lo que reparte 1/n! exacto.

▣

In-place O(n)

Sin lista auxiliar: n−1 sorteos y n−1 swaps. Mezclar para sacar uno solo es despilfarro (Tema 23).

Fisher-Yates frente a atajos.
MétodoCaminosVeredicto
Fisher-Yates (j ≤ i)n! exactosParejo 1/n!
Naive (j < n siempre)nⁿ (no múltiplo de n!)Sesgado: unas salen de más
Ordenar por clave uParejo si claves continuasO(n log n) + empates si clave discreta

03 · Técnica correcta

El rango manda

Con n = 3 el naive hace 3³ = 27 caminos sobre 6 órdenes: 27 no es múltiplo de 6, así que es imposible repartir parejo (unas salen 5/27, otras 4/27). Fisher-Yates hace 3·2·1 = 6 caminos: biyección perfecta con los 6 órdenes.

j ≤ i siempre
El error clásico es randint(0, n−1) en cada paso. Lo correcto: randint(0, i) con i decreciente. Un carácter de diferencia, sesgo total.
Entero sin % viciado
El j debe ser parejo en 0…i: con fuente entera usar rechazo si hay resto (Tema 22) o floor sobre u (Tema 19).
shuffle muta
random.shuffle(mazo) mezcla in-place y devuelve None: m = shuffle(m) deja m = None. Para copiar: sample(mazo, len(mazo)).

Bien mezclado

Fisher-Yates / shuffle

O(n), 1/n! demostrable, con semilla para auditar. El estándar de casinos simulados y exámenes.

Mal mezclado

Naive o sort por rand()

Sesgo 5/27 vs 4/27 con n = 3 (medible), o costo O(n log n) con riesgo de empates. Para un mazo real, trampa.

04 · Ejemplos

Tres mezclas típicas

Lista ordenada→FY de atrás adelante→Permutación 1/n!
  1. 1
    Mazo de 52.

    shuffle(mazo) con semilla registrada: 51 sorteos, 8e67 órdenes parejos.

  2. 2
    Examen de 10 preguntas.

    Cada alumno recibe una permutación distinta: sin ventaja de posición.

  3. 3
    Playlist sin “siempre primero lo mismo”.

    Mezcla completa O(n) una vez, no choice repetido (que repite temas, Tema 23).

05 · Implementación en Python

Fisher-Yates a mano y conteo 1/n!

Implementamos FY con randrange(i+1) y verificamos con n = 3: las 6 permutaciones rondan 1/6 en 6000 mezclas. El naive no lo logra.

Python en tu navegador. Cambiá a randrange(n) (naive) y mirá cómo dos órdenes se disparan.

import random
from collections import Counter

def fisher_yates(xs, rng):
    a = list(xs)
    for i in range(len(a) - 1, 0, -1):
        j = rng.randrange(i + 1)
        a[i], a[j] = a[j], a[i]
    return a


rng = random.Random(24)
c = Counter(tuple(fisher_yates("ABC", rng)) for _ in range(6000))
for perm, n in sorted(c.items()):
    print("".join(perm), round(n / 6000, 4))

Seis valores ≈ 0,1667: biyección 6 caminos → 6 órdenes.

El naive se delata

import random
from collections import Counter

def naive(xs, rng):
    a = list(xs)
    n = len(a)
    for i in range(n):
        j = rng.randrange(n)
        a[i], a[j] = a[j], a[i]
    return a


rng = random.Random(24)
c = Counter(tuple(naive("ABC", rng)) for _ in range(6000))
for perm, n in sorted(c.items()):
    print("".join(perm), round(n / 6000, 4))

06 · Exploración

Laboratorio: 6 órdenes bajo lupa

Con n = 3 hay 6 permutaciones: las barras deben rondar 1/6. Compará Fisher-Yates con el naive: el naive deja dos barras altas sistemáticas (5/27 frente a 4/27). Subí los ensayos: el sesgo no se diluye.

EXPERIMENTO 24

1/6 o nada

FY vs naive · n = 3

Los resultados numéricos aparecen debajo.
Esperada 1/6—
χ² (crit 11,07)—
Máx desvío—
Veredicto—

FY con 6000 ensayos: seis barras en 1/6.

La línea es 1/6 ≈ 0,1667. Barras naranjas sistemáticamente altas = órdenes favorecidos por el algoritmo.

Preguntas para explorar

  1. Con naive y 12000 ensayos: ¿el χ² se achica o crece? ¿Qué dice de sesgo vs ruido?
  2. ¿Qué dos órdenes favorece el naive? ¿Coinciden con la teoría 5/27?
  3. Si mezclaras un mazo de 52 con naive, ¿detectarías el sesgo contando? ¿Por qué no?
Ver respuestas sugeridas
  1. Crece: el sesgo es estructural (≈ 0,0185 de ventaja) y el χ² escala con N. El ruido se diluye; el sesgo, no.
  2. Dos órdenes en ≈ 0,1852 (5/27) y cuatro en ≈ 0,1481 (4/27): la firma 27 ∤ 6.
  3. No contando (8e67 órdenes): por teoría (nⁿ caminos) y por usar FY/shuffle auditados, no por censo.

07 · Comprensión

Confusiones frecuentes

«Ordeno por random() y listo»

Con claves continuas es parejo pero O(n log n) y frágil: con claves discretas (randint) los empates rompen el 1/n!. FY es O(n) y exacto.

«m = shuffle(lista) me da la mezclada»

shuffle devuelve None y muta: m queda en None y la original mezclada. Para nueva lista: sample.

«j en 0…n−1 también mezcla»

Mezcla pero vicia: 27 caminos sobre 6 órdenes con n = 3. El rango que se achica no es decoración, es la prueba.

«Para sorteo uso shuffle y tomo el primero»

O(n) con mutación para un O(1) sin mutación (choice, Tema 23). Funciona, pero es 1000× más trabajo con efectos laterales.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: mazo de 52

Mezclá un mazo con shuffle y verificá que están las 52 cartas sin repetidos ni faltantes.

import random

mazo = [f"{v}{p}" for v in "A23456789JQK" for p in "♠♥♦♣"]
rng = random.Random(10)
rng.shuffle(mazo)
print(mazo[:5], len(mazo), len(set(mazo)))
Ver solución razonada

5 cartas visibles, 52 totales y 52 distintas: permutación completa. Con choices habría repetidos (Tema 25).

Ejercicio 2: medir el naive

Repetí el conteo del naive con 30000 ensayos y comprobá el 5/27 ≈ 0,1852 en los favorecidos.

import random
from collections import Counter

def naive(xs, rng):
    a = list(xs)
    for i in range(len(a)):
        j = rng.randrange(len(a))
        a[i], a[j] = a[j], a[i]
    return a


rng = random.Random(24)
c = Counter(tuple(naive("ABC", rng)) for _ in range(30000))
print(sorted((round(n / 30000, 4) for n in c.values())))
Ver solución

Cuatro valores ≈ 0,148 y dos ≈ 0,185: la firma 4/27 y 5/27. Con 30000 el ruido (±0,004) ya no tapa el sesgo (±0,019).

Ejercicio 3: copia sin mutar

Devolvé una mezcla nueva sin tocar la original de dos formas: FY manual y sample.

Ver una posible respuesta
import random

orig = list("ABCDE")
rng = random.Random(2)
fy = list(orig)
for i in range(len(fy) - 1, 0, -1):
    j = rng.randrange(i + 1)
    fy[i], fy[j] = fy[j], fy[i]
otra = rng.sample(orig, len(orig))
print(orig, fy, otra)

Original intacta y dos mezclas parejas distintas. shuffle directo habría mutado orig.

09 · Síntesis

Ideas para recordar

  • Fisher-Yates: j ∈ 0…i de atrás adelante, 1/n!, O(n), in-place.
  • Naive con j < n: nⁿ caminos, sesgo 5/27 vs 4/27 con n = 3.
  • shuffle muta y devuelve None; sample copia.
  • El j debe ser parejo: rechazo/floor, nunca % con resto (Tema 22).
  • Sattolo (j < i) es otro algoritmo: un ciclo, no mezcla general.

En el próximo tema tomaremos porciones: muestreo aleatorio con y sin reemplazo.