Mostrando entradas con la etiqueta tiempo de ejecución. Mostrar todas las entradas
Mostrando entradas con la etiqueta tiempo de ejecución. Mostrar todas las entradas

miércoles, 3 de septiembre de 2025

Probabilidad con baraja española: manotazo. Solución 3/3 (Matemáticas al rescate)

Todo comienza en un viaje familiar que inicia las vacaciones de verano. Un rato muerto. Una baraja española. Un recuerdo de juego en mi infancia. Y tras ir volteando cartas y diciendo los números consecutivos y "perder" casi todas las veces surge la pregunta: ¿qué probabilidad hay de ganar a este juego?


 Y ya tenemos la chispa encendida. Y siguen un papel y un boli con la probabilidad clásica, combinatoria, árboles y simplificaciones del problema para entender la magnitud real del mismo. Y la magnitud es demasiado grande para mis papeles. ¿Y si cojo el ordenador y programo un poco para contestar a la pregunta? Pero ahí también surgen dificultades.

Pero, como sucede con cierta frecuencia, en el proceso de investigar y buscar estrategias aparecen nuevos conocimientos que acaban hilándose hasta llegar al culmen.

Así que decidí dedicar algunos ratos libres para programar e ir contando en este blog todo de manera gradual y por fascículos.

Y antes de introducir el problema "grande" escribí esta entrada con un problema de urnas y bolas; y en esta otra entrada la solución (con la inesperada visita del número e). Intercalé esta entrada sobre el subfactorial y los desarreglos, que "casualidades de la vida" se utilizaban en la solución.

Parecía el fin de la cuestión y como quien cambia de tema aparece esta entrada sobre los polinomios de Laguerre y cómo calcularlos con Python y wxMaxima.

Y ahora sí, llega el problema original con la baraja de cartas. Enunciado en esta entrada, y primera solución es esta otra entrada. Pero nos encontramos con un problema de tiempos razonables de ejecución así que en esta nueva entrada cuento cómo hacer una aproximación empírica a la solución del problema.

Pero queda la traca final que pone fin a esta serie de entradas, como ya se ha puesto fin a la vacaciones de verano.

Resulta que existe una fórmula para calcular una generalización de los desarreglos cuando tenemos elementos que se repiten.

Teniendo r elementos diferentes, el primero que se repite n1 veces, el segundo n2 veces y así sucesivamente, el número de desarreglos viene dado por: 

Donde Pn_i es el polinomio de Laguerre de grado n_i. (Fuente

Por ejemplo, en una situación con 3 elementos que se repiten 1, 5 y 7 veces respectivamente, dentro de la integral definida tendremos (además de la exponencial de -x) los polinomios de Laguerre de grado 1, de grado 5 y de grado 7.

Por tanto, en nuestro problema original (13 números repetidos 4 veces), dentro de la integral tendremos 13 veces el polinomio de Laguerre de grado 4 (y la exponencial). Así que resolviendo esa integral podemos obtener la solución del problema.

En wxMaxima es muy sencillo y rápido (código en github):

Y la implementación en Python puede encontrarse también en mi repositorio de Matemática Recreativa de Github (ver código).

Con un tiempo de ejecución muy pequeño tenemos la respuesta al problema: la probabilidad de ganar en las condiciones planteadas es de aproximadamente 1,62%.

Recordad que con la estrategia de simulación empírica obtuvimos aproximadamente 1,63%. En este caso no necesitamos simulaciones, porque las Matemáticas tienen una manera relativamente sencilla de calcularlo, pero cabe destacar que la aproximación realizada es bastante buena.

¿Quién me iba a decir en casa de mi familiar cuando estaba jugando con la baraja española que acabaría utilizando integrales definidas con polinomios de Laguerre para contestar a mi pregunta sobre la probabilidad de ganar en el juego?

Pero una cosa sí que tengo constatada, cuando se dispone de tiempo libre siempre surgen buenas ideas. La rutina diaria y el ritmo de vida acelerado nos bloquean la creatividad. 

 

domingo, 24 de agosto de 2025

Probabilidad con baraja española: manotazo. Solución 2/3 (simulaciones)

 

En una entrada anterior planteé una cuestión de probabilidad en un juego de cartas con la baraja española.

Se resolvió el problema en esta entrada utilizando una estrategia exhaustiva: crear todas las permutaciones posibles y comprobar en cada una de ellas si se gana o se pierde el juego.

El problema de dicha resolución es el tiempo de ejecución de crear y recorrer  92.024.242.230.271.040.357.108.320.801.872.044.844.750.000.000.000 barajas (ordenamientos de cartas) diferentes.

Así que en esta ocasión vamos a afrontar el problema con otra estrategia: simular un número grande de partidas para calcular la frecuencia relativa de victorias. Esta estrategia en algunos ámbitos se conoce como el Método de Montecarlo, pero en nuestro caso podemos simplificar mucho su fundamento como una aplicación de la Ley de los Grandes Números en un experimento de Bernoulli.

 

Imagen creada con IA

 Resumen resumido: si repetimos muuuuchas veces un experimento, la frecuencia relativa de éxito tiende a la probabilidad teórica del suceso.

Así que he creado un programa en Python (ver código) que simula jugar tantas partidas como le indiquemos.

En la estrategia exhaustiva se calculó un ejemplo:

... con un mazo de cartas de 4 números y 4 palos (16 cartas), el número de permutaciones es 63.063.000 (fórmula de las permutaciones con elementos repetidos) y la probabilidad de ganar el juego es aproximadamente 0.011869416297987727 (~1,19%). 

Así que ejecutamos el nuevo código con 4 números y 4 palos e indicamos que queremos que simule 5.000.000 de partidas. Tan sólo 12 segundos más tarde en mi ordenador tengo los resultados:

Se ha ganado el juego 59311 veces de un total de 5000000 partidas.
Porcentaje de éxito: 1.18622 % 

Observamos que es una magnífica aproximación a la probabilidad real calculada con la estrategia exhaustiva.

Vamos entonces a simular el problema original de 13 cartas y 4 palos. Le indicamos que simule 5.000.000 de partidas y los resultados obtenidos en 37 segundos son:

Se ha ganado el juego 81267 veces de un total de 5000000 partidas.
Porcentaje de éxito: 1.62534 %
Pues ya tenemos una aproximación a la solución del problema original. La probabilidad de ganar el juego en las condiciones planteadas es aproximadamente 1,63%.

Subimos el número de partidas simuladas a 10.000.000:

Se ha ganado el juego 162883 veces de un total de 10000000 partidas.
Porcentaje de éxito: 1.62883 % 
Para la precisión que buscamos no varía significativamente. Por lo que nos damos ya por satisfechos con la aproximación de la solución obtenida.

Esta estrategia es estupenda cuando es muy complejo o costoso calcular las soluciones reales, o cuando es suficiente encontrar una aproximación empírica a la solución.

Pero en el problema que nos ocupa, ¿y si subimos la apuesta y encontramos otra manera de solucionar el problema?

Esa será la próxima entrada del blog 😉

martes, 19 de agosto de 2025

Probabilidad con baraja española: manotazo. Solución 1/3 (exhaustiva)

En la entrada anterior planteé una cuestión de probabilidad en un juego de cartas con la baraja española.


¿Cuál es la probabilidad de ganar el juego sin que haya coincidido ninguna vez la carta destapada con el número cantado? 

O su complementario, ¿cuál es la probabilidad de "perder"? Entendemos perder en este contexto como que coincida en algún momento la carta destapada con el número cantado.

Una posible idea es utilizar la estrategia exhaustiva: construir TODAS las posibles barajas (ordenamientos de cartas) y comprobar en cuántas de ellas se gana/pierde el juego. A mano está claro que no lo vamos a hacer, pero ¿podemos hacer un programa que realice esa tarea por nosotros?

He realizado un programa en Python que implementa esta estrategia exhaustiva (ver código).

Por ejemplo, con un mazo de cartas de 4 números y 4 palos (16 cartas), el número de permutaciones es 63.063.000 (fórmula de las permutaciones con elementos repetidos) y la probabilidad de ganar el juego es aproximadamente 0.011869416297987727 (~1,19%). Para crear y recorrer todas las barajas posibles, mi ordenador ha tardado algo menos de 4 minutos y medio.

El problema viene cuando ponemos las condiciones del problema: 13 números y 4 palos. El número de permutaciones (barajas distintas) es:

 92.024.242.230.271.040.357.108.320.801.872.044.844.750.000.000.000

Y, claro, va a llevar muuuuuucho más tiempo llegar a la solución de esa manera.

Así que, una vez más, aunque tengamos un algoritmo que resuelve el problema, la realidad con un ordenador medio es que no es factible llegar a la solución en un tiempo razonable.

¿Habrá alguna estrategia alternativa? A seguir pensando...

 

PD: La librería itertools de Python tiene una función para calcular todas las variaciones de un conjunto, pero no tiene ninguna para hacer lo propio con multiconjuntos (elementos que se repiten). Por ello se ha implementado la función permutaciones_repeticion dentro del programa para evitar repetir ordenamientos ya contados.


 

lunes, 14 de abril de 2025

Python: listas VS conjuntos

Imagen creada con IA

En Python, una de las diferencias fundamentales entre los tipos de datos lista (list) y conjunto (set) es que el segundo no puede tener duplicados. De hecho cuando queremos eliminar los duplicados de una lista, una de las maneras de hacerlo es transformar la lista en conjunto y luego volver a pasar el conjunto a lista.

No entro aquí en el tipo de datos que puede contener cada uno de ellos. Si mi variable debe admitir datos duplicados necesariamente debo utilizar una lista. Pero si hay la certeza de que no habrá ningún dato duplicado (o no queremos almacenarlo más de una vez) podemos elegir entre usar una lista o un conjunto. Lo cierto es que la versatilidad de las listas hace que muchas veces nos decantemos por estas, pero no siempre es la mejor opción como mostraré a continuación.

Cuando la cantidad de datos con los que voy a trabajar es considerable, los tiempos de ejecución de los programas puede variar muy significativamente si utilizo listas o si utilizo conjuntos (cuando pueda hacerlo).

¿Hay diferencia en tiempo de ejecución al recorrer una lista y al recorrer un conjunto, ambos con los mismos elementos?

Imagen creada con IA

He hecho una prueba en el ordenador en el que me encuentro escribiendo esta entrada. He creado una lista y un conjunto con los números del 0 al 100000000, este último sin incluir. Recorriendo ambos con un bucle "for ... in" he obtenido los siguientes tiempo de ejecución:

    Tiempo de recorrido en lista: 5.245566 segundos

    Tiempo de recorrido en conjunto: 5.883574 segundos

El tiempo de recorrido sobre el conjunto es aproximadamente un 12% superior al tiempo de recorrido sobre la lista.

Además, en el mismo programa he buscado el elemento 99999999 en la lista (if ... in) y los tiempos de ejecución han sido los siguientes:

    Tiempo de búsqueda en lista: 1.093401 segundos

    Tiempo de búsqueda en conjunto: 0.000001 segundos

El tiempo de búsqueda en el conjunto es muchísimo más corto que el tiempo de búsqueda en la lista y aunque no busquemos el último elemento los tiempos siguen siendo significativamente muy inferiores. Esto en programas con gran cantidad de datos o que debemos realizar muchas búsquedas en una colección de datos puede suponer la diferencia entre un programa con un tiempo de ejecución aceptable o uno de esos que dejas el ordenador encendido y al cabo de muchas horas vuelves para mirar si con suerte ya ha acabado (a mi me pasó con el problema de los cartones del bingo).

A continuación incluyo el código del programa utilizado:

import time

n = 100000000
elementos = list(range(n))
elementoZ = n-1
mi_lista = elementos
mi_conjunto = set(elementos)

# Medir tiempo de recorrido en lista
inicio = time.time()
for elemento in mi_lista:
next
tiempo_lista = time.time() - inicio

# Medir tiempo de recorrido en conjunto
inicio = time.time()
for elemento in mi_conjunto:
next
tiempo_conjunto = time.time() - inicio

print(f"Tiempo de recorrido en lista: {tiempo_lista:.6f} segundos")
print(f"Tiempo de recorrido en conjunto: {tiempo_conjunto:.6f} segundos")

# Medir tiempo de búsqueda en lista
inicio = time.time()
if elementoZ in mi_lista:
tiempo_lista = time.time() - inicio

# Medir tiempo de búsqueda en conjunto
inicio = time.time()
if elementoZ in mi_conjunto:
tiempo_conjunto = time.time() - inicio

print(f"Tiempo de búsqueda en lista: {tiempo_lista:.6f} segundos")
print(f"Tiempo de búsqueda en conjunto: {tiempo_conjunto:.6f} segundos")


Referencia externa sobre el tema: en la documentación de Python, apartado TimeComplexity, se muestra que de promedio una búsqueda "in" en listas tiene orden O(n) y en conjuntos O(1).

miércoles, 8 de enero de 2025

Python: dos maneras de calcular la cantidad de cifras de un número natural

Hace muchos años escribí una entrada titulada ¿Cuántas cifras tiene un número? Por ejemplo, el factorial de 1.000.000

Por aquel entonces seguía programando principalmente en lenguaje C++ y profundizaba en software matemático, como wxMaxima, Mathematica, Sage, etc.

Empezaba a hacer alguna cosita en Python y ahora el lenguaje que más utilizo, por diferentes motivos que ahora no vienen al caso.

 https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Python.svg/240px-Python.svg.png

El tema es que resolviendo el problema 11 del Advent of Code 2024 hay que calcular la cantidad de cifras de números. Lo primero que pensé es utilizar la función len() sobre la conversión del número a cadena de caracteres:

len(str(natural))

Pero es cierto que, como comentaba en la entrada antigua, podemos calcular la cantidad de cifras de un número utilizando el logaritmo en base 10 (quedándonos la parte entera):

from math import log10

int(log10(natural))

Si sólo tenemos que calcular la cantidad de cifras de unos pocos números es prácticamente indiferente utilizar una manera u otra. Tal vez yo opte por la primera por no tener que importar funciones de librerías.

Sin embargo, si hay que realizar ese cálculo muchas muchas veces, ¿cuál será más rápido? Para contestar dicha pregunta he hecho dos programas sencillos para calcular el tiempo de ejecución.


    - Versión len

import time
inicio = time.time()
for i in range(1,100000000):
    len(str(i))
fin = time.time()
print(fin-inicio)

    - Version log10

from math import log10
import time
inicio = time.time()
for i in range(1,100000000):
    int(log10(i))
fin = time.time()
print(fin-inicio)


En mi ordenador el tiempo de ejecución del primer programa ronda los 11 segundos, mientras que el del segundo no llega a los 9 segundos.

Por tanto, en las condiciones planteadas, el logaritmo es la opción más rápida. Por otra parte, ¿qué pasa si el número puede ser 0? Dicho método no sirve. Si añadimos un condicional dentro del bucle, ¿seguirá siendo el método más rápido?

Lo dejo como ejercicio para quien quiera averiguarlo y compartir su respuesta :-)