Logo Studenta

¿De cuántas formas pueden sumarse 20 dados (1 cara de cada dado en cada suma) para obtener el número 70?

💡 1 Respuesta

User badge image

Apuntes Prácticos

Aclaración previa: Esta respuesta toma una orientación algorítmica a la resolución del problema… para una aproximación estadística o una resolución basada en combinatoria, aconsejo la fantástica respuesta de

Ahora sí :-)

¿Orden de complejidad?

El algoritmo más simple que podemos usar evaluaría todas las configuraciones de 20 dados (Variaciones con Repetición de 6 elementos tomados de 20 en 20)

Se trata, por lo tanto, de un un orden de complejidad O(n6n)O(n·6n)pues:

  • Tenemos 6n6n configuraciones de dados distintas.
  • Tenemos que sumar los n dados de cada configuración (n1n−1 sumas).

Esto es demasiado para mi CPU, ya que para 20 dados debería…

  • …evaluar 3.656.158.440.062.976 configuraciones de dados
  • …sumar 20 dados en cada una de ellas: 69.467.010.361.196.544 sumas

Pero no debemos desanimarnos

Para empezar, una definición:

Definimos count( S, D) como "cuántas configuraciones de D dados suman S"

Si el número de dados D es 1, encontraríamos que

count(s,1)=010s<11s66<scount(s,1)={0s<111≤s≤606

Es decir:

  • Para toda suma que esté entre 1 y 6, existe una única configuracion posible: elegir el dado cuyo valor es la suma.
  • Ninguna otra suma (mayor de 6 o menor de 1) tendría solución

Si usamos D = 2 (2 dados) encontramos que

count(s,2)=06ncount(sn,1)0s<22s1212<scount(s,2)={0s<2∑n6count(s−n,1)2≤s≤12012

Es decir:

  • Para toda suma que sea menor a 2 o mayor a 12 no habrá solución posible.
  • Para toda suma que esté entre 2 y 12, podemos expresar la solución en función de 6 elecciones de dado y, para cada una de ellas, contar cuántas soluciones hay para la cantidad restante (suma - cara elegida) usando un único dado.

La forma general para D > 1 sería esta:

count(s,d>1)=06ncount(sn,d1)0s<dds6d6d<scount(s,d>1)={0s

Esta expresión recursiva podría representarse en javascript fusionando los casos d=1 y d>1 como:

  1. function count(s, d) { 
  2. if (s < d || d * 6 < s) 
  3. return 0 
  4. else if (d === 1) 
  5. return 1 
  6. else 
  7. return [1, 2, 3, 4, 5, 6].reduce( 
  8. (total,n)=> 
  9. total + count(s - n, d - 1), 
  10. 0 
  11. ) 
  12. } 

(Ver codigo completo)

Podemos usarla para calcular count(35,10) (de cuántas formas podemos sumar 35 usando 10 dados) y obtendremos:

4395456

Pero, CUIDADO, no lo intentes con count(70, 20).

Este algoritmo recursivo sigue siendo O(n6n)O(n∗6n) y el incremento para cada dado adicional crece significativamente

  1. { sum: 7, dice: 2, result: 6 } 
  2. time: 4.617ms 
  3.  
  4. { sum: 14, dice: 4, result: 146 } 
  5. time: 0.174ms 
  6.  
  7. { sum: 21, dice: 6, result: 4332 } 
  8. time: 3.574ms 
  9.  
  10. { sum: 28, dice: 8, result: 135954 } 
  11. time: 7.158ms 
  12.  
  13. { sum: 35, dice: 10, result: 4395456 } 
  14. time: 131.758ms 
  15.  
  16. { sum: 42, dice: 12, result: 144840476 } 
  17. time: 4587.074ms 
  18.  
  19. { sum: 49, dice: 14, result: 4836766584 } 
  20. time: 156494.069ms 

Número de nodos vs Número de valores

La definición de count(70, 20) es recursiva, y si la desplegase obtendríamos un árbol 6-ario de profundidad 20, es decir 6^20 nodos cada uno de los cuales utiliza a todos sus nodos hijos para calcular su propio valor.

Sin embargo, como máximo encontraremos 70 * 20 = 1400 valores de count(s,d) ya que los parámetros s y d están acotados:

  • Solo tenemos 70 posibles sumas: 1, 2, …, 70
  • Para cada suma, count(s,d) solo puede ser expresada de 20 maneras: count(s,1), count(s,2), …, count(s,20)

Como conclusión

El algoritmo recursivo es extremadamente ineficiente porque calcula una y otra vez los mismos valores sin "recordar" que ya lo hizo anteriormente.

Ejemplo: count(7,2) será llamado múltiples veces:

  • count(8,3) = count(7,2) + count(6,2) + … + count(2,2)
  • count(9,3) = count(8,2) + count(7,2) + … + count(3,2)
  • count(10,3) = count(9,2) + count(8,2) + count(7,2) + …

"Memoization" al rescate

Cuando los nodos teóricos de un algoritmo recursivo producen muy pocos valores (comparados con el número de nodos) la técnica de "memoization" es ideal.

Esta técnica "recuerda" el valor que se obtuvo al llamar a una función con unos parámetros (Ej: recordaría que el resultado de count( 21, 6 ) es 4332 ), así que solo la calcularía 1 vez.

El algoritmo anterior podría ser reescrito de esta manera:

  1. function count(s, d) { 
  2. const countWithMemoization = memoizate((s, d) => { 
  3. if (s < d || d * 6 < s) 
  4. return 0 
  5. else if (d === 1) 
  6. return 1 
  7. else 
  8. return [1, 2, 3, 4, 5, 6]. 
  9. reduce((total,n) =>  
  10. total+countWithMemoization(s - n, d - 1), 
  11. 0). 
  12.  
  13. }); 
  14. return countWithMemoization(s, d) 
  15. } 

Nota: Puedes ver el código completo aquí

Un análisis detallado para count(35,20) arroja estos resultados

  1. { 
  2. sum: 70, 
  3. dice: 20, 
  4. result: 189456975899496, 
  5. totalCalls: 3085, 
  6. totalMemoized: 610 
  7. } 
  8. time: 1.164ms 
  • Se han memorizado 610 estados diferentes
  • La función "countWithMemoization" se ha evaluado 3085 veces que resulta una mejora "muy significativa" respecto a las 3.656.158.440.062.976 veces de la versión sin memorización.

Lo mejor de todo: hemos obtenido la respuesta a la pregunta:

Existen 189456975899496 configuraciones de 20 dados que suman 70

Un saludo

Antonio

… Espera, ¿sabes? creo que aún podemos seguir un poco más… si te apetece, claro :-p .

¿Hay otra forma de solucionar el problema que no sea recursiva?

Sí, mediante una versión "iterativa"

  • El algoritmo recursivo plantea la resolución como un gran árbol que se resuelve en post-orden (primero se resuelven las hojas, luego los padres de las hojas …).
  • La estructura real es la de un grafo donde los nodos que tienen el mismo valor aparecen una única vez.
    • El grafo tiene, de hecho, 1400 nodos y podría representarse en una matriz de 20 columnas (el número de dados) y 70 filas (las sumas del 70 al 1).
    • El grafo conectaría cada nodo con los hasta 6 nodos que se usan para calcularlo a él.

La "memoización" consigue "engañar" a la percepción del desarrollador:

  • Nos hace creer que estamos recorriendo el árbol.
  • En realidad está recorriendo un grafo!!!.

El punto aquí, es que la realidad "subyacente" puede ser explotada de una forma diferente "rompiendo" las limitaciones que impone la recursividad (tamaño de pila, por ejemplo).

Solución iterativa

En otros problemas recursivos "similares" (ej: cálculo de la función de fibonacci) el paso de recursivo a iterativo se realiza pasando por una versión intermedia que cumple las restricciones de "tail recursion"

En nuestro caso, la dependencia entre estados es más compleja: cada nuevo estado count(s,d) depende de hasta 6 subestados (y 2 variables!!!9, así que escribir la versión usando "tail recursion" no es tarea fácil.

Un posible algoritmo iterativo: generar todos los "count" desde count(1,1) hasta count(S,D) teniendo en cuenta que cada estado count(s,d) depende de 6 estados que están a una distancia máxima (respecto a s) de 6: count(s-6, d-1)

Mi propuesta:

  1. function count(totalSums, totalDice) { 
  2. let counts = Counts(totalDice); 
  3. for (let sum = 1; sum <= totalSums; sum++) 
  4. for (let dice = 1; dice <= totalDice; dice++) 
  5. if (sum < dice || dice * 6 < sum) 
  6. counts.set(sum, dice, 0); 
  7. else if (dice === 1) 
  8. counts.set(sum, dice, 1); 
  9. else 
  10. counts.set(sum, dice, 
  11. [1, 2, 3, 4, 5, 6]. 
  12. reduce( (t, side) =>  
  13. (sum - side > 0) ?  
  14. t+counts.get(sum - side, dice - 1) :  
  15. t, 
  16. 0 
  17. ) 
  18. ) 
  19.  
  20. return counts.get(totalSums, totalDice); 
  21. } 

Nota: Puedes ver el código completo aquí

El objeto "counts" de esta solución nos permite almacenar los estados previamente calculados pero de una forma eficiente: solo nos interesa mantener una "ventana" con los counts resueltos de las 6 sumas anteriores (s-1, s-2, …, s-6). En la solución propuesta se usa una array "circular".

Incluyo el código completo de los ejemplos aquí

Ahora sí: Un saludo

Antonio

0
Dislike0

✏️ Responder

FlechasNegritoItálicoSubrayadaTachadoCitaCódigoLista numeradaLista con viñetasSuscritoSobreDisminuir la sangríaAumentar la sangríaColor de fuenteColor de fondoAlineaciónLimpiarInsertar el linkImagenFórmula

Para escribir su respuesta aquí, Ingresar o Crear una cuenta

User badge image

Otros materiales