Programación Dinámica Héctor Navarro Introducción • Colección de herramientas matemáticas usadas para analizar procesos de toma de decisiones • Toma ventaja de la duplicación de subproblemas, almacenando soluciones para su uso posterior • Es una técnica bottom-up Introducción • En muchas ocasiones esto puede llevar a soluciones polinómicas aunque puede ser necesario enumerar todas las soluciones Principio de Optimalidad • Es un principio que debe cumplir todo problema para poder resolverse con P.D. • La solución óptima a un problema es una combinación de soluciones a algunos de sus subproblemas Método básico • Plantear una solución recurrente al problema • Convertir la solución a una solución en donde se combinan soluciones a instancias pequeñas del problema para obtener soluciones a instancias mayores del problema • Idear una forma para almacenar resultados previos Método básico • Otra forma es usar el método de memorización que consiste en resolver el problema con una estrategia top-down, pero almacenando en alguna estructura de datos los resultados a los subproblemas ya resueltos • Suele utilizar una gran cantidad de memoria Fibonacci • La forma recursiva de implementar Fibonacci es implementando la fórmula recurrente: 1 n 0 o n 1 fib (n) n 1 fib (n 1) fib (n 2) • El problema con esa implementación es que crece exponencialmente • Calcular fib(100) tardaría años si se implementa así Fibonacci fib(5) fib(4) fib(3) fib(3) fib(2) fib(1) fib(2) fib(1) fib(0) 15 llamadas fib(1) fib(2) fib(0) fib(1) fib(1) fib(0) Fibonacci fib(5) fib(4) fib(3) fib(2) fib(1) fib(2) fib(1) fib(0) 9 llamadas fib(3) Fibonacci • Implementación con PD int fib(int n) { if(n==0 || n==1) return 1; int f0 = 0, f1 = 1, r; for(int i=1;i<n;i++) { r = f1 + f0; // f1 += f0 f0 = f1; // f0 = f1 - f0 f1 = r; } return r; } Fibonacci • Implementación con memorización int memo[1000]; // calcula hasta fib(1000) int fib(int n) { if(n==0 || n==1) return 1; int & f = memo[n]; if(f==-1) { f = fib(n-1) + fib(n-2); } return f; } memset(memo, -1, sizeof(memo)); Combinaciones • Una manera de calcular Cn,m es mediante la fórmula: n n! m (n m)!m! • Para valores relativamente pequeños de n y m resulta imposible calcularlo de esta forma ya que n! no puede representarse Combinaciones • Una alternativa es usar la fórmula recurrente: 1 m0 n 1 nm m n 1 n 1 1 m n m 1 m Combinaciones • Esta fórmula dio origen al triángulo de Pascal para calcular los factores del binomio de Newton: n=0 1 n=1 1 n=2 1 n=3 1 n=4 1 n=5 n=6 1 1 2 3 4 5 6 1 3 6 10 15 1 1 4 10 20 1 5 15 1 6 1 Combinaciones C5,2 C4,2 C4,1 C3,2 C2,2 C3,1 C2,1 C1,1 C2,1 C1,0 C1,1 C3,1 C2,0 C1,0 C4,0 Combinaciones m 0 n=0 1 n=1 1 n=2 1 n=3 1 n=4 1 n=5 1 n=6 1 1 2 3 4 5 6 1 1 1 1 1 1 Combinaciones m 0 1 n=0 1 n=1 1 1 n=2 1 2 n=3 1 n=4 1 n=5 1 n=6 1 2 3 4 5 6 1 1 1 1 1 Combinaciones m 0 1 2 n=0 1 n=1 1 1 n=2 1 2 1 n=3 1 3 3 n=4 1 n=5 1 n=6 1 3 4 5 6 1 1 1 1 Combinaciones m 0 1 2 3 4 5 n=0 1 n=1 1 1 n=2 1 2 1 n=3 1 3 3 1 n=4 1 4 6 4 1 n=5 1 5 10 10 5 1 n=6 1 6 15 20 15 6 6 1 Combinaciones int comb(int n, int m) { for(int i = 0; i <= n; i++) { _comb[0] = _comb[i] = 1; // marca los casos base int ant = _comb[0]; for(int j = 1; j < i; j++) { int aux = _comb[j]; _comb[j] = ant + _comb[j]; ant = aux; } } return _comb[m]; } Dar vuelto • Ya vimos este problema, y vimos que para algunos conjuntos de monedas el problema puede resolverse con un algoritmo voraz • Otros conjuntos de monedas necesitan probar diferentes combinaciones, por ejemplo mediante un backtracking Dar vuelto • Una solución recursiva (usando Haskell) se plantea aquí: vuelto _ 0 = 0 vuelto (h:t) v = v < 0 : 999999 h <= v : min (1+vuelto(h:t)(v-h)) (vuelto t v) h > v : vuelto t v Dar vuelto • La línea: h <= v : min (1+vuelto(h:t)(v-h)) (vuelto t v) indica que si la moneda actual (h) es menor al vuelto que hay que dar, existen dos opciones: 1. Tomar la moneda: (1+vuelto(h:t)(v-h)) 2. No tomar la moneda: vuelto t v • Estas llamadas generan un comportamiento exponencial que hace imposible su cálculo para números muy grandes Dar vuelto • Para implementar la solución usando DP tendremos una matriz S[M][V+1] en donde V es el vuelto que deseamos dar y M el número de tipos de monedas • S[i][j] contiene el número óptimo de monedas para dar vuelto j considerando monedas desde la 0 hasta la i • Es obvio que S[i][0]= Dar vuelto • Por ejemplo, dar vuelto 11 con el conjunto de monedas: {2,3,5,7} Dar vuelto 0 2 3 5 7 1 2 3 4 5 6 7 8 9 10 11 Dar vuelto • La solución final se encuentra en la posición S[M-1][V] • La solución es de O(MxV) • ¿Cómo saber cuáles monedas debemos usar? KSP • Knapsack Problem (problema de la mochila) • Dado un conjunto de objetos con peso wi y valor vi; y una mochila con capacidad K, seleccionar cuáles transportar para maximizar el valor de los objetos transportado KSP • Existen diversas versiones: – Bounded: existe una cantidad limitada de cada tipo de objeto – Unbounded: existe una cantidad ilimitada de cada tipo de objeto – 0/1 existe un solo objeto de cada tipo KSP - Unbounded • Se puede pensar en ordenar los objetos según la relación valor/peso • Por ejemplo: Peso Valor Valor/peso 3 4 1.33 4 5 1.25 2 1 0.5 KSP - Unbounded Peso Valor Valor/peso 3 4 1.33 4 5 1.25 2 1 0.5 • Para K = 10: – Se transportan 3 objetos del primer tipo. Valor total: 12 – Sin embargo puede transportarse los objetos con valor 4+4+5 = 13 KSP - Unbounded • La heurística no funciona • Supongamos la función ksp(i,j) – retorna el valor máximo que se puede transportar si se toman en cuenta los i+1 primeros objetos, con una mochila de capacidad j • Para calcular ksp(i,j) existen siempre dos opciones: – No colocar más objetos de tipo i: ksp(i,j)=ksp(i-1,j) – Colocar un objeto de tipo i: ksp(i,j)=v[i]+ksp(i,j-w[i]) KSP - Unbounded • Finalmente: ksp(i,j)=max(v[i]+ksp(i,j-w[i]), ksp(i-1,j)) • ksp(i,0) = 0 • Para j>w(i): ksp(i,j) = ksp(i-1,j) KSP - Unbounded 0 3/4 0 4/5 0 2/1 0 1 2 3 4 5 6 7 8 9 10 Maxsum • Dada una secuencia de números enteros S1,…Sn, ¿cuál es la subsecuencia cuyos valores sumen un máximo valor? • Si los elementos Si son todos positivos la respuesta es trivial • Si existen números negativos la solución anterior no funciona • Existe una solución trivial de O(N2) Maxsum • Definimos M(j) como la suma máxima de todas las secuencias que terminan en j • Por ejemplo, si S = {3,1,-5,4,-1,-1,4,3,-6,2} • M(0) = 3 • M(1) = 1 • M(2) = -1 • M(3) = 4 Maxsum • En cada posición j existen dos opciones: – Agregar Sj a la secuencia M(j-1) – Comenzar una nueva secuencia que comienza en Sj • Si se agrega Sj a la secuencia M(j-1): M(j) = M(j-1) + Sj • Si se comienza una nueva secuencia: M(j) = Sj Maxsum • Como queremos la suma máxima: M(j) = max(M(j-1) + Sj, Sj) Ejemplo: i 0 1 2 3 4 5 6 7 8 9 Si 3 1 -5 4 -1 -1 4 3 -6 2 M(i) Maxsum • ¿Cómo aplicar el mismo principio a una matriz? Multiplicación encadenada de matrices • La multiplicación de dos matrices Anxm, Bmxp es una operación que involucra n x m x p multiplicaciones simples • Ahora si deseamos multiplicar varias matrices, por ejemplo A5x3 x B3x9 x C9x2, podemos hacerlo de varias formas: – (AxB)xC – Ax(BxC) Multiplicación encadenada de matrices • ¿De qué forma es más conveniente realizar el cálculo? • (A5x3 x B3x9 ) x C9x2: A x B: 135 multiplicaciones. Luego (A x B) x C: 90 multiplicaciones. En total: 225 multiplicaciones • A5x3 x (B3x9 x C9x2): B x C: 54 multiplicaciones. Ax(BxC): 30 multiplicaciones. Total: 84 multiplicaciones Multiplicación encadenada de matrices • Podemos enumerar las n matrices desde la 0 hasta la n-1. De esta forma, para multiplicar desde la matriz i hasta la j podemos hacer la multiplicación en j-i puntos distintos: Mi x Mi+1 x Mi+2 x … x Mj Multiplicación encadenada de matrices • Por ejemplo, para multiplicar desde i=2 hasta j=5: – M2 x (M3 x M4 x M5) – (M2 x M3)x (M4 x M5) – (M2 x M3 x M4 ) x M5 • Recursivamente podemos saber cómo realizar eficientemente cada una de las multiplicaciones resultantes Multiplicación encadenada de matrices • Definimos M(i,j) la función que retorna cómo calcular óptimamente desde la matriz i hasta la j, i<=j. • Es obvio que: M(i,j)=0 M(i,i+1)=filas(i) x cols(i) x cols(i+1) Multiplicación encadenada de matrices • Ahora, para calcular M(i,j) hay que probar todas las j-i opciones distintas, es decir: M (i, j ) min kj 1i {M (i, k ) K (k 1, j ) fil (i)cols (k )cols ( j )} Multiplicación encadenada de matrices • Por ejemplo: A2x5, B5x1, C1x3, D3x2, E2x4 0 (A) 1 (B) 2 (C) 3 (D) 4 (E) A B C D E 0 1 2 3 4 0 10 0 15 0 6 0 24 0 Longest Common Substring (LCS) • Dados un par de strings, encontrar la secuencia común a ellos más larga. Por ejemplo, si S=“xygxxzyfa” y T = “xyxzyfx”, LCS(S,T)=“xzyf” • Basta con encontrar el sufijo común mas largo para todos los pares de prefijos de los strings Longest Common Substring (LCS) • Basta con encontrar el sufijo común mas largo para todos los pares de prefijos de los strings • S=“xygxxzyfa” y T = “xyxzyfx” Prefijos: x xy xyg xygx xygxx xygxxz xygxxzy xygxxzyf xygxxzyfa Prefijos: x xy xyx xygxz xygxzy xygxzyf Xygxzyfx Longest Common Substring (LCS) • Basta con encontrar el sufijo común mas largo para todos los pares de prefijos de los strings • S=“xygxxzyfa” y T = “xyxzyfx” Prefijos: x xy xyg xygx xygxx xygxxz xygxxzy xygxxzyf xygxxzyfa Prefijos: x xy xyx xygxz xygxzy xygxzyf Xygxzyfx Longest Common Substring (LCS) • Si S tiene n caracteres y T tiene m caracteres, existen n sufijos para S y m para T • Existen en total n x m pares de prefijos posibles • Sea Si el prefijo de S de tamaño i, comenzaremos por comparar Si y Tj • Si Si=Tj, LCS(Si,Tj)=1+LCS(Si-1,Tj-1) • Si Si≠Tj, LCS(Si,Tj)=0 Longest Common Substring (LCS) • La implementación se lleva a cabo sobre una matriz de nxm, en donde LCS[i][j]=LCS(i,j) X X Y G X X Z Y F A Y X Z Y F X String Distance • Dados un par de strings A,B, la distancia entre ellos (Distancia de Levenshtein) se define como la menor cantidad de cambios que deben hacérsele a A, para convertirlo en B • Un cambio puede ser eliminar un carácter, agregar un carácter o modificarlo • Por ejemplo, A=“casa” puede convertirse en B=“azar”, haciendo tres cambios: eliminar ‘c’, agregar ‘r’, cambiar ‘s’ por ‘z’ String Distance • Supongamos que n es el tamaño de A, y m el tamaño de B • Si n=0: sd(A,B)= m (insertar m caracteres) • Si m=0: sd(A,B) = n (eliminar m caracteres) String Distance • Si A[i]==B[j]: – A: xxxxxxxC – B: yyyyyyyC • sd(Ai, Bj) = sd(Ai-1, Bi-1) ya que los caracteres son iguales String Distance • Si A[i]≠B[j]: – A: xxxxxxxC – B: yyyyyyyD • Opción 1: Cambiar C por D: • sd(Ai, Bj) = 1+sd(Ai-1, Bi-1) String Distance • Si A[i]≠B[j]: – A: xxxxxxxC – B: yyyyyyyD • Opción 2: Eliminar C: • sd(Ai, Bj) = 1+sd(Ai-1, Bi) String Distance • Si A[i]≠B[j]: – A: xxxxxxxC – B: yyyyyyyD • Opción 3: Insertar D en A: • sd(Ai, Bj) = 1+sd(Ai, Bi-1) String Distance • Finalmente, cuando A[i]≠B[j]: String Distance PALABRA A P A L A B R A L U ALUMBRA M B R A Longest Common Sequence • Dado un conjunto (generalmente de dos) secuencias, encontrar la subsecuencia más larga común a todas las secuencias • Por ejemplo: • S=5,4,6,7,3,2,7,9,2,6,4,2 • T=8,4,5,6,11,7,9,6,3,14,1,2 • LCS(S,T) = 4,6,7,9,6,2 Longest Common Sequence • Examinemos que sucede con el último elemento de la secuencia • Si el último elemento de Sn es igual al último elemento de Tm: • LCS(Sn, Tm) = 1 + LCS(Sn-1, Tm-1) Longest Common Sequence • Si el último elemento de Sn es distinto al último elemento de Tm: • S = XXXXXXA • T = YYYYYYB • Hay dos opciones: tratar de que A coincida con otro elemento de T, o que B coincida con otro elementos de S Longest Common Sequence • LCS(Sn, Tm) = max(LCS(Sn-1, Tm), LCS(Sn, Tm-1)) A = 4, 8, 2, 9, 1, 7 ? B = 6, 7, 8, 1, 2, 7, 5 Longest Common Sequence 8 5 4 6 7 3 2 7 9 2 6 4 2 4 5 6 11 7 9 6 3 14 1 2 Longest Increasing Sequence • Dada una secuencia de números enteros, la secunecia incremental más larga es una subsecuencia de números que están ordenados ascendentemente • La secuencia no es necesariamente contigua • Por ejemplo, para S = 0,8,4,12,2,10,6,14,1,9,5,13,3,11,7,15 • LIS(S) = 0,2,6,9,13,15 (no es única) Longest Increasing Sequence • Definimos qk(S) como la LIS de S que termina en Sk • Supongamos que estamos considerando la posición i de la secuencia • En cada iteración se debe tomar la decisión sobre incluir o no Si en la LIS • Para incluir Si en la LIS es necesario ver a cual de todas las secuencias anteriores podemos incluirlo Longest Increasing Sequence • Por ejemplo, para S = 4,11,2,9,5,6: • El 6 puede agregarse a la secuencia que termina en 4,2 o 5. Pero, ¿a cuál conviene más? • Obviamente a la que sea más larga entre estas Longest Increasing Sequence • qk(S) = maxi<k(qk-1(S)+1|Si<Sk) • 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 TSP • Agente viajero • Dado un grafo que representa ciudades conectadas, cuál es el circuito más corto que conecta a todas las ciudades TSP 1 2 2 4 3 3 0 2 2 3 4 5 3 4 TSP • Puede usarse una máscara de bits para marcar cuáles nodos están en el camino • Podemos suponer que el circuito siempre parte de algún nodo (por ejemplo el 0) • Luego debemos tomar en cuenta cuáles nodos están en el camino que se ha seleccionado • Para poder agregar un nuevo nodo al camino es necesario llevar el control del último nodo del camino actual TSP 1 2 2 4 TSP(111111, 0) es la solución 3 3 0 2 2 3 4 5 3 4 TSP(111111,0) = min{ TSP(111110,1)+arco(1,0), TSP(111110,2)+arco(2,0), TSP(111110,3)+arco(3,0)} TSP(111110,2) = min{ TSP(111010,1)+arco(1,2), TSP(111010,3)+arco(3,2), TSP(111010,5)+arco(5,2)} TSP 0 1 3 2 1 2 2 1 3 4 3 3 4
© Copyright 2024