3 Recursividad y Backtracking Qu es y para
3. Recursividad y Backtracking
¿Qué es y para qué se usa? • Al programar en forma recursiva, buscamos dentro de un problema otro sub-problema que posea su misma estructura • Ejemplo: Calcular xn. // Version 1, estrategia: xn =x* xn-1 public static float elevar( float x, int n ) { if( n==0 ) return 1; else return x * elevar(x, n-1); } // Version 2, estrategia xn = xn/2 * xn/2 public static float elevar( float x, int n ) { if( n==0 ) return 1; else if( n es impar ) return x * elevar( x, n-1 ); else return elevar( x*x, n/2 ); }
Ejemplo 2: Las torres de Hanoi • Pasar las argollas desde la estaca 1 a la 3 • Restricciones: – Mover una argolla a la vez – Nunca puede quedar una argolla mas grande sobre una más pequeña public class Torres. De. Hanoi { static void Hanoi( int n, int a, int b, int c ) { if( n>0 ) { Hanoi( n-1, a, c, b ); System. out. println( a + " --> " + c ); Hanoi( n-1, b, a, c ); } } public static void main( String[] args ) { Hanoi( Integer. parse. Int(args[0]), 1, 2, 3 ); } }
Breve análisis • • • Cada invocación del método Hanoi genera a su vez dos llamadas recusrivas Cada llamada recursiva se hace “achicando” el problema en una argolla Cada ejecución toma tiempo constante T(n) = 1 + 2 T(n-1) En cada nivel se tienen 21 ejecuciones 20 T(n) 21 • Σ 2 i = 2 n - 1 22 N veces T(n-1) T(n-2) i = 0. . n-1 2 n-1 T(1) • Se puede demostrar por inducción T(1). . . T(1)
Ejemplo 3: Generar permutaciones • Se tiene un arreglo a[0]. . a[n-1] • Se quieren generar (e imprimir) todas las permutaciones posibles • Estrategia: intercambiar el primer elemento con el i-esimo y generar todas las permutaciones para los n-1 siguientes, i = 0. . n-1 • • Ej 1, 2, 3 1 3, 2 2 1, 3 2 3, 1 3 2, 1 3 1, 2
El programa public class Permuta. Arreglo { static void permutaciones( int[] x, int ini, int fin) { if( ini == fin ) { imprimir(x); return; } for (int i = ini; i<= fin; i++) { intercambiar(x, ini, i); permutaciones(x, ini+1, fin); intercambiar(x, ini, i); } } public static void imprimir(int[] x) { for(int i = 0; i < x. length; i++) System. out. print(x[i]+" "); System. out. println(); } public static void main( String[] args ) { int[] a = {1, 2, 3, 4, 5}; permutaciones( a, 0, 4 ); } public static void intercambiar(int[] x, int y, int z) { int aux = x[y]; x[y] = x[z]; x[z] = aux; } }
Breve análisis • • • Cada invocación del método permutaciones genera a su vez n-1 llamadas recursivas Cada llamada recursiva se hace “achicando” el problema en un elemento Cada ejecución toma orden n (por el for) T(n) = n + n. T(n-1) En cada nivel se tienen n(n-1)(n-2)…(n-i+1) ejecuciones, cada una efectúa k(n-i) instrucciones ( • En el último nivel tenemos la n ejecuciones cada una con un elemento – n(n-1)(n-2) – n! • Cota superior: si en todos los niveles colocamos n! y tenemos n niveles tendriamos aprox (n+1)! • El resultado está entre n! y (n+1)! (IGUAL MUCHO)
El backtraking • Solucionar un problema por prueba y error • Se basa en generar todas las posibles soluciones a un problema y probarlas • Por esto mismo, el tiempo requerido para solucionar el problema puede explotar • Ejemplos típicos: las n-reinas, el caballo, el laberinto
Ejemplo 1: el laberinto • Se tiene una matriz de caracteres de dimensiones Mx. N que representa un laberinto. • Carácter ‘*’ significa pared, no se puede pasar • Carácter ‘ ‘ implica se puede pasar. • Carácter ‘&’ indica salida del laberinto • public static boolean salida(char[][] x, int i, int j) retorna true si a desde la posición i, j se puede encontrar una salida. * * * * * * * * & * * * * * * * * * * * *
Algoritmo backtraking • Recursivamente esto se puede programar de la siguiente manera probando todos los caminos posibles: – si en la posición donde estoy (i, j) hay un ‘*’ no hay salida y retorno false. – si en la posición donde estoy (i, j) hay un ‘&‘ entonces estoy fuera y retorno true. – si estoy en posición (i, j) y hay un espacio, pruebo recursivamente si hay salida por alguna de las 4 vecinas (i+1, j), (i-1, j), (i, j+1), (i, j-1). – si alguna de las llamadas retorna true, yo retorno true (suponemos que no se puede mover en diagonal). Si todas retornan false, retorno false.
Prgrama: version 1 public static salida 1(char[][] x, i, j) { if (x[i][j] == ‘&’) return true; if (salida 1(x, i+1, j)) return true; if (salida 1(x, i-1, j )) return true; if (salida 1(x, i, j+1)) return true; if (salida 1(x, i, j-1, )) return true; return false; } • Esta solución tiene el problema que puede generar llamadas infinitas. Por ejemplo, si llamamos a salida(x, a, b, M, N) y esá vacía pero no es salida, esta llamará a salida(x, a+1, b, M, N). Si la celda (a+1, b) está vacía y no es salida, llamará a salida(x, a+1 -1, b, M, N), generandose así un ciclo infinito.
Programa version 2 • Para evitar esto podemos ir “marcando” (por ejemplo, con +) los lugares por donde hemos pasado para no pasar de nuevo por ahí: public static boolean salida 1( char[][] x, i, j) { if (x[i][j] == ‘&’) return true; if (x[i][j] == '*' || x[i][j] == '+') return false; x[i][j] = ‘+'; if (salida 1(x, i+1, j)) return true; if (salida 1(x, i-1, j)) return true; if (salida 1(x, i, j+1)) return true; if (salida 1(x, i, j-1)) return true; return false; }
Rescatando el camino • Podemos retornar un string que contenga la secuencia de (i, j) por donde hay que pasar para llegar a la salida. Para eso debemos modificar el encabezado public static String sailda(char[][] x, int i, int j) { if (x[i][j] == ‘&’) return "("+i+", "+j+")"; String l = s. next. Line(); if (x[i][j] == '*' || x[i][j] == ‘+') return null; x[i][j] = '+'; String camino = (salida 2(x, i+1, j)); if (camino != null) return "("+i+", "+j+")"+camino; camino = (salida 2(x, i-1, j)); if (camino != null) return "("+i+", "+j+")"+camino; camino = (salida 2(x, i, j+1)); if (camino != null) return "("+i+", "+j+")"+camino; camino = (salida 2(x, i, j-1)); if (camino != null) return "("+i+", "+j+")"+camino; return null; }
Camino mas corto • Queremos saber cuánto mide el camino (de existir) entre la celda i, j y la salida más próxima. Para esto tenemos que probar todas las posibilidades y nos quedamos con la mejor (más corta): public static int sailda(char[][] x, int i, int j) { if (x[i][j] == ‘&’) return 0; String l = s. next. Line(); if (x[i][j] == '*' || x[i][j] == ‘+') return -1; int mascorto = M*Z+1; //se supone Mx. Z dimensiones de matriz x[i][j] = '+'; int camino = (salida 3(x, i+1, j)); if (camino != -1 && camino < mascorto) mascorto = camino; camino = (salida 3(x, i-1, j)); if (camino != -1 && camino < mascorto) mascorto = camino; camino = (salida 3(x, i, j+1)); if (camino != -1 && camino < mascorto) mascorto = camino; camino = (salida 3(x, i, j-1)); if (camino != -1 && camino < mascorto) mascorto = camino; x[i][j] = ' '; if (mascorto == -1) return -1; return mascorto +1; }
Ejemplo: mejor jugada del gato • función que evalúa qué tan buena es una jugada en el gato. • suponiendo que tanto mi contrincante como yo vamos a seguir escogiendo la mejor jugada posible en cada etapa. • retorno 1 si gano con la jugada x, y, 0 si empato, -1 si pierdo int gato(char[][] t, int x, int y, char z) { t[x][y] = z; if (gano(t, z)) return 1; if (empate(t, x, y, z)) return 0; char contrincante = 'O'; if (z == 'O') contrincante = 'X'; int mejor. Cont = -1; for (int i = 0; i <= 2; i++) for (int j = 0; j <= 2; j++) if (t[i][j] == ' ') { int c = gato(t, i, j, contrincante); if (c > mejor. Cont) mejor. Cont = c; } return -mejor. Cont: }
Análisis: ¿ Cuanto se demora mi programa ?
Funciones Discretas • Para estudiar la eficiencia de los algoritmos, generalmente usamos funciones discretas, que miden cantidades tales tiempo de ejecución, memoria utilizada, etc. • Estas funciones son discretas porque dependen del tamaño del problema (n). Por ejemplo, n podría representar el número de elementos a ordenar. • Notación: f (n) o bien fn , representa al tiempo, por eso también se usa T(n) o Tn
Notación O • Se dice que una función f (n) es O(g(n)) si existe una constante c > 0 y un n 0 >= 0 tal que para todo n >= n 0 se tiene que f (n) <= cg(n). (cota superior de un algoritmo) • Se dice que una función f (n) es Ω(g(n)) si existe una constante c > 0 y un n 0 >= 0 tal que para todo n >= n 0 se tiene que f (n) >= cg(n). (cota inferior) • Se dice que una función f (n) es Θ (g(n)) si f (n) = O(g(n)) y f (n) = Ω(g(n)).
Ejemplos • • • 3 n = O(n) 2 = O(1) 2 = O(n) 3 n + 2 = O(n) An 2+ Bn + C = O(n 2) Alog n + Bn + C nlog n + Dn 2 = ? 3 = Ω(1) 3 n = Ω(n) 3 n = Ω(1) 3 n + 2 = Ω(n) Θ (n)
Ecuaciones de Recurrencia • Son ecuaciones en que el valor de la función para un n dado se obtiene en función de valores anteriores. • Esto permite calcular el valor de la función para cualquier n, a partir de condiciones de borde (o condiciones iniciales) • Ejemplo: Torres de Hanoi an = 2 an-1 + 1 a 0 = 0 • Ejemplo: Fibonacci fn = fn-1 + fn-2 f 0 = 0 f 1 = 1
Ecuaciones de Primer Orden • Consideremos una ecuación de la forma an = ban-1 + cn • donde b es una constante y cn es una función conocida. • Como precalentamiento, consideremos el caso b = 1: an = an-1 + cn • Esto se puede poner en la forma an - an-1 = cn • Sumando a ambos lados, queda una suma telescópica: • an = a 0 + Σck 1<=k<=n
Ecuaciones de Primer Orden: (cont. ) • Para resolver el caso general: an = ban-1 + cn • dividamos ambos lados por el “factor sumante” bn: an/bn = an-1/bn-1 +cn/bn • Si definimos An = an /bn, Cn = cn/bn, queda una ecuación que ya sabemos resolver: An = An-1 + Cn con solución An = A 0 + Σck 1<=k<=n • y finalmente an = a 0 bn + Σckbn-k 1<=k<=n
Ejemplo: Torres de Hanoi • El número de movimientos de discos está dado por la ecuación n n-k n = a 0 b + Σckb an = 2 an-1 + 1 a 1<=k<=n a 0 = 0 • De acuerdo a lo anterior, la solución es an = Σ 2 n-k = Σ 2 k 1<=k<=n • Lo que significa an = 2 n-1 0<=k<=n-1
Propuesto • Generalizar este método para resolver ecuaciones de la forma an = bnan-1 + cn • donde bn y cn son funciones conocidas.
Ecuaciones Lineales con coef. Const. • Ejemplo: Fibonacci fn = fn-1 + fn-2 f 0 = 0 f 1 = 1 • Este tipo de ecuaciones tienen soluciones exponenciales, de la forma fn = λn: fn = fn-1 + fn-2 λn = λn-1 + λn-2 • Dividiendo ambos lados por λn-2 obtenemos la ecuación característica λ 2 - λ - 1 = 0 • cuyas raíces son Ф 1= (1+ sqrt(5))/2 ≈ 1. 618 Ф 2= (1 - sqrt(5))/2 ≈ 0. 618
Ecuaciones Lineales con coef. Const. • La solución general se obtiene como una combinación lineal de estas soluciones: fn = A Ф 1 n + B Ф 2 n • La condición inicial f 0 = 0 implica que B = -A, esto es, fn = A(Ф 1 n - Ф 2 n) • y la condición f 1 = 1 implica que A(Ф 1 - Ф 2) = A sqrt(5) = 1 • con lo cual obtenemos finalmente la fórmula de los números de Fibonacci: fn =(1 /sqrt(5)) (Ф 1 n - Ф 2 n) • Nótese que Ф 2 n tiende a 0 cuando n tiende a infinito, de modo que fn = Θ (n)
Teorema Maestro (div. para reinar) • Consideremos la ecuación de la forma T(n) = p. T(n/q) + Kn ( Esto se ve muy seguido en los algoritmos “div. Para reinar”) • Supongamos que n es una potencia de q, digamos n = q k Entonces T(q k ) = p. T(q k -1 ) + Kq k • Y si definimos a k = T(q k ) tenemos la ecuación: a k = pa k -1 + Kq k • La cual tiene solución a k = a 0 p k + K Σqjpk-j (ver al principio) 1<=j<=n
Teorema Maestro (cont. ) • Como k = log q n, tenemos T(n) = T(1)p log q n + Kplog q n Σ(q/p)j 1<=j<=log q n • Y observamos que plog q n = (qlog q p) log q n =(qlog q n) log q p =(n) log q p • Por lo tanto: T(n) = (n) log q p (T(1) + K Σ(q/p)j ) 1<=j<=log q n
Teorema Maestro: caso p < q T(n) = p. T(n/q) + Kn
Teorema Maestro: caso p = q T(n) = p. T(n/q) + Kn
Teorema Maestro: caso p > q T(n) = p. T(n/q) + Kn
Dividir para Reinar Este es un método de diseño de algoritmos que se basa en subdividir el problema en sub-problemas, resolverlos recursivamente, y luego combinar las soluciones de los sub-problemas para construir la solución del problema original. Ejemplo: Multiplicación de Polinomios. Supongamos que tenemos dos polinomios con n coeficientes, o sea, de grado n-1: A(x) = a 0+a 1*x+. . . +an-1*xn-1 B(x) = b 0+b 1*x+. . . +bn-1*xn-1 representados por arreglos a[0], . . , a[n-1] y b[0], . . , b[n-1]. Queremos calcular los coeficientes del polinomio C(x) tal que C(x) = A(x)*B(x).
Solulción Un algoritmo simple para calcular esto es: // Multiplicación de polinomios for( k=0; k<=2*n-2; ++k ) c[k] = 0; for( i=0; i<n; ++i) for( j=0; j<n; ++j) c[i+j] += a[i]*b[j]; Evidentemente, este algoritmo requiere tiempo O(n 2). ¿Se puede hacer más rápido?
Dividir-componer Supongamos que n es par, y dividamos los polinomios en dos partes. Por ejemplo, si A(x) = 2 + 3*x - 6*x 2 + x 3 entonces se puede reescribir como A(x) = (2+3*x) + (-6+x)*x 2 y en general A(x) = A'(x) + A"(x) * xn/2 B(x) = B'(x) + B"(x) * xn/2 Entonces C = (A' + A"*xn/2) * (B' + B"*xn/2) = A'*B' + (A'*B" + A"*B') * xn/2 + A"*B" * xn
Dividir-componer (cont. ) C = (A' + A"*xn/2) * (B' + B"*xn/2) = A'*B' + (A'*B" + A"*B') * xn/2 + A"*B" * xn Esto se puede implementar con 4 multiplicaciones recursivas, cada una involucrando polinomios de la mitad del tamaño que el polinomio original. T(n) = 4*T(n/2) + K*n donde K es alguna constante cuyo valor exacto no es importante. Por lo tanto la solución del problema planteado (p=4, q=2) es T(n) = O(nlog 2 4) = O(n 2) lo cual no mejora al algoritmo visto inicialmente.
Dividir-componer (cont. ) Pero. . . hay una forma más eficiente de calcular C(x). Si renombramos : D = (A'+A") * (B'+B") E = A'*B‘ F = A"*B" entonces C = E + (D-E-F)*xn/2 + F*xn Lo cual utiliza sólo 3 multiplicaciones recursivas, en lugar de 4. Esto implica que T(n) = O(nlog 2 3) = O(n 1. 59)
Tabulación • La recursividad puede ser muy ineficiente a veces • Ejemplo: Números de Fibonacci. • se definen mediante la recurrencia fn = fn-1+fn-2 (n>=2) f 0 = 0 f 1 = 1 n 0 1 2 3 4 5 6 7 8 9 10 11. . . fn 0 1 1 2 3 5 8 13 21 34 55 89. . . Se puede demostrar que los números de Fibonacci crecen exponencialmente, como una función O(øn) donde ø=1. 618. .
Problema: calcular fn para un n dado public static int F( int n ) { if( n<= 1) return n; else return F(n-1)+F(n-2); } Este método resulta muy ineficiente, si llamamos T(n) al número de operaciones de suma ejecutadas para calcular fn, tenemos que T(0) = 0 T(1) = 0 T(n) = 1 + T(n-1) + T(n-2) n 0 1 2 3 4 5 6 7 8 9 10. . . T(n) 0 0 1 2 4 7 12 20 33 54 88. . . Ejercicio: Demostrar que T(n) = fn+1 -1.
Método eficiente O(n) • Error: se calcula varias veces un mismo valor • Solución: usar un arreglo auxiliar para ir guardando los valores ya calculados • Algoritmo general (Programación Dinámica) – inicializar elementos de fib con algún valor "nulo". – Al llamar a F(n), primero se consulta el valor de fib[n]. • Si éste no es "nulo", se retorna el valor almacenado en el arreglo. • En caso contrario, se hace el cálculo recursivo y luego se anota en fib[n] el resultado, antes de retornarlo. • De esta manera, se asegura que cada valor será calculado recursivamente sólo una vez.
Programa O(n) • En casos particulares, es posible organizar el cálculo de los valores de modo de poder ir llenando el arreglo en un orden tal que, al llegar a fib[n], ya está garantizado que los valores que se necesitan (fib[n-1] y fib[n-2]) ya hayan sido llenados previamente. • En este caso, esto es muy sencillo, y se logra simplemente llenando el arreglo en orden ascendente de subíndices: fib[0] = 0; fib[1] = 1; for( j=2; j<=n; ++j ) fib[j] = fib[j-1]+fib[j-2]; • El tiempo total que esto demora es O(n).
Es posible mas eficiencia aún • Tenemos : fn = fn-1+fn-2 f 0 = 0 f 1 = 1 • Esta es una ecuación de recurrencia de segundo orden, porque fn depende de los dos valores inmediatamente anteriores. • Definamos una función auxiliar gn = fn-1 • Con esto, podemos re-escribir la ecuación para fn como un sistema de dos ecuaciones de primer orden: • fn = fn-1+gn-1 gn = fn-1 f 1 = 1 g 1 = 0
Resolucion • Tenemos : fn = fn-1+gn-1 gn = fn-1 f 1 = 1 g 1 = 0 • Lo anterior se puede escribir como la ecuación vectorial fn = A*fn-1 donde fn = [ fn ] A = [ 1 1 ] [ gn ] [ 1 0 ] • con la condición inicial f 1 = [ 1 ] [ 0 ] • La solución de esta ecuación es fn = An-1*f 1 • lo cual puede calcularse en tiempo O(log n) usando el método rápido de elevación a potencia visto anteriormente.
Programación Dinámica (PD) • Similar a dividir para reinar, (dividir, solucionar sub, componer) • Diferencia: se usa programación dinámica cuando subproblemas se repiten, ( ej. Números de Fibonacci) • En este caso, en vez de usar recursión para obtener las soluciones a los subproblemas éstas se van tabulando en forma bottom-up, y luego estos resultados son utilizados para resolver subproblemas más grandes. • PD se usa en general para resolver problemas de optimización (maximización o minimización de alguna función objetivo). • Estos problemas pueden tener una o varias soluciones óptimas, y el objetivo es encontrar alguna de ellas.
Algoritmo General (PD) • Los pasos generales de programación dinámica : – Encontrar la subestructura óptima del problema: encontrar los subproblemas que componen el problema original, tal que si uno encuentra sus soluciones óptimas entonces es posible obtener la solución óptima al problema original. – Definir el valor de la solución óptima en forma recursiva. – Calcular el valor de la solución partiendo primero por los subproblemas más pequeños y tabulando las soluciones, lo que luego permite obtener la solución de subproblemas más grandes. – Terminar cuando se tiene la solución al problema original. • También es posible ir guardando información extra en cada paso del algoritmo, que luego permita reconstruir el camino realizado para hallar la solución óptima (por ejemplo, para obtener la instancia específica de la solución óptima, y no sólo el valor óptimo de la función objetivo)
Ejemplo: Multiplicación de secuencia de matrices • Sea una secuencia de n matrices A 1. . . An. Se desea obtener el producto de ellas. • Se debe cumplir que dos matrices consecutivas en la secuencia se pueden multiplicar. (n°de cols. de Ai igual al n° de filas de Ai+1. • El producto se puede obtener multiplicando las matrices en orden de izquierda a derecha. Ej. (A * B) * C. • Ineficiencia: si A es de 100 x 10, B es de 10 x 100, y C es de 100 x 10, (A * B) * C implica calcular (100 * 100) + (100 * 10) = 200. 000 multiplicaciones (multiplicar dos matrices de p x q y q x r implica calcular p*q*r multiplicaciones escalares). • Como la multiplicación de matrices es asociativa, también se puede hacer A*(B*C), lo cual tiene un costo de (10 * 10) + (100 * 10) = 20. 000 multiplicaciones (10*mas rápido).
Problema • Dada la secuencia de n matrices, encontrar la parentización óptima que minimice el número de multiplicaciones escalares realizadas para obtener el producto de la secuencia de matrices. • Solución utilizando recursión – dividir en subproblemas que tienen la misma estructura. Ej. , si la solución óptima implica (A 1 *. . . * Ak) * (Ak+1 *. . . * An), el problema se reduce a encontrar la parentización óptima para A 1. . . Ak y Ak+1. . . An. – subestructura óptima: se puede dividir el problema en subproblemas, y es posible encontrar las soluciones óptimas a los subproblemas -> se puede encontrar la solución óptima al problema original. • Propuesto: Demuestre por contradicción que, en el problema de la multiplicación de una cadena de matrices, necesariamente las soluciones a los subproblemas deben ser las óptimas para poder alcanzar el óptimo global.
¿k para (A 1*. . . *Ak)*(Ak+1*. . . *An)) optimo? • Solución 1 (fuerza bruta): Se prueban todas las opciones (k = 1, 2, . . . , n-1), y el algoritmo retorna aquel k que minimice el número de multiplicaciones. • Esta recursion da origen a los Numeros de Catalán que tienen solución • Bien Malo !
Solución recursiva • Una forma de resolverlo con recursion es la siguinte: para el intervalo Ai. . . Aj, se prueba con k = i, i+1, i+2, . . . , j-1 y se ve cual es el que da el minimo • Le costo minimo para calcular los subproblemas Ai. . k y A k+1. . j mas el costo de multiplicar estas dos matrices entre si, lo que requiere p i− 1 * p k * pj multiplicaciones • Si m[i, j] representa el costo mínimo en multiplicaciones necesarias para multiplicar la cadena Ai. . . Aj, encontrar el óptimo es resolver: – m[i, j] = 0 si i == j – min {m[i, k] + m[k+i, j] + p(i-1)*p(k)*p(j)} si i < j, para i <= k < j
Solución recursiva – m[i, j] = 0 si i == j – min {m[i, k] + m[k+i, j] + p(i-1)*p(k)*p(j)} si i < j, para i <= k < j • Los valores en m[i, j] muestran los costos de la solución optima para subproblemas. • To help us keep track of how to construct an optimal solution, let us define s[i, j] to be a value of k at which we can split the product Ai. Ai+1. . . Aj to obtain an optimal parentization. Esto es, s[i, j] vale k que hace que m[i, j] = m[i, k] + m[k + 1, j] + pi − 1 pkpj sean mínimos. • Propuesto: Escriba la ecuación de recurrencia que corresponde al costo del algoritmo recursivo.
Encontrar k para (A 1*. . . *Ak)*(Ak+1*. . . *An)) optimo • La solución a esta ecuación de recurrencia es exponencial, de hecho no es mejor que el costo de la solución por fuerza bruta. • ¿Dónde radica la ineficiencia de la solución recursiva? Al igual que en Fibonacci, el problema es que muchos de los llamados recursivos se repiten, es decir, los subproblemas se "traslapan" (overlapping problems). • En total, se requiere realizar un número exponencial de llamados recursivos. Sin embargo, el número total de subproblemas distintos es mucho menor que exponencial. • Propuesto: Muestre que el número de subproblemas distintos es O(n 2). • Hint: por cada partición se generan n-1 subproblemas, hay n particiones
Encontrar k con Programación Dinámica Consideraciones • El hecho que el número de subproblemas distintos es cuadrático (y no exponencial), es una indicación que el problema puede ser resuelto en forma eficiente. • En vez de resolver los subproblemas en forma recursiva, se utilizará la estrategia de la programación dinámica, • Se tabularán los resultados de los subproblemas, partiendo desde los subproblemas más pequeños, y haciendo los cálculos en forma bottom-up. • La siguiente página muestra el seudocódigo muestra cómo se puede implementar el algoritmo que utiliza programación dinámica:
Codigo public static int mult. Matrix(int[] p, int[][] m, int[][] s) { // Matriz Ai con dimensiones p[i-1] x p[i], i = 1. . n // Primer indice para p = 0, primer indice para m = s = 1 int n = p. length - 1; for (int i = 1; i <= n; i++) m[i][i] = 0; for (int l = 2; l <= n; l++) { for (int i = 1; i <= n - l + 1; i++) { int j = i + l - 1; m[i][j] = Integer. MAX_VALUE; for (int k = i; k <= j-1; k++) { int q = m[i][k] + m[k+1][j] + p[i-1]*p[k]*p[j]; if (q < m[i][j]) { m[i][j] = q; s[i][j] = k; } } } } return m[1][n]; }
Gráficamente Las matrices m y s calculadas por el algoritmo para n = 6 y las dimensiones: A 1: 30 X 35, A 2: 35 X 15, A 3: 15 X 5, A 4: 5 X 10, A 5: 10 X 20, A 6: 20 X 25
Algoritmos Avaros • En problemas de optimización, un algoritmo avaro siempre elige la opción que parece ser la mejor en el momento que la toma. Si bien esto no resuelve todo problema de optimización (puede caer en un óptimo local), hay problemas para los cuales una estrategia avara encuentra siempre el óptimo en forma eficiente. • Ejemplo: Asignación de actividades: • Sea A un conjunto de n actividades a 1, . . . , an que comparten algún recurso importante (y escaso). Cada actividad ai tiene un tiempo de inicio tinii y un tiempo de término tfini, definido por el intervalo semi-abierto [tinii, tfini). Se dice que dos actividades distintas ai y aj son mutuamente compatibles si sus intervalos de tiempo [tinii, tfini) y [tinij, tfinj) no se traslapan. En caso contrario, sólo una de ellas puede llevarse acabo ya que no es posible que dos actividades compartan simultáneamente el recurso escaso. El problema de asignación de actividades consiste en encontrar un subconjunto maximal A de S que sólo contenga actividades mutuamente compatibles.
Problema • Para resolver el problema se utilizará una estrategia avara. • suponer que las actividades están ordenadas temporalmente en forma ascendente de acuerdo al tiempo de término (tfin) • Los tiempos de inicio y término de cada actividad se almacenan en arreglos asignacion. Actividades. Avaro(int[] tini, int[] tfin) { // Los indices van de 1. . n int n = tini. length; A = {1} // primera actividad siempre es parte de la respuesta int j = 1; for (int i = 2; i <= n; i++) { if tini[i] >= tfin[j] { A = A U {i}; // union de conjuntos j = i; } } return A; }
Demostracion • La estrategia del algoritmo propuesto es avara, ya que cada vez que es posible se agrega una actividad que es mutuamente compatible con las que ya están en el conjunto A. Este algoritmo toma tiempo O(n) en realizar la asignación de actividades. Falta demostrar que la asignación de actividades realizada por el algoritmo avaro es maximal. • Teorema: el algoritmo implementado en la función asignacion. Actividades. Avaro produce un conjunto maximal de actividades mutuamente compatibles. • Demostración: Sea A una solución optima para el problema, y suponga que las actividades en A están ordenadas por tiempo de término de cada actividad. .
Demostracion • Supongamos que la primera actividad en A tiene índice k. Si k = 1, entonces A comienza con una decisión avara. • Si k > 1, se define B = A - ak U a 1. Dado que A es una solución óptima y las tareas están ordenadas, a 1 tiene que ser mutuamente compatible con la segunda actividad en A, por lo que B también es una solución óptima. • Es decir, toda solución óptima contiene a la actividad cuyo tiempo de término es el menor de todos, en otras palabras, toda solución óptima comienza con una decisión avara. • Por último, la solución A' = A - a 1 es una solución óptima para el problema de asignación de tareas para el conjunto S' = {i en S: tinii >= tfin 1}, es decir, S' contiene todas las actividades restantes en S que son mutuamente compatibles con a 1
Demostracion • propuesto: demuestre por contradicción que para el conjunto S' no existe una solución óptima B' con más actividades que A'. • Esto muestra que el problema de asignación de actividades tiene subestructura óptima: – se define un problema más pequeño (S') con la misma estructura que el problema original, cuya solución óptima es parte de la solución al problema original. – Por inducción en el número de decisiones tomadas, el tomar la decisión avara en cada subproblema permite encontrar la solución óptima al problema original
- Slides: 58