Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones Dr. Jaime Osorio Ubaldo Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 1 / 40 Técnicas de diseño de algoritmos El diseño de algoritmos es un área central en la Ciencia de la Computación, existen diversas técnicas o patrones de diseño de algoritmos que son muy útiles para la solución de diversos problemas, las principales son: 1 Divide y vencerás. 2 Programación dinámica. 3 Algoritmo Greedy ( voraz, goloso, devorador o ávido). 4 Backtracking. 5 Ramificación y poda. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 2 / 40 Divide y vencerás Consiste en resolver el problema a partir de la solución de subproblemas del mismo tipo que el original. Si el problema x es de tamaño n y los tamaños de los subproblemas x1 , x2 , · · · , xk son respectivamente n1 , n2 , · · · , nk el tiempo de ejecución T (n) está definido por T (n) = g (n), k X n ≤ no T (nj ) + f (n), n > no j=1 donde 1 no y g (n) son respectivamente es el tamaño y el costo temporal del caso base. 2 f (n) es el tiempo de descomponer en subproblemas y combinar el resultado de estos. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 3 / 40 Procedimiento 1 Plantear el problema de forma que pueda descomponerse en k subproblemas del mismo tipo, pero de menor tamaño y que no exista traslape entre ellos (si hay traslape se puede usar otra técnica llamada programación dinámica), 2 Resolver independientemente todos los subproblemas, ya sea directamente si son elementales o bien de forma recursiva. 3 Por último, combinar las soluciones obtenidas en el paso anterior para construir la solución del problema original. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 4 / 40 Mergesort (ordenamiento por mezcla) Su estrategia consiste en dividir un conjunto de datos en dos subconjuntos que sean de un tamaño tan similar como sea posible (a la mitad), dividir estas dos partes mediante llamadas recursivas, y finalmente, al llegar al caso base, mezclar los dos partes para obtener un arreglo ordenado. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 5 / 40 Mergesort Pasos 1 Dividir recursivamente la primera mitad del conjunto. 2 Dividir recursivamente la segunda mitad del conjunto. 3 Mezclar los dos subarreglos para ordenarlos. Observación. Ignoramos los casos base. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 6 / 40 Mergesort Merge(A, B, C ) i = 1, j = 1 para k = 1 · · · n hacer si A[i] < B[j] entonces C [k] = A[i] i =i +1 sino C [k] = B[j] j =j +1 fin si fin para. El costo temporal es T1 = 4n + 2. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 7 / 40 Mergesort MergeSort(A[1, · · · , n]) si n = 1 return A m ≈ n/2 B = MergeSort(A[1, · · · , m]) C = MergeSort(A[m + 1, · · · , n]) Merge(B, C , A0 ) return A0 . El costo temporal es T (n) = 2T Jaime Osorio n 2 + 4n + 2 Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 8 / 40 Mergesort De la figura n =1 2k luego k = log2 n Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 9 / 40 Mergesort T (n) ≤ 2T (n/2) + 6n = 22 T (n/4) + 2(6n) .. . = 2k T (1) + k(6n) = 2log2 n (2) + 6n log2 n = 2n + 6n log2 n Por lo tanto, es de orden O(n log2 n). Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 10 / 4 Programación Dinámica “En la programación dinámica normalmente se empieza por los subcasos más pequeños, y por tanto más sencillos. Combinando sus soluciones, obtenemos las respuestas para subcasos de tamaños cada vez mayores, hasta que finalmente llegamos a la solución del caso original” [Brassard Bratley, 2008]. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 11 / 4 Programación Dinámica La programación dinámica permite mejorar el rendimiento de algunos algoritmos. En ciertos casos, cuando se divide un problema en varios subproblemas, resulta ser que éstos no son independientes entre sı́, es decir, cuando se resuelve un subproblema traslapado con otros, esto genera que se tenga que procesar nuevamente la solución generando un mayor costo temporal (en algunos casos puede generar un algoritmo de complejidad exponencial). La programación dinámica consiste en conservar la solución de cada subproblema que ya se ha resuelto en una tabla (puede ser un arreglo o una matriz), para tomarla cuando ésta se requiera. Esto reduce el tiempo necesario para obtener el resultado final, pues evita repetir algunos de los cálculos. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 12 / 4 Pasos 1 Plantear un algoritmo de solución recursivo. 2 Organizar las soluciones parciales en una tabla. 3 Modificar el algoritmo recursivo de manera que antes de hacer el llamado recursivo se consulte la tabla y, en caso de hallarse la solución en ésta, ya no se hace el llamado recursivo. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 13 / 4 Aplicación: Sucesión de Fibonacci La sucesión de Fibonacci es 1, 1, 2, 3, 5, 8, · · · recursivamente está definida por 1, n = 0, 1 F (n) = F (n − 1) + F (n − 2), n ≥ 2 Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 14 / 4 Algoritmo usando Programación Dinámica Tabla[1 · · · n] = 0 Fibonacci(n) Si n ≤ 1 retorna 1 Si Tabla[n] = 0 Tabla[n] = Fibonacci(n − 1) + Fibonacci(n − 2) retorna Tabla[n] Si bien en este algoritmo hay recursión, ésta solo se lleva a cabo cuando no hay datos en la tabla. Cada vez que se requiere un nuevo dato en la tabla se hace la recursión, sin embargo ésta no es muy profunda, ya que toma los datos que ya se encuentran calculados en la tabla sin necesidad de llegar cada vez al caso base. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 15 / 4 Caracterı́sticas 1 En la programación dinámica va guardando la historia y construyendo la solución a partir de las soluciones óptimas de problemas más pequeños resueltos previamente. 2 En la programación dinámica el problema debe poder dividirse en varias etapas, y la decisión en una etapa se toma después de haber considerado los resultados de otras etapas más simples. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 16 / 4 Algoritmo Greedy Caracterı́sticas 1 Generalmente son utilizados para resolver problemas de optimización (maximización o minimización de la función objetivo). 2 Estos algoritmos toman decisiones basándose en la información que se tiene disponible de forma inmediata, sin tomar en cuenta los efectos que pudieran causar en el futuro. 3 Son fáciles de implementar. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 17 / 4 Algoritmo Greedy Procedimiento. 1 Se define el conjunto candidatos, por ejemplo: el conjunto de monedas disponibles, las aristas de un grafo que se pueden utilizar para construir una ruta, el conjunto de tareas que hay que planificar, etc. 2 En cada paso se considera al candidato indicado por una función de selección voraz, que indica cuál es el candidato más prometedor de entre los que aún no se han considerado, es decir, los que aún no han sido seleccionados ni rechazados. 3 Conforme avanza el algoritmo, los elementos del conjunto inicial de candidatos pasan a formar parte de uno de dos conjuntos: el de los candidatos que ya se consideraron y que forman parte de la solución o el conjunto de los candidatos que ya se consideraron y se rechazaron. 4 Mediante una función de factibilidad se determina si es posible o no completar un conjunto de candidatos que constituya una solución al problema. Es decir, se analiza si existe un candidato compatible con la solución parcial construida hasta el momento. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 18 / 4 El problema del cambio 1 2 3 Dado un monto n y un conjunto de denominaciones de billetes, se desea elegir la mı́nima cantidad de billetes cuya suma sea n. Para construir la solución con un algoritmo voraz tomaremos cada vez el billete de mayor denominación, siempre y cuando la suma acumulada no sea mayor a n. El algoritmo falla cuando se presenta el caso en el que la suma de los billetes no puede igualar a n, en este caso el algoritmo ávido no encuentra una solución. Nótese que en este caso en particular no existe restricción en cuanto al número de billetes que se pueden tomar, es decir, siempre es posible elegir k billetes de una determinada denominación. Por lo tanto no es necesario tomar en cuenta si un billete ya se consideró o no. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 19 / 4 El problema del cambio Veamos como serı́a la implementación #define NUM 5 int denominaciones[NUM] = {100, 20, 10, 5, 1}; int numBilletes[NUM]; for (int i = 0; i < NUM; i + +) numBilletes[i] = 0; int SeleccionVoraz(int resto){ int i = 0; while(i < NUM ∧ denominaciones[i] > resto) i + +; return i; }; Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 20 / 4 El problema del cambio void cambio(int monto, int ∗ numBilletes){ int suma = 0; int iDen = 0; while(suma < monto){ iDen = SeleccionVoraz(monto − suma); if (iDen >= NUM){ cout << “No se encontr ó soluci ón. ”; return; }; numBilletes[iDen] = numBilletes[iDen] + 1; suma = suma + denominaciones[iDen]; }; } Los algoritmos voraces construyen la solución en etapas sucesivas, tratando siempre de tomar la decisión óptima para cada etapa. Sin embargo, la búsqueda de óptimos locales no tiene por qué conducir siempre a un óptimo global. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 21 / 4 El problema del cambio Supongamos que queremos cambiar 15 soles y el sistema monetario está compuesto por billetes de 1, 5 y 11 soles. El algoritmo voraz tomará primero el billete mayor, por lo que la solución que obtendrá es 15 = 11 + 1 + 1 + 1 + 1 sin embargo, la solución óptima es 15 = 5 + 5 + 5 “La estrategia de los algoritmos ávidos consiste en tratar de ganar todas las batallas sin pensar que, como bien saben los estrategas militares y los jugadores de ajedrez, para ganar la guerra muchas veces es necesario perder alguna batalla.” [Guerqueta y Vallecillo, 2000]. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 22 / 4 Backtracking 1 Cuando las técnicas divide y vencerás, algoritmos greedy y programación dinámica no son aplicables para resolver un problema, entonces no queda más remedio que utilizar la fuerza bruta (algoritmos exhaustivos), pero esto en algunos casos resulta inviable debido a su alto costo temporal. 2 El backtracking (búsqueda con retroceso) se aplica a problemas que requieren de una búsqueda exhaustiva dentro del conjunto de todas las soluciones potenciales. El espacio a explorar se estructura de tal forma que se puedan ir descartando bloques de soluciones que posiblemente no son satisfactorias 3 Los algoritmos de backtracking optimizan el proceso de búsqueda de tal forma que se puede hallar una solución de una forma más rápida que con los algoritmos de la fuerza bruta. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 23 / 4 Pasos Ya que las soluciones se construyen por etapas se procede de la siguiente manera: 1 El espacio de posibles soluciones se estructura como un árbol de exploración (cuyos nodos son denominados nodos estados), en el que en cada nivel se toma la decisión de la etapa correspondiente. 2 En cada etapa de búsqueda en el algoritmo backtracking, el recorrido del árbol de búsqueda se realiza en profundidad. Si una extensión de la solución parcial actual no es posible, se retrocede para intentar por otro camino, de la misma manera en la que se procede en un laberinto cuando se llega a un callejón sin salida. 3 Para una determinada etapa, se elige una de las opciones dentro del árbol de búsqueda y se analiza para determinar si esta opción es aceptable como parte de la solución parcial. En caso de que no sea aceptable, se procede a elegir otra opción dentro de la misma etapa hasta encontrar una opción aceptable o bien agotar todas las opciones. Si se encuentra una solución aceptable, ésta se guarda y se profundiza en el árbol de búsqueda aplicando el mismo proceso a la Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 24 / 4 Pseudocódigo Función retroceso(etapa) iniciarOpciones() Hacer opcion→ seleccionarOpción Si aceptable(opcion) entonces guardar(opcion) Si incompleta(solución) entonces exito → retroceso(siguiente(etapa)) Si(exito=FALSO) entonces retirar(opcion) finSi Sino exito → VERDADERO finSi finSi mientras(exito= FALSO AND opcion 6= últimaOpción) regresar exito Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 25 / 4 El problema de las N reinas Consiste en situar N Reinas en un tablero de ajedrez de N filas y N columnas, sin que se ataquen entre si. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 26 / 4 El problema de las N reinas ¿Cuántas alternativas tenemos que explorar? Por conteo, tenemos 64 casillas en un tablero de ajedrezde 8 × 8, y 8 reinas que ubicar en posiciones distintas, ası́ que hay 64 8 alternativas de solución (tableros con las 8 reinas situadas en posiciones distintas), esto es, 4 426 165 368. Al número de alternativas se le denomina tamaño del espacio de búsqueda, y siempre que tengamos un problema de búsqueda como éste de las N-Reinas es conveniente estimarlo mediante técnicas de conteo. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 27 / 4 Algoritmo 1 Ir colocando las reinas una a una en columnas en un vector A de tal manera que no se ataquen 2 Si las primeras k reinas colocadas no se atacan avanzar a colocar la reina k+1. Si tenemos éxito k irá aumentando de 1 hasta n y habremos situado las n reinas en columnas donde no se ataquen entre sı́ y obtendremos una solución. 3 Si no tenemos exito, es necesario hacer una vuelta atrás para cambiar la columna en la que se colocó la última reina. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 28 / 4 Usando Backtracking El árbol de exploración es Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 29 / 4 Ramificación y poda Es una variante de backtracking. Se usa generalmente para resolver problemas de optimización. Hace uso de un árbol de expansión para analizar las posibles soluciones. A diferencia del backtracking, permite generar nodo utilizando distintas estrategias. Puede recorrer el árbol no solo en profundidad sino también en anchura o utilizando el cálculo de funciones de coste para seleccionar el nodo más prometedor. Utiliza cotas (superiores o inferiores) para poder podar (eliminar) ramas del árbol que no conducen a la solución óptima. Estos algoritmos pueden ser paralelizados. Necesita más memoria que los algoritmos backtracking, ya que cada nodo debe contener la información necesaria para realizar los procesos de bifurcación. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 30 / 4 Etapas 1 Selección. Consiste en extraer un nodo entre el conjunto de nodos vivos (nodo que no ha sido podado). 2 Ramificación. Aquı́ se construyen los nodos hijos del nodo seleccionado en la etapa anterior. 3 Poda. En esta etapa se eliminan algunos nodos creados en la ramificación con la finalidad de disminuir el espacio de búsqueda y atenuar la complejidad del algoritmo. Con aquellos nodos que no han sido podados se repite nuevamente el proceso de selección, el algoritmo termina cuando se encuentra la solución o se agoten las posibilidades. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 31 / 4 Pseudocódigo Repetir elegir el nodo más prometedor como nodo P generar todos sus hijos podar el nodo P Para cada hijo hacer Si coste(hijo) > coste(mejor solución en curso) entonces se poda sino si (no es solución) entonces se pasa a la lista de nodos vivos sino es la mejor solución en curso y se revisa la lista de nodos vivos, eliminando los que prometen algo peor. fsi fsi fpara hasta que la lista está vacı́a. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 32 / 4 El problema de la Planificación de Tareas Trab/Tarea a b c d 1 11 14 11 17 2 12 15 17 14 3 18 13 19 20 4 40 22 23 28 Cota Superior. Si asignamos al trabajador a tarea 1, al trabajador b tarea 2, al trabajador c tarea 3 y al trabajador d tarea 4, el costo serı́a de 11 + 15 + 19 + 28 = 73. Si tomamos la otra diagonal será 40 + 13 + 17 + 17 = 87. Por lo tanto, nos quedamos con 73 por se el menor. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 33 / 4 El problema de la Planificación de Tareas Cota Inferior. Tomando el menor costo de cada tarea independientemente de quien lo realice, con lo cual el costo será de 11 + 12 + 13 + 22 = 58. Si tomamos el menor de cada fila asumiendo que cada trabajador debe hacer alguna de ellas se tiene 11 + 13 + 11 + 14 = 49. Por lo tanto, elegiremos 58. Con lo cual nuestro intervalo de búsqueda será [58, 73]. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 34 / 4 El problema de la Planificación de Tareas Busquemos otra cota inferior usando árboles con soluciones parciales. Asignar al trabajador a la primera tarea, luego asignemos los menores costos de las tareas a los demás, es decir, tarea 2 a d, tarea 3 a b y tarea 4 a b, con lo cual es costo es de 11 + 14 + 13 + 22 = 60 Analogamente si asignamos al trabajador a la segunda tarea, tendremos 11 + 12 + 13 + 22 = 58. Ahora la tercera tarea 18 + 11 + 14 + 22 = 65 Y la cuarta tarea 40 + 11 + 14 + 13 = 78 Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 35 / 4 El problema de la Planificación de Tareas Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 36 / 4 El problema de la Planificación de Tareas Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 37 / 4 El problema de la Planificación de Tareas Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 38 / 4 El problema de la Planificación de Tareas Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 39 / 4 Temas de investigación. Resuelva los siguientes problemas usando algoritmos greedy, backtraking y ramificación y poda: 1 El problema de la mochila. 2 El problema de la planificación de tareas. 3 El problema del agente viajero. Jaime Osorio Técnicas para el Diseño de Algoritmos Estructura de Datos y Aplicaciones 40 / 4