Upload
maricruz-cid
View
226
Download
0
Embed Size (px)
Citation preview
¿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:}
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
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