La descarga está en progreso. Por favor, espere

La descarga está en progreso. Por favor, espere

3. Recursividad y Backtracking

Presentaciones similares


Presentación del tema: "3. Recursividad y Backtracking"— Transcripción de la presentación:

1 3. Recursividad y Backtracking

2 ¿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 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 );  } // 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);   }

3 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 TorresDeHanoi { 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.parseInt(args[0]), 1, 2, 3 );

4 Σ 2i = 2n - 1 Breve análisis Se puede demostrar por inducción
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 + 2T(n-1) En cada nivel se tienen 21 ejecuciones Σ 2i = 2n - 1 i = 0..n-1 Se puede demostrar por inducción 20 T(n) 21 T(n-1) T(n-1) 22 T(n-2) T(n-2) T(n-2) T(n-2) N veces 2n-1 T(1) T(1) T(1) T(1) T(1) . . .

5 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 2,3 1 3,2 2 1,3 2 3,1 3 2,1 3 1,2

6 El programa public class PermutaArreglo {
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); } 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; }  }

7 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 + nT(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(n-1) 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)

8 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

9 Ejemplo 1: el laberinto Se tiene una matriz de caracteres de dimensiones MxN 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. * * * * * * * * * *   *       *   * *   * * * & *       *   *   * * * *   *   *   * *   *           *

10 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.

11 Prgrama: version 1 public static salida1(char[][] x,i,j) {
if (x[i][j] == ‘&’) return true; if (salida1(x, i+1, j)) return true; if (salida1(x, i-1, j )) return true; if (salida1(x, i, j+1)) return true; if (salida1(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.

12 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 salida1( char[][] x, i, j) { if (x[i][j] == ‘&’) return true; if (x[i][j] == '*' || x[i][j] == '+') return false; x[i][j] = ‘+'; if (salida1(x, i+1, j)) return true; if (salida1(x, i-1, j)) return true; if (salida1(x, i, j+1)) return true; if (salida1(x, i, j-1)) return true; return false; }

13 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.nextLine(); if (x[i][j] == '*' || x[i][j] == ‘+') return null; x[i][j] = '+'; String camino = (salida2(x, i+1, j)); if (camino != null) return "("+i+","+j+")"+camino; camino = (salida2(x, i-1, j)); camino = (salida2(x, i, j+1)); camino = (salida2(x, i, j-1)); return null; }

14 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.nextLine(); if (x[i][j] == '*' || x[i][j] == ‘+') return -1; int mascorto = -1; x[i][j] = '+'; int camino = (salida3(x, i+1, j)); if (camino != -1 && camino < mascorto) mascorto = camino; camino = (salida3(x, i-1, j)); camino = (salida3(x, i, j+1)); camino = (salida3(x, i, j-1)); x[i][j] = ' '; if (mascorto == -1) return -1; return mascorto +1; }

15 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 mejorCont = -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 > mejorCont)             mejorCont = c;        }     return -mejorCont: }

16 Análisis: ¿ Cuanto se demora mi programa ?

17 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

18 Notación O Se dice que una función f (n) es O(g(n)) si existe una constante c > 0 y un n0 >= 0 tal que para todo n >= n0 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 n0 >= 0 tal que para todo n >= n0 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)).

19 Ejemplos Θ (n) 3n = O(n) 2 = O(1) 2 = O(n) 3n + 2 = O(n)
An2+ Bn + C = O(n2) Alog n + Bn + C nlog n + Dn2 = ? 3 = Ω(1) 3n = Ω(n) 3n = Ω(1) 3n + 2 = Ω(n) Θ (n)

20 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 = 2an-1 + 1 a0 = 0 Ejemplo: Fibonacci fn = fn-1 + fn-2 f0 = 0 f1 = 1

21 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 = a0 + Σck 1<=k<=n

22 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 = A0 + Σck 1<=k<=n y finalmente an = a0bn + Σckbn-k

23 Ejemplo: Torres de Hanoi
El número de movimientos de discos está dado por la ecuación an = 2an-1 + 1 a0 = 0 De acuerdo a lo anterior, la solución es an = Σ2n-k = Σ2k 1<=k<=n 0<=k<=n-1 Lo que significa an = 2n-1

24 Propuesto Generalizar este método para resolver ecuaciones de la forma
an = bnan-1 + cn donde bn y cn son funciones conocidas.

25 Ecuaciones Lineales con coef. Const.
Ejemplo: Fibonacci fn = fn-1 + fn-2 f0 = 0 f1 = 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

26 Ecuaciones Lineales con coef. Const.
La solución general se obtiene como una combinación lineal de estas soluciones: fn = A Ф1n + B Ф2n La condición inicial f0 = 0 implica que B = -A, esto es, fn = A(Ф1n - Ф2n) y la condición f1 = 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)) (Ф1n - Ф2n) Nótese que Ф2n tiende a 0 cuando n tiende a infinito, de modo que fn = Θ (n)

27 Teorema Maestro (div. para reinar)
Consideremos la ecuación de la forma T(n) = pT(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 ) = pT(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

28 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 )

29 Teorema Maestro: caso p < q
T(n) = pT(n/q) + Kn

30 Teorema Maestro: caso p = q
T(n) = pT(n/q) + Kn

31 Teorema Maestro: caso p > q
T(n) = pT(n/q) + Kn

32 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) = a0+a1*x an-1*xn-1B(x) = b0+b1*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).

33 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(n2). ¿Se puede hacer más rápido?

34 Dividir-componer Supongamos que n es par, y dividamos los polinomios en dos partes. Por ejemplo, si A(x) = 2 + 3*x - 6*x2 + x3 entonces se puede reescribir como A(x) = (2+3*x) + (-6+x)*x2 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

35 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(nlog2 4) = O(n2) lo cual no mejora al algoritmo visto inicialmente.

36 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(nlog2 3) = O(n1.59)

37 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) f0 = 0 f1 = 1 n   0  1  2  3  4  5  6  7  8  fn  0  1  1  2  3  5  Se puede demostrar que los números de Fibonacci crecen exponencialmente, como una función O(øn) donde ø=

38 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  T(n)  0  0  1  2  4  Ejercicio: Demostrar que T(n) = fn+1-1.

39 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.

40 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).

41 Es posible mas eficiencia aún
Tenemos : fn = fn-1+fn f0 = f1 = 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 f1 = 1 g1 = 0

42 Resolucion Tenemos : fn = fn-1+gn-1 gn = fn-1 f1 = 1 g1 = 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 f1 = [ 1 ]     [ 0 ] La solución de esta ecuación es   fn = An-1*f1  lo cual puede calcularse en tiempo O(log n) usando el método rápido de elevación a potencia visto anteriormente.

43 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.

44 Algoritmo General (PD)
Los pasos generales de programación dinámica : Encontrar la subestructura óptima del problema: encontrar los sub-problemas 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 sub-problemas más pequeños y tabulando las soluciones, lo que luego permite obtener la solución de sub-problemas 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)

45 Ejemplo: Multiplicación de secuencia de matrices
Sea una secuencia de n matrices A1 ... 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 * 10 * 100) + (100 * 100 * 10) = 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 * 100 * 10) + (100 * 10 * 10) = multiplicaciones (10*mas rápido).

46 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 sub-problemas que tienen la misma estructura. Ej., si la solución óptima implica (A1 * ... * Ak) * (Ak+1 * ... * An), el problema se reduce a encontrar la parentización óptima para A1...Ak y Ak+1...An. subestructura óptima: se puede dividir el problema en sub-problemas, y es posible encontrar las soluciones óptimas a los sub-problemas -> 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 sub-problemas deben ser las óptimas para poder alcanzar el óptimo global.

47 ¿k para (A1*...*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. ¿ Cuantas opciones hay ? Esta recursión da origen a los Números de Catalán que tienen solución Bien Malo !

48 Solución recursiva Una forma de resolverlo con recursión es la siguiente: 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 mínimo El costo mínimo para una particion k se calcula como el costo mínimo para calcular los sub-problemas 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 (que es p ?) Recordemos M[mxn]*N[nxp] = P[mxp] y requiere m*n*p multiplicaciones, M es el resultado de multiplicar Ai.. Ak-1 y N el resultado de multiplicar Ak+1 .. Aj 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

49 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 sub-problemas. Para ayudarnos a llevar un registro de lo que hemos calculado hasta ahora, frginamos s[i, j] como el valor que k debe tener para dividir el producto AiAi Aj de modo de obtener una parentización óptima. Esto es, s[i, j] vale k que hace que m[i, j] = m[i, k] + m[k + 1, j] + p(i-1)*p(k)*p(j) sean mínimos. Propuesto: Escriba la ecuación de recurrencia que corresponde al costo del algoritmo recursivo.

50 Encontrar k para (A1*...*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 sub-problemas se "traslapan" (overlapping problems). En total, se requiere realizar un número exponencial de llamados recursivos. Sin embargo, el número total de sub-problemas distintos es mucho menor que exponencial. Propuesto: Muestre que el número de sub-problemas distintos es O(n2). Hint: por cada partición se generan n-1 subproblemas, hay n particiones

51 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:

52 Codigo public static int multMatrix(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];

53 Gráficamente Las matrices m y s calculadas por el algoritmo para n = 6 y las dimensiones: A1: 30 X 35, A2: 35 X 15, A3: 15 X 5, A4: 5 X 10, A5: 10 X 20, A6: 20 X 25

54 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 a1,...,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.

55 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 asignacionActividadesAvaro(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;

56 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 asignacionActividadesAvaro 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..

57 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 a1. Dado que A es una solución óptima y las tareas están ordenadas, a1 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 - a1 es una solución óptima para el problema de asignación de tareas para el conjunto S' = {i en S: tinii >= tfin1}, es decir, S' contiene todas las actividades restantes en S que son mutuamente compatibles con a1

58 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


Descargar ppt "3. Recursividad y Backtracking"

Presentaciones similares


Anuncios Google