58
3. Recursividad y Backtracking

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

Embed Size (px)

Citation preview

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 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 ); } }

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 + 2T(n-1)• En cada nivel se tienen 21 ejecuciones

• Σ 2i = 2n - 1 i = 0..n-1

• Se puede demostrar por inducción

T(n)

T(n-1) T(n-1)

T(n-2) T(n-2) T(n-2) T(n-2)

T(1) T(1) T(1) T(1) T(1). . .

N veces

20

21

22

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

El programapublic 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); 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 + 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)

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 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.* * * * * * * * **   *       *   **   * * * &*       *   *   ** * *   *   *   **   *           **   *       *   ** * * * * * * * *

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 1public 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.

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;}

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)); if (camino != null) return "("+i+","+j+")"+camino; camino = (salida2(x, i, j+1)); if (camino != null) return "("+i+","+j+")"+camino; camino = (salida2(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.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)); if (camino != -1 && camino < mascorto) mascorto = camino; camino = (salida3(x, i, j+1)); if (camino != -1 && camino < mascorto) mascorto = camino; camino = (salida3(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 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:}

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

Ejemplos• 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)

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

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 formaan - an-1 = cn

• Sumando a ambos lados, queda una suma telescópica:

• an = a0 + Σ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 = A0 + Σck

1<=k<=n

• y finalmentean = a0bn + Σckbn-k

1<=k<=n

Ejemplo: Torres de Hanoi• El número de movimientos de discos está dado por la

ecuaciónan = 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 significaan = 2n-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

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

Ecuaciones Lineales con coef. Const.• La solución general se obtiene como una combinación lineal de

estas soluciones:fn = A Ф1

n + B Ф2n

• La condición inicial f0 = 0 implica que B = -A, esto es,

fn = A(Ф1n - Ф2

n)

• 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)) (Ф1

n - Ф2n)

• Nótese que Ф2n 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) = 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

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 < qT(n) = pT(n/q) + Kn

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

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

Dividir para ReinarEste 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).

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?

Dividir-componerSupongamos 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 comoA(x) = (2+3*x) + (-6+x)*x2

y en generalA(x) = A'(x) + A"(x) * xn/2

B(x) = B'(x) + B"(x) * xn/2

EntoncesC = (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(n2) 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(n1.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) f0 = 0 f1 = 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 dadopublic 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) = 0T(1) = 0T(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 f0 = 0 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

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.

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

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) = 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 * 100 * 10) + (100 * 10 * 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 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.

• 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 !

¿k para (A1*...*Ak)*(Ak+1* ...*An)) optimo?

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

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+1 . . . 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.

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

Encontrar k con Programación DinámicaConsideraciones

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

Codigopublic 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]; }

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 

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.

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 arreglosasignacionActividadesAvaro(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 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..

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

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