
La programación dinámica ( PD ) es tanto un método de optimización matemática como un paradigma algorítmico . El método fue desarrollado por Richard Bellman en la década de 1950 y ha encontrado aplicaciones en numerosos campos, como la ingeniería aeroespacial y la economía .
En ambos contextos, se refiere a simplificar un problema complejo dividiéndolo en subproblemas más simples de forma recursiva . Si bien algunos problemas de decisión no pueden descomponerse de esta manera, las decisiones que abarcan varios momentos en el tiempo sí suelen descomponerse recursivamente. Del mismo modo, en informática, si un problema puede resolverse de forma óptima dividiéndolo en subproblemas y luego encontrando recursivamente las soluciones óptimas para dichos subproblemas, se dice que tiene una subestructura óptima .
Si los subproblemas pueden anidarse recursivamente dentro de problemas más grandes, de modo que se puedan aplicar métodos de programación dinámica, entonces existe una relación entre el valor del problema más grande y los valores de los subproblemas. [ 1 ] En la literatura de optimización, esta relación se denomina ecuación de Bellman .
Descripción general
Optimización matemática
En términos de optimización matemática, la programación dinámica generalmente se refiere a simplificar una decisión dividiéndola en una secuencia de pasos de decisión a lo largo del tiempo. [ 2 ]
Esto se hace definiendo una secuencia de funciones de valor V 1 , V 2 , ..., V n tomando y como argumento que representa el estado del sistema en los instantes i desde 1 hasta n .
La definición de V n ( y ) es el valor obtenido en el estado y en el último instante n .
Los valores V i en tiempos anteriores i = n − 1, n − 2, ..., 2, 1 se pueden encontrar trabajando hacia atrás, utilizando una relación recursiva llamada ecuación de Bellman .
For i = 2, ..., n, Vi−1 at any state y is calculated from Vi by maximizing a simple function (usually the sum) of the gain from a decision at time i − 1 and the function Vi at the new state of the system if this decision is made.
Since Vi has already been calculated for the needed states, the above operation yields Vi−1 for those states.
Finally, V1 at the initial state of the system is the value of the optimal solution. The optimal values of the decision variables can be recovered, one by one, by tracking back the calculations already performed.
Control theory
In control theory, a typical problem is to find an admissible control which causes the system to follow an admissible trajectory on a continuous time interval that minimizes a cost function
The solution to this problem is an optimal control law or policy , which produces an optimal trajectory and a cost-to-go function. The latter obeys the fundamental equation of dynamic programming:
a partial differential equation known as the Hamilton–Jacobi–Bellman equation, in which and . One finds that minimizing in terms of , , and the unknown function and then substitutes the result into the Hamilton–Jacobi–Bellman equation to get the partial differential equation to be solved with boundary condition .[3] In practice, this generally requires numerical techniques for some discrete approximation to the exact optimization relationship.
Alternatively, the continuous process can be approximated by a discrete system, which leads to a following recurrence relation analog to the Hamilton–Jacobi–Bellman equation:
at the -th stage of equally spaced discrete time intervals, and where and denote discrete approximations to and . This functional equation is known as the Bellman equation, which can be solved for an exact solution of the discrete approximation of the optimization equation.[4]
Example from economics: Ramsey's problem of optimal saving
En economía , el objetivo generalmente es maximizar (en lugar de minimizar) alguna función dinámica de bienestar social . En el problema de Ramsey, esta función relaciona las cantidades de consumo con los niveles de utilidad . En términos generales, el planificador se enfrenta a la disyuntiva entre el consumo contemporáneo y el consumo futuro (a través de la inversión en capital que se utiliza en la producción), conocida como elección intertemporal . El consumo futuro se descuenta a una tasa constante.. Una aproximación discreta a la ecuación de transición del capital viene dada por
dóndees el consumo,es capital, yes una función de producción que satisface las condiciones de Inada . Un stock de capital inicialse da por sentado.
DejarSea el consumo en el período t , y supongamos que el consumo produce utilidad.mientras el consumidor viva. Supongamos que el consumidor es impaciente, de modo que descuenta la utilidad futura por un factor b en cada período, donde. Dejarsea el capital en el período t . Suponga que el capital inicial es una cantidad dada.y supongamos que el capital y el consumo de este período determinan el capital del próximo período como, donde A es una constante positiva ySupongamos que el capital no puede ser negativo. Entonces, el problema de decisión del consumidor se puede escribir de la siguiente manera:
- sujeto aa pesar de
Escrito de esta manera, el problema parece complicado, porque implica resolver todas las variables de elección.. (La capital(no es una variable de elección; el capital inicial del consumidor se considera dado).
El enfoque de programación dinámica para resolver este problema implica dividirlo en una secuencia de decisiones más pequeñas. Para ello, definimos una secuencia de funciones de valor., paraque representan el valor de tener cualquier cantidad de capital k en cada momento t . No hay (por supuesto) ninguna utilidad en tener capital después de la muerte,.
El valor de cualquier cantidad de capital en cualquier momento anterior se puede calcular mediante inducción hacia atrás utilizando la ecuación de Bellman . En este problema, para cada, la ecuación de Bellman es
- sujeto a
Este problema es mucho más sencillo que el que escribimos anteriormente, porque involucra solo dos variables de decisión,yIntuitivamente, en lugar de elegir todo su plan de vida al nacer, el consumidor puede ir paso a paso. En el momento t , su capital actual es...Se da, y él solo necesita elegir el consumo actual.y ahorro.
Para resolver este problema, trabajamos hacia atrás. Para simplificar, el nivel actual de capital se denota como k .ya se conoce, así que usando la ecuación de Bellman una vez podemos calculary así sucesivamente hasta que lleguemos a, que es el valor del problema de decisión inicial para toda la vida útil. En otras palabras, una vez que sabemos, podemos calcular, que es el máximo de, dóndees la variable de elección y.
Trabajando hacia atrás, se puede demostrar que la función de valor en el tiempoes
donde cadaes una constante, y la cantidad óptima a consumir en el momentoes
que se puede simplificar a
Observamos que lo óptimo es consumir una mayor proporción de la riqueza actual a medida que uno envejece, para finalmente consumir toda la riqueza restante en el período T , el último período de la vida.
Ciencias de la Computación
Para que un problema sea aplicable a la programación dinámica, debe poseer dos atributos clave: una subestructura óptima y subproblemas superpuestos . Si un problema se puede resolver combinando soluciones óptimas de subproblemas que no se superponen , la estrategia se denomina « divide y vencerás ». [ 1 ] Por esta razón, la ordenación por fusión y la ordenación rápida no se clasifican como problemas de programación dinámica.
Una subestructura óptima significa que la solución a un problema de optimización dado se puede obtener mediante la combinación de soluciones óptimas a sus subproblemas. Estas subestructuras óptimas se suelen describir mediante recursión . Por ejemplo, dado un grafo G=(V,E) , el camino más corto p desde un vértice u a un vértice v presenta una subestructura óptima: tomemos cualquier vértice intermedio w en este camino más corto p . Si p es realmente el camino más corto, entonces se puede dividir en subcaminos p₁ de u a w y p₂ de w a v, de modo que estos, a su vez, sean efectivamente los caminos más cortos entre los vértices correspondientes (mediante el sencillo argumento de cortar y pegar descrito en la Introducción a los algoritmos ) . Por lo tanto, se puede formular fácilmente la solución para encontrar caminos más cortos de manera recursiva, que es lo que hacen el algoritmo de Bellman-Ford o el algoritmo de Floyd-Warshall .
La superposición de subproblemas implica que el espacio de subproblemas debe ser pequeño; es decir, cualquier algoritmo recursivo que resuelva el problema debería resolver los mismos subproblemas repetidamente, en lugar de generar nuevos subproblemas. Por ejemplo, consideremos la formulación recursiva para generar la secuencia de Fibonacci: F i = F i − 1 + F i − 2 , con el caso base F 1 = F 2 = 1. Entonces F 43 = F 42 + F 41 , y F 42 = F 41 + F 40 . Ahora bien, F 41 se resuelve en los subárboles recursivos tanto de F 43 como de F 42 . Aunque el número total de subproblemas es pequeño (solo 43), terminamos resolviendo los mismos problemas una y otra vez si adoptamos una solución recursiva ingenua como esta. La programación dinámica tiene en cuenta este hecho y resuelve cada subproblema solo una vez.

Esto se puede lograr de dos maneras: [ 5 ]
- Enfoque descendente : Este es el resultado directo de la formulación recursiva de cualquier problema. Si la solución a un problema puede formularse recursivamente utilizando la solución a sus subproblemas, y si estos subproblemas se superponen, entonces se pueden memorizar o almacenar fácilmente las soluciones de los subproblemas en una tabla (generalmente un array o una tabla hash ). Cada vez que intentamos resolver un nuevo subproblema, primero consultamos la tabla para ver si ya está resuelto. Si se ha registrado una solución, podemos usarla directamente; de lo contrario, resolvemos el subproblema y agregamos su solución a la tabla.
- Enfoque ascendente : Una vez que formulamos la solución a un problema recursivamente en términos de sus subproblemas, podemos intentar reformular el problema de forma ascendente: intentar resolver primero los subproblemas y usar sus soluciones para construir y llegar a soluciones para subproblemas más grandes. Esto también se suele hacer en forma tabular generando iterativamente soluciones para subproblemas cada vez mayores utilizando las soluciones de subproblemas pequeños. Por ejemplo, si ya conocemos los valores de F 41 y F 40 , podemos calcular directamente el valor de F 42 .
Algunos lenguajes de programación pueden memorizar automáticamente el resultado de una llamada a función con un conjunto particular de argumentos, para acelerar la evaluación por nombre (este mecanismo se conoce como llamada por necesidad ). Algunos lenguajes lo hacen posible de forma portable (por ejemplo, Scheme , Common Lisp , Perl o D ). Algunos lenguajes tienen memorización automática integrada, como Prolog tabulado y J , que admite memorización con el adverbio M. [ 6 ] En cualquier caso, esto solo es posible para una función referencialmente transparente . La memorización también se encuentra como un patrón de diseño fácilmente accesible dentro de lenguajes basados en reescritura de términos como Wolfram Language .
Bioinformática
La programación dinámica se utiliza ampliamente en bioinformática para tareas como el alineamiento de secuencias , el plegamiento de proteínas , la predicción de la estructura del ARN y la unión proteína-ADN. Los primeros algoritmos de programación dinámica para la unión proteína-ADN fueron desarrollados en la década de 1970 de forma independiente por Charles DeLisi en Estados Unidos [ 7 ] y por Georgii Gurskii y Alexander Zasedatelev en la Unión Soviética [ 8 ] . Recientemente, estos algoritmos se han vuelto muy populares en bioinformática y biología computacional , particularmente en los estudios de posicionamiento de nucleosomas y unión de factores de transcripción .
Ejemplos: algoritmos informáticos
Algoritmo de Dijkstra para el problema del camino más corto
Desde el punto de vista de la programación dinámica, el algoritmo de Dijkstra para el problema del camino más corto es un esquema de aproximación sucesiva que resuelve la ecuación funcional de programación dinámica para el problema del camino más corto mediante el método de alcance . [ 9 ] [ 10 ] [ 11 ]
De hecho, la explicación de Dijkstra sobre la lógica detrás del algoritmo, [ 12 ] es decir
Problema 2. Hallar el camino de longitud total mínima entre dos nodos dados.y.
Utilizamos el hecho de que, sies un nodo en la ruta mínima desdea, el conocimiento de este último implica el conocimiento del camino mínimo desdea.
es una paráfrasis del famoso Principio de Optimalidad de Bellman en el contexto del problema del camino más corto .
secuencia de Fibonacci
El uso de programación dinámica en el cálculo del enésimo término de la secuencia de Fibonacci mejora considerablemente su rendimiento. He aquí una implementación sencilla, basada directamente en la definición matemática:
función fib(n) si n <= 1 devuelve n devuelve fib(n − 1) + fib(n − 2)
Observe que si llamamos, por ejemplo, a fib(5), producimos un árbol de llamadas que llama a la función sobre el mismo valor muchas veces diferentes:
fib(5)fib(4) + fib(3)(fib(3) + fib(2)) + (fib(2) + fib(1))((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
En particular, fib(2)se calculó tres veces desde cero. En ejemplos más grandes, se recalculan muchos más valores de fib, o subproblemas , lo que da como resultado un algoritmo de tiempo exponencial.
Ahora bien, supongamos que tenemos un objeto de mapa simple, m , que asigna a cada valor fibque ya se ha calculado su resultado, y modificamos nuestra función para usarlo y actualizarlo. La función resultante requiere solo O ( n ) tiempo en lugar de tiempo exponencial (pero requiere O( n ) espacio):
var m := map (0 → 0, 1 → 1) function fib(n) if key n is not in map m m[n] := fib(n − 1) + fib(n − 2) devolver m[n]
Esta técnica de guardar valores que ya se han calculado se llama memorización ; este es el enfoque de arriba hacia abajo, ya que primero dividimos el problema en subproblemas y luego calculamos y almacenamos los valores.
En el enfoque ascendente , primero calculamos los valores más pequeños fiby luego construimos valores más grandes a partir de ellos. Este método también utiliza un tiempo de O( n ) ya que contiene un bucle que se repite n − 1 veces, pero solo ocupa un espacio constante (O(1)), a diferencia del enfoque descendente que requiere un espacio de O( n ) para almacenar el mapa.
función fib(n) si n = 0 devuelve 0 sino var previousFib := 0, currentFib := 1 repetir n − 1 veces // el bucle se omite si n = 1 var newFib := previousFib + currentFib Fibrosis anterior := Fibrosis actual Fibrilación actual := Fibrilación nueva devolver la fibrilación actual
En ambos ejemplos, calculamos solo fib(2)una vez y luego lo usamos para calcular tanto como fib(4), fib(3)en lugar de calcularlo cada vez que se evalúa cualquiera de ellos.
Un tipo de matriz equilibrada 0-1
Consideremos el problema de asignar valores, ya sean cero o uno, a las posiciones de una matriz n × n , con n par, de modo que cada fila y cada columna contenga exactamente n / 2 ceros y n / 2 unos. Nos preguntamos cuántas asignaciones diferentes existen para un n dado . Por ejemplo, cuando n = 4 , hay cinco posibles soluciones:
Existen al menos tres enfoques posibles: fuerza bruta , retroceso y programación dinámica.
La fuerza bruta consiste en comprobar todas las asignaciones de ceros y unos y contar aquellas que tienen filas y columnas equilibradas ( n /2 ceros y n /2 unos). Como hayposibles asignaciones yasignaciones sensatas, esta estrategia no es práctica para valores arbitrariamente grandes de.
El método de retroceso para este problema consiste en elegir un orden determinado de los elementos de la matriz y colocar recursivamente unos o ceros, mientras se comprueba que en cada fila y columna el número de elementos que no se han asignado más el número de unos o ceros sean ambos al menos n / 2. Si bien es más sofisticado que la fuerza bruta, este enfoque visitará cada solución una vez, lo que lo hace poco práctico para n mayor que seis, ya que el número de soluciones ya es116 963 796 250 para n = 8, como veremos.
La programación dinámica permite contar el número de soluciones sin visitarlas todas. Imaginemos retroceder los valores de la primera fila: ¿qué información necesitaríamos sobre las filas restantes para poder contar con precisión las soluciones obtenidas para cada valor de la primera fila? Consideramos tableros de k × n , donde 1 ≤ k ≤ n , cuyas k filas contienenceros yunos. La función f a la que se aplica la memorización asigna vectores de n pares de enteros al número de tableros admisibles (soluciones). Hay un par para cada columna, y sus dos componentes indican respectivamente el número de ceros y unos que aún no se han colocado en esa columna. Buscamos el valor de( n argumentos o un vector de n elementos). El proceso de creación de subproblemas implica iterar sobre cada uno de ellos. posibles asignaciones para la fila superior del tablero, y recorriendo cada columna, restando uno al elemento apropiado del par para esa columna, dependiendo de si la asignación para la fila superior contenía un cero o un uno en esa posición. Si alguno de los resultados es negativo, entonces la asignación es inválida y no contribuye al conjunto de soluciones (la recursión se detiene). De lo contrario, tenemos una asignación para la fila superior del tablero k × n y calculamos recursivamente el número de soluciones para el tablero restante ( k − 1) × n , sumando los números de soluciones para cada asignación admisible de la fila superior y devolviendo la suma, que se está memorizando. El caso base es el subproblema trivial, que ocurre para un tablero 1 × n . El número de soluciones para este tablero es cero o uno, dependiendo de si el vector es una permutación de n / 2 (0, 1) y n / 2 (1, 0) pares o no.
Por ejemplo, en los dos primeros tableros mostrados arriba, las secuencias de vectores serían:
((2, 2) (2, 2) (2, 2) (2, 2)) ((2, 2) (2, 2) (2, 2) (2, 2)) k = 4 0 1 0 1 0 0 1 1 ((1, 2) (2, 1) (1, 2) (2, 1)) ((1, 2) (1, 2) (2, 1) (2, 1)) k = 3 1 0 1 0 0 0 1 1 ((1, 1) (1, 1) (1, 1) (1, 1)) ((0, 2) (0, 2) (2, 0) (2, 0)) k = 2 0 1 0 1 1 1 0 0 ((0, 1) (1, 0) (0, 1) (1, 0)) ((0, 1) (0, 1) (1, 0) (1, 0)) k = 1 1 0 1 0 1 1 0 0 ((0, 0) (0, 0) (0, 0) (0, 0)) ((0, 0) (0, 0), (0, 0) (0, 0))
El número de soluciones (secuencia A058527 en el OEIS ) es
- 1, 2, 90,297 200 ,116 963 796 250 ,6 736 218 287 430 460 752 , ...
Entre los enlaces externos se pueden encontrar enlaces a la implementación de MAPLE del enfoque de programación dinámica .
Tablero de damas
Consideremos un tablero de ajedrez con n × n casillas y una función de costo c(i, j)que devuelve un costo asociado a cada casilla (i,j)( isiendo la fila, jsiendo la columna). Por ejemplo (en un tablero de ajedrez de 5 × 5),
De este modoc(1, 3) = 5
Digamos que hay una ficha que puede comenzar en cualquier casilla de la primera fila y queremos saber el camino más corto (la suma de los costos mínimos en cada fila visitada) para llegar a la última fila; suponiendo que la ficha solo puede moverse en diagonal hacia la izquierda, en diagonal hacia la derecha o en línea recta. Es decir, una ficha en (1,3)puede moverse a (2,2), (2,3)o (2,4).
Este problema presenta una subestructura óptima . Es decir, la solución al problema completo depende de las soluciones a los subproblemas. Definamos una función q(i, j)como
- q ( i , j ) = el costo mínimo para llegar al cuadrado ( i , j ).
Comenzando en el rango ny descendiendo hasta el rango 1, calculamos el valor de esta función para todos los cuadrados en cada rango sucesivo. Eligiendo el cuadrado que tiene el valor mínimo en cada rango obtenemos el camino más corto entre el rango ny el rango 1.
La función q(i, j)es igual al costo mínimo para llegar a cualquiera de las tres casillas que están debajo de ella (ya que esas son las únicas casillas que pueden alcanzarla) más c(i, j). Por ejemplo:
Ahora, definamos q(i, j)en términos algo más generales:
La primera línea de esta ecuación trata sobre un tablero modelado como casillas indexadas en 1el límite inferior y nen el límite superior. La segunda línea especifica lo que sucede en el primer rango, proporcionando un caso base. La tercera línea, la recursión, es la parte importante. Representa los A,B,C,Dtérminos del ejemplo. A partir de esta definición, podemos derivar un código recursivo directo para . En el siguiente pseudocódigo, es el tamaño del tablero, es la función de costo y devuelve el mínimo de varios valores:q(i, j)nc(i, j)min()
función minCost(i, j) si j < 1 o j > n devuelve infinito sino si i = 1 devuelve c(i, j) sino devuelve min ( minCost(i-1, j-1), minCost(i-1, j), minCost(i-1, j+1) ) + c(i, j)Esta función solo calcula el costo de la ruta, no la ruta en sí. Analizaremos la ruta en sí más adelante. Esto, al igual que el ejemplo de los números de Fibonacci, es terriblemente lento porque también presenta el atributo de subproblemas superpuestos . Es decir, recalcula los mismos costos de ruta una y otra vez. Sin embargo, podemos calcularlo mucho más rápido de abajo hacia arriba si almacenamos los costos de ruta en una matriz bidimensional q[i, j]en lugar de usar una función. Esto evita el recálculo; todos los valores necesarios para la matriz q[i, j]se calculan de antemano solo una vez. Los valores precalculados para (i,j)simplemente se consultan cuando sea necesario.
También necesitamos saber cuál es el camino más corto real. Para ello, utilizamos otro arreglo p[i, j]; un arreglo de predecesores . Este arreglo registra el camino a cualquier casilla s. El predecesor de sse modela como un desplazamiento relativo al índice (en q[i, j]) del costo de camino precalculado de s. Para reconstruir el camino completo, buscamos el predecesor de s, luego el predecesor de esa casilla, luego el predecesor de esa casilla, y así sucesivamente de forma recursiva, hasta llegar a la casilla de inicio. Considere el siguiente pseudocódigo:
función computeShortestPathArrays() para x desde 1 hasta n q[1, x] := c(1, x) para y de 1 a n q[y, 0] := infinito q[y, n + 1] := infinito para y de 2 a n para x de 1 a n m := min(q[y-1, x-1], q[y-1, x], q[y-1, x+1]) q[y, x] := m + c(y, x) si m = q[y-1, x-1] p[y, x] := -1 de lo contrario, si m = q[y-1, x] p[y, x] := 0 de lo contrario p[y, x] := 1
Ahora, el resto es simplemente cuestión de encontrar el mínimo e imprimirlo.
función computeShortestPath() calcularShortestPathArrays() minIndex := 1 min := q[n, 1] para i desde 2 hasta n si q[n, i] < min minIndex := i min := q[n, i] printPath(n, minIndex)función printPath(y, x) print (x) print (" < -") if y = 2 print (x + p[y, x]) else printPath(y-1, x + p[y, x])Alineación de secuencias
En genética , el alineamiento de secuencias es una aplicación importante donde la programación dinámica es esencial. [ 13 ] Típicamente, el problema consiste en transformar una secuencia en otra usando operaciones de edición que reemplazan, insertan o eliminan un elemento. Cada operación tiene un costo asociado, y el objetivo es encontrar la secuencia de ediciones con el menor costo total .
El problema se puede plantear de forma natural como una recursión: una secuencia A se edita de forma óptima en una secuencia B mediante:
- insertando el primer carácter de B y realizando una alineación óptima de A y la cola de B.
- eliminando el primer carácter de A y realizando la alineación óptima de la cola de A y B.
- reemplazar el primer carácter de A con el primer carácter de B y realizar alineaciones óptimas de las colas de A y B.
Las alineaciones parciales se pueden tabular en una matriz, donde la celda (i,j) contiene el costo de la alineación óptima de A[1..i] a B[1..j]. El costo en la celda (i,j) se puede calcular sumando el costo de las operaciones relevantes al costo de las celdas vecinas y seleccionando la óptima.
Existen diferentes variantes; véanse el algoritmo de Smith-Waterman y el algoritmo de Needleman-Wunsch .
Rompecabezas de la Torre de Hanoi


La Torre de Hanoi es un juego matemático o rompecabezas . Consta de tres varillas y varios discos de diferentes tamaños que se deslizan sobre cualquiera de ellas. El juego comienza con los discos apilados ordenadamente en orden ascendente de tamaño sobre una varilla, con el más pequeño en la parte superior, formando así una figura cónica.
El objetivo del rompecabezas es mover toda la pila a otra barra, obedeciendo las siguientes reglas:
- Solo se puede mover un disco a la vez.
- Cada movimiento consiste en tomar el disco superior de una de las varillas y deslizarlo sobre otra varilla, encima de los demás discos que ya puedan estar presentes en esa varilla.
- No se puede colocar ningún disco encima de un disco más pequeño.
La solución de programación dinámica consiste en resolver la ecuación funcional.
- S(n,h,t) = S(n-1,h, no(h,t)) ; S(1,h,t) ; S(n-1, no(h,t),t)
donde n denota el número de discos a mover, h denota la varilla de inicio, t denota la varilla objetivo, not(h,t) denota la tercera varilla (ni h ni t), ";" denota concatenación, y
- S(n, h, t) := solución a un problema que consiste en n discos que deben moverse de la varilla h a la varilla t.
Para n=1 el problema es trivial, a saber S(1,h,t) = "mover un disco de la varilla h a la varilla t" (solo queda un disco).
El número de movimientos requeridos por esta solución es 2 n − 1. Si el objetivo es maximizar el número de movimientos (sin ciclos), entonces la ecuación funcional de programación dinámica es un poco más compleja y se requieren 3 n − 1 movimientos. [ 14 ]
Rompecabezas de lanzamiento de huevos
Un famoso acertijo consiste en dejar caer huevos desde un edificio para determinar a qué altura comienzan a romperse. La siguiente es una descripción que involucra N=2 huevos y un edificio con H=36 pisos: [ 15 ]
- Supongamos que queremos saber desde qué pisos de un edificio de 36 plantas se pueden dejar caer huevos sin peligro, y desde cuáles se romperán al caer (usando la terminología del inglés estadounidense , donde la primera planta está a nivel del suelo). Partimos de algunas suposiciones:
- Un huevo que sobrevive a una caída puede volver a utilizarse.
- Un huevo roto debe desecharse.
- El efecto de una caída es el mismo para todos los huevos.
- Si un huevo se rompe al caer, también se rompería si se dejara caer desde una ventana más alta.
- Si un huevo sobrevive a una caída, también sobreviviría a una caída más corta.
- No se descarta que las ventanas del primer piso rompan los huevos, ni tampoco se descarta que los huevos puedan sobrevivir a las ventanas del piso 36.
- Si solo disponemos de un huevo y queremos asegurarnos de obtener el resultado correcto, el experimento solo puede realizarse de una manera. Dejamos caer el huevo desde la ventana del primer piso; si sobrevive, lo dejamos caer desde la ventana del segundo piso. Continuamos subiendo hasta que se rompa. En el peor de los casos, este método puede requerir 36 intentos. Supongamos que disponemos de 2 huevos. ¿Cuál es el número mínimo de intentos que garantizan el éxito en todos los casos?
Para derivar una ecuación funcional de programación dinámica para este rompecabezas, sea el estado del modelo de programación dinámica un par s = (n,k), donde
- n = número de huevos de prueba disponibles, n = 0, 1, 2, 3, ..., N − 1.
- k = número de pisos (consecutivos) que aún no se han probado, k = 0, 1, 2, ..., H − 1.
Por ejemplo, s = (2,6) indica que hay dos huevos de prueba disponibles y que quedan 6 pisos (consecutivos) por probar. El estado inicial del proceso es s = ( N , H ), donde N representa la cantidad de huevos de prueba disponibles al comienzo del experimento. El proceso finaliza cuando no quedan huevos de prueba ( n = 0) o cuando k = 0, lo que ocurra primero. Si la finalización se produce en el estado s = (0, k ) y k > 0, entonces la prueba ha fallado.
Ahora, dejemos
- W ( n , k ) = número mínimo de ensayos necesarios para identificar el valor del piso crítico en el peor escenario posible dado que el proceso está en el estado s = ( n , k ).
Entonces se puede demostrar que [ 16 ]
- W ( n , k ) = 1 + min{max( W ( n − 1, x − 1), W ( n , k − x )): x = 1, 2, ..., k }
con W ( n ,0) = 0 para todo n > 0 y W (1, k ) = k para todo k . Es fácil resolver esta ecuación iterativamente aumentando sistemáticamente los valores de n y k .
Solución DP más rápida utilizando una parametrización diferente.
Observe que la solución anterior tomatiempo con una solución DP. Esto se puede mejorar paratiempo mediante búsqueda binaria en el óptimoen la recurrencia anterior, ya queestá aumentando enmientrasestá disminuyendo en, por lo tanto, un mínimo local dees un mínimo global. Además, al almacenar el óptimoPara cada celda de la tabla DP y en referencia a su valor en la celda anterior, el óptimopara cada célula se puede encontrar en tiempo constante, mejorándolo atiempo. Sin embargo, existe una solución aún más rápida que implica una parametrización diferente del problema:
Dejarsea el número total de pisos tales que los huevos se rompen al caer desde elpiso (El ejemplo anterior es equivalente a tomar).
Dejarsea el piso mínimo desde el cual se debe dejar caer el huevo para que se rompa.
Dejarsea el número máximo de valores deque se distinguen medianteintenta yhuevos.
Entoncesa pesar de.
DejarSea el piso desde el cual se deja caer el primer huevo en la estrategia óptima.
Si el primer huevo se rompió,es deay distinguible usando como máximointenta yhuevos.
Si el primer huevo no se rompió,es deay distinguible medianteintenta yhuevos.
Por lo tanto,.
Entonces el problema es equivalente a encontrar el mínimode tal manera que.
Para ello, podríamos calcularen orden creciente, lo que tomaríatiempo.
Por lo tanto, si tratamos por separado el caso de, el algoritmo tomaríatiempo.
Pero la relación de recurrencia sí puede resolverse, dando como resultado:, que se puede calcular entiempo usando la identidada pesar de.
Desdea pesar de, podemos realizar una búsqueda binaria enencontrar, dando unaalgoritmo. [ 17 ]
Multiplicación de cadenas de matrices
La multiplicación de cadenas de matrices es un ejemplo bien conocido que demuestra la utilidad de la programación dinámica. Por ejemplo, las aplicaciones de ingeniería a menudo tienen que multiplicar una cadena de matrices. No es sorprendente encontrar matrices de grandes dimensiones, por ejemplo 100×100. Por lo tanto, nuestra tarea es multiplicar matricesLa multiplicación de matrices no es conmutativa, sino asociativa; y solo podemos multiplicar dos matrices a la vez. Por lo tanto, podemos multiplicar esta cadena de matrices de muchas maneras diferentes, por ejemplo :
- (( A 1 × A 2 ) × A 3 ) × ... A n
- A 1 ×((( A 2 × A 3 )× ... ) × A n )
- ( A 1 × A 2 ) × ( A 3 × ... A n )
y así sucesivamente. Existen numerosas maneras de multiplicar esta cadena de matrices. Todas producirán el mismo resultado final, aunque el tiempo de cálculo variará según las matrices que se multipliquen. Si la matriz A tiene dimensiones m×n y la matriz B tiene dimensiones n×q, entonces la matriz C=A×B tendrá dimensiones m×q y requerirá m*n*q multiplicaciones escalares (utilizando un algoritmo simplificado de multiplicación de matrices a modo de ejemplo).
Por ejemplo, multipliquemos las matrices A, B y C. Supongamos que sus dimensiones son m×n, n×p y p×s, respectivamente. La matriz A×B×C tendrá un tamaño de m×s y se puede calcular de dos maneras, como se muestra a continuación:
- Ax(B×C) Este orden de multiplicación de matrices requerirá nps + mns multiplicaciones escalares.
- (A×B)×C Este orden de multiplicación de matrices requerirá mnp + mps cálculos escalares.
Supongamos que m = 10, n = 100, p = 10 y s = 1000. Por lo tanto, la primera forma de multiplicar la cadena requerirá 1.000.000 + 1.000.000 de cálculos. La segunda forma requerirá solo 10.000 + 100.000 cálculos. Obviamente, la segunda forma es más rápida, y deberíamos multiplicar las matrices utilizando esa disposición de paréntesis.
Por lo tanto, nuestra conclusión es que el orden de los paréntesis importa, y que nuestra tarea es encontrar el orden óptimo de los paréntesis.
En este punto, tenemos varias opciones, una de las cuales es diseñar un algoritmo de programación dinámica que divida el problema en problemas superpuestos y calcule la disposición óptima de los paréntesis. La solución de programación dinámica se presenta a continuación.
Llamemos m[i,j] al número mínimo de multiplicaciones escalares necesarias para multiplicar una cadena de matrices desde la matriz i hasta la matriz j (es decir, A i × ... × A j , es decir, i ≤ j). Dividimos la cadena en alguna matriz k, de modo que i ≤ k < j, e intentamos encontrar qué combinación produce el mínimo m[i,j].
La fórmula es:
si i = j, m[i,j]= 0 si i < j, m[i,j]= min sobre todos los valores posibles de k (m[i,k]+m[k+1,j] +)donde k varía de i a j − 1.
- es la dimensión de fila de la matriz i,
- es la dimensión de columna de la matriz k,
- es la dimensión de columna de la matriz j.
Esta fórmula se puede codificar como se muestra a continuación, donde el parámetro de entrada "chain" es la cadena de matrices, es decir:
función OptimalMatrixChainParenthesis(cadena) n = longitud(cadena) para i = 1, n m[i,i] = 0 // Dado que no se requieren cálculos para multiplicar una matriz para len = 2, n para i = 1, n - len + 1 j = i + len -1 m[i,j] = infinito // De modo que el primer cálculo se actualiza para k = i, j-1 q = m[i, k] + m[k+1, j] +Si q < m[i, j] // El nuevo orden de paréntesis es mejor que el que teníamos m[i, j] = q // Actualizar s[i, j] = k // Registrar en qué k dividir, es decir, dónde colocar el paréntesisHasta ahora, hemos calculado valores para todos los posibles m [ i , j ] , el número mínimo de cálculos para multiplicar una cadena de la matriz i a la matriz j , y hemos registrado el "punto de división" correspondiente s [ i , j ] . Por ejemplo, si estamos multiplicando la cadena A 1 × A 2 × A 3 × A 4 , y resulta que m [1, 3] = 100 y s [1, 3] = 2 , eso significa que la colocación óptima de paréntesis para las matrices 1 a 3 esy multiplicar esas matrices requerirá 100 cálculos escalares.
Este algoritmo generará las tablas m [, ] y s [, ] que contendrán entradas para todos los valores posibles de i y j. La solución final para toda la cadena es m[1, n], con la división correspondiente en s[1, n]. El desarrollo de la solución será recursivo, comenzando desde el principio y continuando hasta llegar al caso base, es decir, la multiplicación de matrices individuales.
Por lo tanto, el siguiente paso es dividir la cadena, es decir, colocar los paréntesis donde (óptimamente) corresponden. Para ello, podríamos utilizar el siguiente algoritmo:
función ImprimirParéntesisÓptimos(s, i, j) si i = j imprimir "A"i demás imprimir "(" ImprimirParéntesisÓptimo(s, i, s[i, j]) ImprimirParéntesisÓptimo(s, s[i, j] + 1, j) imprimir ")"Por supuesto, este algoritmo no sirve para realizar multiplicaciones reales. Simplemente es una forma sencilla de visualizar el resultado.
Para multiplicar las matrices utilizando las divisiones adecuadas, necesitamos el siguiente algoritmo:
función MatrixChainMultiply ( cadena de 1 a n ) // devuelve la matriz final, es decir, A1×A2×... ×An OptimalMatrixChainParenthesis ( cadena de 1 a n ) // esto producirá las "tablas" s[.] y m[.] OptimalMatrixMultiplication ( s , cadena de 1 a n ) // realmente multiplicafunción MultiplicaciónMatricesÓptima ( s , i , j ) // devuelve el resultado de multiplicar una cadena de matrices desde Ai hasta Aj de forma óptima si i < j // continúa dividiendo la cadena y multiplicando las matrices en los lados izquierdo y derecho LadoIzquierdo = MultiplicaciónMatricesÓptima ( s , i , s [ i , j ]) LadoDerecho = MultiplicaciónMatricesÓptima ( s , s [ i , j ] + 1 , j ) return MultiplicaciónMatrices ( LadoIzquierdo , LadoDerecho ) else if i = j return Ai // matriz en la posición i else print "error, i <= j debe mantenerse"función MatrixMultiply ( A , B ) // función que multiplica dos matrices si columnas ( A ) = filas ( B ) para i = 1 , filas ( A ) para j = 1 , columnas ( B ) C [ i , j ] = 0 para k = 1 , columnas ( A ) C [ i , j ] = C [ i , j ] + A [ i , k ] * B [ k , j ] retornar C sino imprimir "error, dimensiones incompatibles."Historia del nombre
El término programación dinámica fue utilizado originalmente en la década de 1940 por Richard Bellman para describir el proceso de resolución de problemas en los que se necesita encontrar las mejores decisiones una tras otra. En 1953, lo perfeccionó hasta darle su significado moderno, refiriéndose específicamente a la anidación de problemas de decisión más pequeños dentro de decisiones más grandes, [ 18 ] y, posteriormente, el campo fue reconocido por el IEEE como un tema de análisis e ingeniería de sistemas . La contribución de Bellman se recuerda en el nombre de la ecuación de Bellman , un resultado fundamental de la programación dinámica que reformula un problema de optimización en forma recursiva .
Bellman explica el razonamiento detrás del término programación dinámica en su autobiografía, Eye of the Hurricane: An Autobiography :
Pasé el trimestre de otoño (de 1950) en RAND . Mi primera tarea fue encontrar un nombre para los procesos de decisión multietapa. Una pregunta interesante es: "¿De dónde surgió el nombre, programación dinámica?". Los años 50 no fueron buenos para la investigación matemática. Teníamos un caballero muy interesante en Washington llamado Wilson . Era Secretario de Defensa y, de hecho, tenía un miedo y un odio patológicos hacia la palabra "investigación". No uso el término a la ligera; lo uso con precisión. Su rostro se enrojecía, se ponía rojo y se ponía violento si alguien usaba el término investigación en su presencia. Pueden imaginar cómo se sentía, entonces, con respecto al término matemáticas. La Corporación RAND era empleada por la Fuerza Aérea, y la Fuerza Aérea tenía a Wilson como su jefe, esencialmente. Por lo tanto, sentí que tenía que hacer algo para proteger a Wilson y a la Fuerza Aérea del hecho de que realmente estaba haciendo matemáticas dentro de la Corporación RAND. ¿Qué título, qué nombre podía elegir? En primer lugar, me interesaba la planificación, la toma de decisiones, el pensamiento. Pero «planificación» no es una buena palabra por varias razones. Por lo tanto, decidí usar la palabra «programación». Quería transmitir la idea de que esto era dinámico, multifásico y variable en el tiempo. Pensé: matemos dos pájaros de un tiro. Tomemos una palabra con un significado absolutamente preciso, a saber, dinámico, en el sentido físico clásico. También tiene una propiedad muy interesante como adjetivo: es imposible usar la palabra «dinámico» en sentido peyorativo. Intenten pensar en alguna combinación que pueda darle un significado peyorativo. Es imposible. Así que pensé que «programación dinámica» era un buen nombre. Era algo a lo que ni siquiera un congresista podría oponerse. Así que lo usé como término general para mis actividades.
— Richard Bellman, El ojo del huracán: una autobiografía (1984, página 159)
Bellman eligió la palabra «dinámico» para reflejar el carácter variable en el tiempo de los problemas y porque sonaba impactante. [ 13 ] La palabra «programación» se refería al uso del método para encontrar un programa óptimo , en el sentido de un cronograma militar para entrenamiento o logística. Este uso es el mismo que el de las expresiones « programación lineal» y «programación matemática» , sinónimo de «optimización matemática» . [ 19 ]
La explicación anterior sobre el origen del término podría ser inexacta: Según Russell y Norvig, la historia anterior "no puede ser estrictamente cierta, porque su primer artículo que utilizó el término (Bellman, 1952) apareció antes de que Wilson se convirtiera en Secretario de Defensa en 1953". [ 20 ] Asimismo, Harold J. Kushner afirmó en un discurso que, "Por otro lado, cuando le hice [a Bellman] la misma pregunta, respondió que estaba tratando de eclipsar la programación lineal de Dantzig añadiéndole dinámica. Quizás ambas motivaciones fueran ciertas". [ 21 ]
Véase también
- Convexidad en economía : un tema importante en economía.
- Algoritmo voraz : secuencia de elecciones localmente óptimas.
- No convexidad (economía) – Violaciones de los supuestos de convexidad de la economía elemental
- Programación estocástica : marco para modelar problemas de optimización que implican incertidumbre.
- Programación dinámica estocástica : técnica de 1957 para modelar problemas de toma de decisiones en condiciones de incertidumbre.
- Aprendizaje por refuerzo : campo del aprendizaje automático
Referencias
- 1 2 Cormen, TH; Leiserson, CE; Rivest, RL; Stein, C. (2001), Introducción a los algoritmos (2ª ed.), MIT Press & McGraw-Hill, ISBN 0-262-03293-7. págs. 344.
- ↑ Cormen, Thomas H. (2009). Introducción a los algoritmos (3.ª ed.). EE. UU.: The MIT Press . pág. 359. ISBN 978-0262033848.
{{cite book}}: Mantenimiento CS1: fecha y año ( enlace ) - ↑ Kamien, MI ; Schwartz, NL (1991). Optimización dinámica: El cálculo de variaciones y el control óptimo en economía y administración (Segunda edición). Nueva York: Elsevier. pág. 261. ISBN 978-0-444-01609-6.
- ↑ Kirk, Donald E. (1970). Teoría del control óptimo: una introducción . Englewood Cliffs, NJ: Prentice-Hall. págs. 94–95 . ISBN 978-0-13-638098-6.
- ↑ "Algoritmos de Jeff Erickson" . jeffe.cs.illinois.edu . Consultado el 6 de diciembre de 2024 .
- ↑ "M. Memo" . Vocabulario J. Software J. Consultado el 28 de octubre de 2011 .
- ↑ Delisi, Charles (julio de 1974), "Fenómenos cooperativos en homopolímeros: una formulación alternativa de la función de partición", Biopolymers , 13 (7): 1511–1512 , doi : 10.1002/bip.1974.360130719
- ↑ Gurskiĭ, GV; Zasedatelev, AS (septiembre de 1978), "Relaciones precisas para calcular la unión de proteínas reguladoras y otros ligandos reticulares en polinucleótidos de doble cadena", Biofizika , 23 (5): 932–946 , PMID 698271
- ↑ Sniedovich, M. (2006), "El algoritmo de Dijkstra revisitado: la conexión con la programación dinámica" (PDF) , Journal of Control and Cybernetics , 35 (3): 599– 620.Versión en línea del artículo con módulos computacionales interactivos.
- ↑ Denardo, EV (2003), Programación dinámica: modelos y aplicaciones , Mineola, NY: Dover Publications , ISBN 978-0-486-42810-9
- ↑ Sniedovich, M. (2010), Programación dinámica: Fundamentos y principios , Taylor & Francis , ISBN 978-0-8247-4099-3
- ^ Dijkstra, EW (diciembre de 1959). "Una nota sobre dos problemas relacionados con los gráficos". Matemática numérica . 1 (1): 269– 271. doi : 10.1007/BF01386390 .
- 1 2 Eddy, SR (2004). "¿Qué es la programación dinámica?". Nature Biotechnology . 22 (7): 909– 910. doi : 10.1038/nbt0704-909 . PMID 15229554 . S2CID 5352062 .
- ↑ Moshe Sniedovich (2002), "Juegos OR/MS: 2. El problema de las torres de Hanoi", INFORMS Transactions on Education , 3 (1): 34– 51, doi : 10.1287/ited.3.1.45 .
- ↑ Konhauser JDE, Velleman, D., y Wagon, S. (1996). ¿Hacia dónde fue la bicicleta? Exposiciones matemáticas Dolciani – N° 18. La Asociación Matemática de América .
- ↑ Sniedovich, Moshe (2003). "Juegos OR/MS: 4. El placer de lanzar huevos en Braunschweig y Hong Kong" . INFORMS Transactions on Education . 4 (1): 48– 64. doi : 10.1287/ited.4.1.48 .
- ↑ Dean Connable Wills, Conexiones entre la combinatoria de permutaciones y los algoritmos y la geometría
- ↑ Stuart Dreyfus. "Richard Bellman sobre el nacimiento de la programación dinámica" .
- ↑ Nocedal, J.; Wright, SJ (2006). Optimización numérica . Springer. pág . 9. ISBN 9780387303031.
- ↑ Russell, S.; Norvig, P. (2009). Inteligencia artificial: un enfoque moderno (3.ª ed.). Prentice Hall. ISBN 978-0-13-207148-2.
- ↑ Kushner, Harold J. (1 de julio de 2004). "Premio Richard E. Bellman al Patrimonio del Control" . Archivado del original el 19 de octubre de 2014.
Lecturas adicionales
- Adda, Jerome; Cooper, Russell (2003), Economía dinámica , MIT Press, ISBN 9780262012010Una introducción accesible a la programación dinámica en economía. Código MATLAB del libro archivado el 9 de octubre de 2020 en Wayback Machine .
- Bellman, Richard (1954), "La teoría de la programación dinámica", Boletín de la Sociedad Matemática Americana , 60 (6): 503– 516, doi : 10.1090/S0002-9904-1954-09848-8 , MR 0067459 Incluye una extensa bibliografía de la literatura sobre el tema, hasta el año 1954.
- Bellman, Richard (1957), Programación dinámica , Princeton University PressEdición de bolsillo de Dover (2003), ISBN 0-486-42809-5.
- Cormen, Thomas H .; Leiserson, Charles E .; Rivest, Ronald L .; Stein, Clifford (2001), Introducción a los algoritmos (2ª ed.), MIT Press & McGraw–Hill, ISBN 978-0-262-03293-3. Especialmente págs. 323–69.
- Dreyfus, Stuart E.; Law, Averill M. (1977), El arte y la teoría de la programación dinámica , Academic Press, ISBN 978-0-12-221860-6.
- Giegerich, R.; Meyer, C.; Steffen, P. (2004), "Una disciplina de programación dinámica sobre datos secuenciales" (PDF) , Science of Computer Programming , 51 (3): 215– 263, doi : 10.1016/j.scico.2003.12.005.
- Meyn, Sean (2007), Técnicas de control para redes complejas , Cambridge University Press, ISBN 978-0-521-88441-9Archivado del original el 19 de junio de 2010..
- Sritharan, SS (1991). "Programación dinámica de las ecuaciones de Navier-Stokes". Systems and Control Letters . 16 (4): 299– 307. doi : 10.1016/0167-6911(91)90020-f .
- Stokey, Nancy ; Lucas, Robert E .; Prescott, Edward (1989), Métodos recursivos en dinámica económica , Harvard Univ. Press, ISBN 978-0-674-75096-8.
Enlaces externos
- King, Ian, 2002 (1987), " Una introducción sencilla a la programación dinámica en modelos macroeconómicos ". Una introducción a la programación dinámica como una herramienta importante en la teoría económica.
- Programación dinámica
- Algoritmos y métodos de optimización
- Ecuaciones
- Ingeniería de sistemas
- Control óptimo