. 1 UNIDAD 2: TEMA: ESTRATEGIAS DE DISEÑO DE ALGORITMOS (APUNTES DE APOYO A CLASES TEÓRICAS) (TIEMPO DE EXPOSICIÓN: 2 HS) Bibliografía: 2 1. Pareja Flores, Ojeda Aciego, Algoritmos y programación en Pascal. Versión pdf. ó ISBN 8478972900, 9788478972906 Editorial Ra-ma 2. es.wikipedia.org/ 3. Brassard, Gilles; Bratley, Paul (1997). «Exploración de grafos». Fundamentos de Algoritmia. Madrid: PRENTICE HALL. ISBN: 84-89660-00-X. 4. http://www.lcc.uma.es/~av/Libro/CAP6.pdf: Técnicas de diseño de algoritmos Objetivos: 3 1. Mostrar algunas estrategias de diseño de algoritmos, con el objeto de mejorar nuestros propios algoritmos. Ellas son: 1) Gready :Voraz ó golosa ó algoritmos ávidos. 2) Divide y vencerás 3) De programación Dinámica 4) De vuelta atrás ó con retroceso ó backtracking 5) otras 2. Mostrar el esquema general y ejemplos de aplicación de cada estrategias de diseño de algoritmos. Estrategia: Gready (Voráz) 4 Idea: La estrategia de estos algoritmos es básicamente iterativa Consiste en una serie de etapas. En cada etapa se consume una parte de los datos y se construye una parte de la solución, parando cuando se hayan consumido todos los datos. Cada parte consumida se evalúa una única vez, siendo descartada o seleccionada, de tal forma que si es seleccionada forma parte de la solución, y si es descartada, no forma parte de la solución ni volverá a ser considerada para la misma El nombre (Voraz-ávido-goloso) de esta estrategia es muy descriptivo: • En cada etapa se consume (bocado) una parte de los datos. • Se intentará que la parte consumida sea lo mayor posible, bajo ciertas condiciones. Estrategia: Gready (Voráz)(Ejemplo I) Descomposición en factores primos 5 La descomposición de un número natural n en factores primos : 600 = 23 . 31 . 52 El proceso consiste en eliminar en cada etapa un divisor de n cuantas veces sea posible: • en la primera etapa se elimina el 2, y se considera el correspondiente cociente, • en la segunda etapa se elimina el 3, y • así siguiendo hasta que el cociente sea 1, como lo hicimos en la clase teórica de Teorema Fundamental de la Aritmética (TFA) ordPar( p, a) = Sea (c, r) = cr10(a, p) Si r 0 luego (a, 0) Sino sea (m, q) = ordPar(p, c) (m, q+1) factorizar(a) = factorizar1 (a, 2, ) donde factorizar1(a, p, L) = Si (a = 1) luego L Sino Sea (m, q) = ordPar(p, a) factorizar1(m, proximoPrimo(p), si (q = 0) L sino L U {(p, q)} ) Estrategia: Gready (Voráz) Esquema general de un algoritmo voráz 6 (versión iterativa) Resolver P (D: datos; var S: solucion); Inicio Generar la parte inicial de la solución S (y las condiciones iniciales) Mientras (D ) {todavía quedan datos por procesar } Extraer de D el máximo trozo posible T { D:= D – T } Procesar T Incorporar el procesado de T a la solucion S: { S := S T } Fin_mientras Fin Estrategia: Gready (Voráz) Esquema general de un algoritmo voráz 7 (versión recursiva) Resolver P (D: datos; var S: solucion); ResolverP1 (Parte inicial de la solución S y las condiciones iniciales) Donde ResolverP1 (D:datos; var S: solución); Inicio Si (D = ) luego S sino Extraer de D el máximo trozo posible T Procesar T ResolverP1 (D-T, S T) Fin_si Fin Estrategia: Gready (Voráz) (Ejemplo II) Cambio con el menor nro. de billetes 8 "Se pide crear un algoritmo que permita a una máquina expendedora devolver el cambio mediante el menor número de billetes posible, considerando que el número de billetes es limitado, es decir, se tiene un número concreto de billetes de cada tipo". Supongamos que se tiene la siguiente disponibilidad de billetes y monedas: La estrategia a seguir consiste en seleccionar los billetes de mayor valor que no superen la cantidad de cambio a devolver, en cada etapa. Estrategia: Gready (Voráz) (Ejemplo II) Cambio con el menor nro. de billetes (Trabajo Práctico: codificación del programa que resuelve el problema) Hay que devolver la cantidad 110 pesos: 1) Se tomaría 1 billete de 50, quedando una cantidad de cambio a devolver de 60 pesos. 2) Como 50 (el billete) es aún menor que 60 pesos, se tomaría otro billete de 50, quedando ahora una cantidad de cambio a devolver de 10 pesos. 3) Como 50 y 20 (la denominación de los billetes) son mayores que 10 se desechan, tomando 1 billete de 5 pesos. 4) La cantidad de cambio a devolver ahora es 5 pesos. 5) Se tomaría otro billete de 5, pero ya no nos queda ninguno, entonces deberán devolverse 5 monedas de 1 peso, terminando así el problema de forma correcta. Luego 110$ = 2 billetes de 50$ + 1 billete de 5$ + 5 monedas de 1$ Estrategia: Gready (Voráz)(Ejemplo III) Arbol cubridor mínimo: algoritmo de Prim 10 El algoritmo incrementa continuamente el tamaño de un “árbol”, comenzando por un nodo inicial (cualquiera) , al que se le van agregando sucesivos nodos. Vamos guardando en un conjunto C (Visitados) los nodos conexos con un nodo inicial, y en un conjunto B las aristas que permiten mantener la conexión de todos los nodos en C. En cada iteración las aristas a considerar son aquellas que inciden en los nodos que ya pertenecen al árbol, pero: aristas ‘a’ / c(a) es mín donde a = {p, q} con p C y q C. Finaliza cuando C = N (se haya visitado todos los nodos) 1 1 1 5 1 1 5 1 4 1 1 3 1 5 1 4 1 2 1 2 3 1 5 1 4 Estrategia: Gready (Voráz) (Ejemplo III) Arbol cubridor mínimo: algoritmo de Prim 11 Algoritmo: arbolCubMinimo(G) = cub1({p}, { }) //p es cualquier nodo // donde cub1(C, B) = Si (C = N) luego B Sino cub1 (C U {q}, B U {a}) Donde ‘a’ es una arista de G / P(a) = {p,q} con p C y q C y c(a) lo más pequeño posible Estrategia: Divide y Vencerás 12 Idea: Básicamente esta estrategia consiste en: Dado un problema P, con datos D: • Si los datos permiten una solución directa para D, se ofrece esta solución; • Sino se siguen las siguientes fases: (a) Se dividen los datos D en varios conjuntos de datos más pequeños Di . (b) Se resuelven los problemas P(Di) parciales, sobre los conjuntos de datos Di , recursivamente (c) Se combinan las soluciones parciales, resultando así la solución final. Estrategia: Divide y Vencerás Ejemplo I: Quicksort 13 Quicksort (Hoare en 1960 Complejidad O(n) = n log2(n)) Dado un vector D de elementos desordenados: - elegimos un elemento llamado pivote (puede ser el primero de D) - organizamos el resto de los elementos de manera tal que a la: • izquierda del pivote: se encuentren todos los elementos que son menores que él (sub-vector1 ó D1) • derecha del pivote: se encuentren todos los que sean mayores o iguales a él, (sub-vector2 ó D2) - Aplicamos este proceso a los dos sub-vectores , componiendo la ordenación de cada sub-vector con el pivote: Quicksort(D1) ° Pivote ° Quicksort(D2) Ejemplo en el pizarrón D = 20 5 22 13 33 10 7 Estrategia: Divide y Vencerás Ejemplo I: Quicksort (Trabajo Práctico: codificación del programa que resuelve el problema) 14 QuickSort: Algoritmo Si longitud(D) = 0 luego ( vacío) si no Pivote := Primero(D) dividir D en dos partes: D1 y D2 donde todos los elementos de D1 son menores que Primero(D) y todos los elementos de D2 son mayores o iguales que Primero(D) S1 := Ordenar D1 usando QuickSort S2 := Ordenar D2 usando QuickSort Devolver: S1 ° Pivote ° S2 En Haskell: ordenar D = quicksort (D) quicksort (D) = if vacia(D) then [] else quicksort(D1) ++ [primero(V)] ++ quicksort (D2) where D1 = [x | x <- resto(D) , x < primero(D)] D2= [x | x <- resto(D) , x >= primero(D)] Estrategia: Divide y Vencerás Esquema general: 15 Resolver P (D: datos; var S: solucion); Inicio Si los datos D admiten un tratamiento directo luego S sino Dividir D en varios subconjuntos de datos: D1, …, Dk Resolver P(D1,S1); … Resolver P(Dk, Sk); S:= Combinar(S1, …,Sk.) Fin_si Fin Estrategia: Divide y Vencerás Ejemplo II: Búsqueda binaria 16 Búsqueda Binaria. Complejidad (O(n) = log2(n)) Comparar x con el elemento central: Si x = elemento central ya hemos terminado, Sino buscamos en la mitad del array que nos interese: Si x < elemento central, buscamos en la primera mitad del array Sino buscamos en la segunda mitad del array. Repetimos este proceso comparando x con el elemento central del subarray seleccionado, y así sucesivamente hasta que: ó bien encontremos el valor x ó bien podamos concluir que x no está (porque el subarray de búsqueda está vacío). Estrategia: Divide y Vencerás Ejemplo II: Búsqueda binaria 17 Buscar (x:Tipo_elemento; L:Lista) : Posicion; {Devuelve la posición donde se encuentra x en L, caso contrario devuelve 0 } Buscar := buscar2 (x, L, 1, L.ult); Donde: buscar2 (x:Tipo_Elemento; L: Lista; i, j:Posición ): Posicion; Var K:Posicion; Inicio Si (i ≤ j) luego k := cociente (i + j, 2); Si (L.Elementos[k] = x) luego buscar2:=k sino Si (x < L.Elementos[k]) luego buscar2:=buscar2 (x, L, i, k – 1) Sino buscar2 := buscar2 (x, L, k + 1, j) sino 0 Fin Estrategia: Programación dinámica Esquema general: 18 La programación dinámica es un método para reducir el tiempo de ejecución de un algoritmo mediante la utilización de subproblemas superpuestos y subestructuras óptimas. Un problema tiene subproblemas superpuestos cuando se utiliza a un mismo subproblema varias veces para resolver diferentes problemas mayores. Una subestructura óptima significa que se pueden usar soluciones óptimas de subproblemas para encontrar la solución óptima del problema en su conjunto. En general, se pueden resolver problemas con esta técnica siguiendo estos tres pasos: 1. Dividir el problema en subproblemas más pequeños. 2. Resolver estos problemas de manera óptima usando este mismo proceso de tres pasos recursivamente. 3. Usar estas soluciones óptimas para construir una solución óptima al problema original. Estrategia: Programación dinámica Ejemplo I: Sucesión de Fibonacci 19 Fn = 0 si n = 0 1 si n = 1 Fn-1 + Fn -2 si n > 1 Para n=3, Para n=4, Para n=5, Para n=6, F 3 = F2 + F1 F 4 = F3 + F 2 F5 = F4 + F3 F 6 = F 5 + F4 Supone calcular F2 más de dos veces Estrategia: Programación dinámica Ejemplo I: Sucesión de Fibonacci 20 Fibonacci (n: NATURAL): NATURAL; INICIO Si n = 0 luego 0 Sino Si n = 1 luego 1 Sino Fibonacci := Fibonacci(n-1)+ Fibonacci (n-2); Fin_Si FIN Complejidad exponencial C(n) = n donde = 1+ 5 2 Al calcular F6, se calculó F2 más de dos veces: “Esto sucede siempre que haya subproblemas superpuestos: desperdiciando tiempo recalculando las soluciones óptimas a problemas que ya han sido resueltos anteriormente.” Estrategia: Programación dinámica Ejemplo I: Sucesión de Fibonacci 21 Esto se puede evitar: memorizando las soluciones que ya hemos calculado. Entonces, si necesitamos resolver el mismo problema más tarde, podemos obtener la solución de la lista de soluciones calculadas y reutilizarla. Fibonacci (n: NATURAL): NATURAL; VAR Fib: ARRAY [0..n] of NATURAL; i: NATURAL; INICIO SI n = 0 luego 0 SINO SI n = 1 luego 1 SINO Fib[0] := 0; Fib[1] := 1; PARA i = 2 HASTA n HACER Fib[i] := Fib[i-1] + Fib[i-2] FINPARA Fibonacci:= Fib[n] FINSI FIN Complejidad lineal C(n) = n Estrategia: Programación dinámica Ejemplo I: Sucesión de Fibonacci 22 Un mejoramiento del algoritmo anterior consiste en quedarnos solo con los dos últimos valores, (no con un vector de valores) calculados en cada iteración: FUNCTION Fibonacci (n: NATURAL): NATURAL; INICIO SI n = 0 luego 0 SINO SI n = 1 luego 1 SINO Fib1 (0, 1 ,n-2) FIN Donde: Fib1 (x, y ,n) = Si n = 0 luego (x + y) SINO Fib1( y, x + y, n-1) FIN Complejidad lineal C(n) = n Trabajo Práctico: Programar todas las versiones del problema Estrategia: Programación dinámica Ejemplo II: El problema de la mochila 23 Resolver este problema seleccionando los elementos más caros en primer lugar, o los más livianos, no necesariamente garantiza llegar a una solución ideal. Es posible que otras combinaciones de elementos produzcan mejores resultados! Estrategia: Programación dinámica Ejemplo II: El problema de la mochila 24 Sea w = capacidad de la mochila Sea wi = la capacidad de cada caja Sea vi = el valor de la caja i Sea Z = función a maximizar Z(w) = máximo valor obtenible con una capacidad de w. Si la solución óptima de Z(w) incluye el objeto i, entonces la eliminación del objeto i nos proporcionará la solución óptima de Z(w-wi). Z(w) simplemente es Z(w-wi)+vi para algún i. Como no sabemos para qué i, tenemos que probar todas las posibilidades, es decir 𝑍 𝑤 = max { 𝑍 𝑤 − 𝑤𝑖 + 𝑣𝑖 } i: wi w Trabajo Práctico: Programar para obtener la solución al problema Estrategia: Vuelta atrás ó retroceso ó backtracking 25 Algunas veces debemos encontrar una solución óptima a un subproblema, pero ocurre que no es aplicable ninguna teoría que hasta ahora hemos visto. Entonces debemos recurrir a una búsqueda exhaustiva sistemática. Consideremos el problema de completar un rompecabezas: En un momento dado, se han colocado unas cuantas piezas, y se tantea la colocación de una nueva pieza. Por lo general, sería posible continuar de diversos modos, y cada uno de ellos podría ofrecer a su vez diversas posibilidades, multiplicándose así las posibilidades de tanteo. Por otra parte, el tanteo de soluciones supone muchas veces abandonar una vía muerta cuando se descubre que no conduce a la solución, deshaciendo algunos movimientos y regresando por otras ramas del árbol de búsqueda a una posición anterior. De ahí viene la denominación de esta clase de algoritmos: vuelta atrás o búsqueda con retroceso Estrategia: Vuelta atrás ó retroceso ó backtracking Ejemplo I: El problema de las 8 reinas 26 El problema de las ocho reinas es un juego en el que se colocan ocho reinas en un tablero de ajedrez sin que se amenacen entre ellas: En el ajedrez la reina amenaza a aquellas piezas que se encuentren en su misma fila, columna o diagonal. Una solución correcta entre las 92 posibles sería la siguiente: Estrategia: Vuelta atrás ó retroceso ó backtracking Ejemplo I: El problema de las 8 reinas 27 Como cada reina puede amenazar a todas las reinas que estén en la misma fila, cada una de ellas debe situarse en una fila diferente. Podemos representar las 8 reinas mediante un vector V de 8 elementos, teniendo en cuenta que cada índice i del vector representa una fila y el valor V[i] una columna. Así cada reina estaría en la posición (i, v[i]) para i = 1..8. Por ejemplo, el Vector V = [3,1,6,2,8,6,4,7] nos indica que: • • • • • la reina 1 esta en la columna 3, fila1; la reina 2 en la columna 1, fila 2; la reina 3 en la columna 6, fila 3; la reina 4 en la columna 2, fila 4; etc... Como se puede ver esta solución es incorrecta ya que estarían la reina 3 y la 6 en la misma columna: V[3] = V[6] = 6. Estrategia: Vuelta atrás ó backtracking Ejemplo I: El problema de las 8 reinas 28 Para decidir en cada etapa cuáles son los valores que puede tomar cada uno de los elementos de V debemos tener en cuenta “restricciones “ a fin de que el número de opciones a considerar en cada etapa sea el menor posible. Podemos diferenciar dos tipos de restricciones: • Restricciones explícitas: Formadas por reglas que restringen los valores que pueden tomar los elementos de V a un conjunto determinado. En nuestro problema este conjunto es S = {1,2,3,4,5,6,7,8}. (valores de las columnas del tablero de ajedrez) • Restricciones implícitas. Indican la relación existente entre los posibles valores V para que éstos puedan formar parte de una 8-tupla solución. Deberán formar parte de la lógica de nuestro algoritmo. • • • Restricción 1: “dos reinas no pueden situarse en la misma columna” y por tanto no puede haber dos valores de V iguales, entonces V[i] ≠ V[j] (la propia definición de la tupla impide situar a dos reinas en la misma fila, con lo cual tenemos cubiertos los dos casos, el de las filas y el de las columnas). Restricción 2: “dos reinas no pueden estar en la misma diagonal”. Sean x y x’= valores de fila, y, y’ = valores de columnas. Sean Ri = (x,y) y Rj = (x’, y’) las coordenadas de las reinas i y j en el tablero de ajedrez. Para que no se amenacen en la diagonal debe ocurrir que |x – x’| ≠ |y – y’| Estrategia: Vuelta atrás ó backtracking Esquema general: 29 La estrategia de diseño de Vuelta Atrás es un método exhaustivo de tanteo (prueba y error) que se caracteriza por un avance progresivo en la búsqueda de una solución mediante una serie de etapas. En dichas etapas se presentan unas opciones cuya validez ha de examinarse con objeto de seleccionar una de ellas para proseguir con el siguiente paso. Este comportamiento supone la generación de un árbol y su examen y eventual poda hasta llegar a una solución o a determinar su imposibilidad. Este avance se puede detener cuando se alcanza una solución, o bien si se llega a una situación en que ninguna de las soluciones es válida; en este caso se vuelve al paso anterior, lo que supone que deben recordarse las elecciones hechas en cada paso para poder probar otra opción aún no examinada. Este retroceso (vuelta atrás) puede continuar si no quedan opciones que examinar hasta llegar a la primera etapa (punto de partida). El agotamiento de todas las opciones de la primera etapa supondrá que no hay solución posible pues se habrán examinado todas las posibilidades. Estrategia: Vuelta atrás ó backtracking Esquema general: 30 VueltaAtras(etapa); Inicio InicializarOpciones; Repetir SeleccionarNuevaOpcion; Si Aceptable (se cumplen las restricciones) luego AnotarOpcion; Si SolucionIncompleta luego VueltaAtras(etapa_siguiente); Si NO(éxito) luego CancelarAnotacion; Sino (* solucion completa *) exito:=TRUE Fin_si Fin_si Hasta (exito) ó (UltimaOpcion) Fin Trabajo Práctico: Programar para obtener la solución al problema Hasta aquí vimos …. 31 Voráz (Algoritmos ávidos) Estrategias de diseño de Algoritmos Divide y Vencerás Idea Ejemplos: Factorización, cambio, arbol cub. Esquema General Idea Ejemplos: QuickSort, búsqueda binaria Esquema General Programación Dinámica Vuelta atrás (Backtracking) Idea Ejemplos: Fibonacci, problema de la mochila Esquema General Idea Ejemplos: el problema de las 8 reinas Esquema General