Hola
Recientemente resolví problemas del archivo
Timus Online Judge y encontré una sección llamada
tareas de programación dinámica . Este tipo de tarea es de particular interés para mí, porque a menudo este enfoque garantiza la velocidad y la elegancia de la solución. ¿Qué es la programación dinámica?
La programación dinámica es un enfoque para resolver problemas en el que hay una división en subtareas que son "más simples" en comparación con la original. La palabra "dinámico" tiene un significado cercano a "inductivo": se supone que la respuesta se conoce por algún significado
, y quiero encontrar la respuesta para
. En matemáticas, esto se llama transición inductiva y es la idea principal de la programación dinámica.
Ejemplos simples
La tarea más llamativa e indicativa es la tarea de computación
número de la secuencia de Fibonacci.
Se sabe que la secuencia tiene las siguientes propiedades:
Esto implica inmediatamente la fórmula de recurrencia:
int Fibonacci(int n) { if(n == 1 || n == 2) return 1; return Fibonacci(n-1) + Fibonacci(n-2); }
Si la recursión está buscando un número "desde el final", entonces el siguiente método calcula secuencialmente todos los números ubicados entre
y
:
int dpFibonacci(int n) { int prev1 = 1; int prev2 = 1; int curr = 0; for(int j = 2; j < n; j++) { curr = prev1 + prev2; prev1 = prev2; prev2 = curr; } return curr; }
Está claro que para lo suficientemente grande
Este algoritmo funciona mucho más rápido: no calcula valores intermedios varias veces. Considere un ejemplo un poco más complejo.
Ejemplo 1. Estás caminando por una escalera de peaje. Pisar
paso tienes que pagar
monedas Puede pasar al siguiente paso o saltar sobre uno. Tarea: pasar
pasos y gastar la menor cantidad de monedas posible.
Está claro que al pasar por cada paso, minimizamos el número de "pagos", pero podemos encontrar un paso muy costoso, que nos gustaría evitar. Crea una matriz de valores
en el cual en
-el puesto será el número (mínimo) de monedas que se deben gastar para llegar a
paso Está claro de inmediato que
. Y luego tomaremos un mínimo de los dos pasos anteriores y agregaremos el costo del paso en sí:
Cambiamos un poco las condiciones del problema: supongamos que en algunos pasos puede obtener monedas (esto significará que
) ¿Qué hay que cambiar en el algoritmo para que dé el resultado correcto?
SoluciónSolo es necesario cambiar el "comienzo" de nuestra dinámica. Si la primera escalera no nos trae monedas, entonces es aconsejable saltar sobre ella, sin embargo, si , es mejor pisar y recoger tus monedas. Entonces .
Considere otro ejemplo que utiliza dinámicas "bidimensionales".
Ejemplo 2. En el laberinto hay
habitaciones, cada una de las cuales contiene oro (en una jaula
mentiras
oro). La tarea es determinar qué cantidad máxima de oro se puede recolectar con una ruta óptima desde un punto
hasta el punto
si puedes ir hacia abajo o hacia la derecha.
Entonces, queremos saber la mejor ruta a la celda
. Podemos llegar aquí desde dos celdas:
y
. Dado que se conocen las rutas óptimas para estas dos celdas (se almacenan en alguna tabla
), entonces la respuesta para la celda
obtenido de la siguiente manera:
Esta es otra tarea de programación dinámica clásica, cuyas modificaciones son bastante comunes en las tareas de programación deportiva. Una tarea similar se explica con más detalle
aquí .
Tareas más desafiantes
Si lo desea, se puede atornillar un enfoque dinámico donde lo desee. Considere una
tarea del archivo Timus Online Judge.
La formulación matemática del problema es la siguiente: se requiere encontrar el número mínimo de términos necesarios para descomponer un número dado en cuadrados completos.
Como antes, supongamos que sabemos las respuestas para todos los números.
que se almacenan en alguna matriz
y nos gustaría encontrar
.
Toma este número
y analizar qué situaciones pueden ser:
- Es una plaza llena. En este caso .
- Quizás el número anterior Era un cuadrado completo. Entonces .
En general, la opción de agregar una unidad a la anterior no parece tan mala.
Procedemos de la siguiente manera: buscamos una descomposición
tal que
Desde
- plaza completa entonces
y
es decir, encontramos una partición que es simplemente mejor que
, y la respuesta en este caso será
Ejemplo de código Java que implementa este algoritmo: for(int k = 1; k <= n; k++) { int best = d[k - 1] + 1;
Considere el siguiente
problema . El objetivo es construir una escalera desde
cubos según las reglas:
- la escalera tiene al menos dos escalones;
- una escalera no puede tener dos escalones idénticos;
- los escalones de las escaleras van en orden ascendente (es decir, el siguiente es más grande que el anterior).
Esta vez construiremos dinámicas bidimensionales. Crear una tabla
en el que el puesto
el número de escaleras que consiste en
cubos cuya altura no exceda
. Si funciona, la respuesta a nuestro problema será la suma.
Entonces, resolveremos el problema de encontrar el número de escaleras compuesto por
cubos que son altos
. La imagen muestra las escaleras que caen en
:

Como conocemos todas las escaleras, que consisten en menos cubos, las dividiremos.
columna derecha El resultado es una escalera c
cubos Ejemplo para
:

Pero para tales escaleras, el resultado ya se conoce, por lo que clasificaremos todas esas escaleras con un ciclo en
y sumar todos los resultados. De esta manera
Ahora clasificaremos las alturas de las escaleras:
Finalmente cambiando
de
antes
Obtenemos la respuesta.
Importante : en el proceso de construcción de nuestra matriz, es necesario considerar
, ya que de lo contrario algunos tipos de escaleras se “perderán” (cuando se “separen”), pero no hace falta decir que dicha escalera no satisface las condiciones del problema, por lo que la respuesta será el número
.
Ejemplo de código Java que implementa este algoritmo: dp = new long[n + 1][n+1]; d[1][1] = 1; d[2][1] = 0; d[2][2] = 1; for(int i = 3; i < n + 1; i++) { for(int j = 2; j <i; j++) { long cnt = 0; for(int k = 1; k < j; k++) { cnt += d[i - j][k]; } d[i][j] = cnt; } d[i][i] = 1;
La siguiente
tarea se resuelve utilizando una matriz unidimensional.
Entonces lo que tenemos. El primero ent sabe 2 palabras. Cada ent enseña todas las palabras que él mismo conoce dos ents: jóvenes y viejos. A su vez, a los jóvenes se les enseñaron tantas palabras como él ya sabe, y al viejo solo se le enseñó una palabra. Necesitas saber cuántos ents saben exactamente
palabras (es necesario deducir el número de estos módulos ents
)
La solución es bastante simple. Crea una matriz
en el cual en
-º lugar almacenaremos el número de ents (módulo
) quien sabe
palabras Todo comienza con el primer ent, que conoce dos palabras, por lo tanto
. Y luego todo es simple:
- Todos los entrantes que conocen un número impar de palabras son viejos y solo pudieron aprender de los anteriores. Por lo tanto para impar
- En cuanto a los entrantes que saben un número par de palabras, estos son todos aquellos que recibieron la misma cantidad de palabras de los elfos (jóvenes) aquellos que han aprendido de lo anterior (antiguo); es decir, incluso tenemos .
Queda por tratar con el módulo de cálculo. Para no almacenar grandes cantidades, recordaremos de inmediato todos los módulos de valores.
Ejemplo de código Java que implementa este algoritmo: int[] d = new int[K + 1]; if(K >= 2) d[2] = 1; if(P != 1) { for(int i = 3; i <= K; i++) { if(i % 2 != 0) { d[i] = d[i - 1]; } else { d[i] = ((d[i/2] % P) + d[i - 1] % P) % P; } } } else d[K] = 0;
Recursos utilizados:
- Timus Juez en línea;
- Un poco sobre programación dinámica;
- Propiedades de comparación módulo.