PPP - Algoritmos
PPP - Algoritmos
PPP - Algoritmos
February 1, 1999
Contents
1 Introduccion
1.1 Algoritmos : : : : : : : : : : : : : : : : : : : : : : : : : : 1.1.1 Analisis de algoritmos : : : : : : : : : : : : : : : : 1.1.2 Diseno de algoritmos : : : : : : : : : : : : : : : : : 1.2 Modelos computacionales : : : : : : : : : : : : : : : : : : 1.2.1 Maquina de Turing : : : : : : : : : : : : : : : : : : 1.2.2 Maquina RAM de costo jo : : : : : : : : : : : : : 1.2.3 Maquina RAM de costo variable : : : : : : : : : : 1.3 Un problema basico: elevando un numero a una potencia. 1.4 Resumen y aspectos clave : : : : : : : : : : : : : : : : : : 1.5 Ejercicios : : : : : : : : : : : : : : : : : : : : : : : : : : : 2.1 Algoritmos iterativos : : : : : : : : : : : : : : : 2.1.1 Sort por burbujeo : : : : : : : : : : : : 2.1.2 Insert-sort : : : : : : : : : : : : : : : : : 2.2 Orden de un algoritmo : : : : : : : : : : : : : : 2.3 Notacion \O" : : : : : : : : : : : : : : : : : : : 2.3.1 O(g(n)) : : : : : : : : : : : : : : : : : : 2.3.2 (g(n)) : : : : : : : : : : : : : : : : : : 2.3.3 (g(n)) : : : : : : : : : : : : : : : : : : 2.4 El problema de la distancia minima en el plano 2.4.1 Solucion iterativa : : : : : : : : : : : : : 2.4.2 Analisis : : : : : : : : : : : : : : : : : : 2.5 Resumen y puntos clave : : : : : : : : : : : : : 2.6 Ejercicios : : : : : : : : : : : : : : : : : : : : : 3.1 Algoritmos recursivos : : : : : : : : : : : : : 3.2 Recurrencias : : : : : : : : : : : : : : : : : : 3.2.1 El metodo de sustitucion : : : : : : : : 3.2.2 El metodo iterativo : : : : : : : : : : : 3.2.3 El teorema maestro de las recurrencias 3.2.4 Otras herramientas : : : : : : : : : : : 1 : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 8 9 10 10 10 13 15 16 18 19
2 Algoritmos iterativos
20
20 20 24 27 27 28 28 28 30 30 30 31 32 33 34 34 35 36 36
3 Algoritmos Recursivos
33
3.3 Ejemplos : : : : : : : : : : : : : 3.3.1 Ejemplo I. (Messier) : : : 3.3.2 Ejemplo II : : : : : : : : 3.4 Calculo de numeros de Fibonacci 3.4.1 Solucion recursiva : : : : 3.4.2 Solucion iterativa : : : : : 3.4.3 Una solucion mejor : : : : 3.4.4 Conclusiones : : : : : : : 3.5 Resumen y puntos clave : : : : : 3.6 Ejercicios : : : : : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
38 38 39 40 40 42 43 45 45 45
4.1 Divide & Conquer : : : : : : : : : : : : : : : : : : : : : : 4.2 Merge Sort : : : : : : : : : : : : : : : : : : : : : : : : : : 4.2.1 Propiedades : : : : : : : : : : : : : : : : : : : : : : 4.2.2 Optimizaciones y trucos : : : : : : : : : : : : : : : 4.2.3 Resumen MergeSort : : : : : : : : : : : : : : : : : 4.3 El problema de la seleccion : : : : : : : : : : : : : : : : : 4.3.1 Cribas : : : : : : : : : : : : : : : : : : : : : : : : : 4.3.2 Eligiendo el pivote : : : : : : : : : : : : : : : : : : 4.3.3 Particionando : : : : : : : : : : : : : : : : : : : : : 4.4 Problemas no tan simples: Como multiplicar dos numeros 4.4.1 Dividir para conquistar la multiplicacion : : : : : : 4.4.2 El metodo de Gauss : : : : : : : : : : : : : : : : : 4.5 El problema de los puntos mas cercanos. Segunda parte : 4.5.1 Analisis : : : : : : : : : : : : : : : : : : : : : : : : 4.6 Resumen y puntos clave : : : : : : : : : : : : : : : : : : : 4.7 Ejercicios : : : : : : : : : : : : : : : : : : : : : : : : : : :
47
47 47 50 50 51 51 51 53 56 56 57 58 59 62 62 63 65 65 65 65 66 67 69 71 71 72 72 72 75 76 76 77
5 Algoritmos Aleatorizados
5.1 Algoritmos aleatorizados : : : : : : : : : : : : : : : : : : : : : 5.1.1 Algoritmos tipo Las Vegas : : : : : : : : : : : : : : : : 5.1.2 Algoritmos tipo Montecarlo : : : : : : : : : : : : : : : 5.2 Quick Sort : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 5.2.1 Fundamentos de Quick Sort : : : : : : : : : : : : : : : 5.2.2 Algoritmo para particionar el vector : : : : : : : : : : 5.2.3 Analisis de QuickSort : : : : : : : : : : : : : : : : : : 5.2.4 La ventaja del algoritmo aleatorizado : : : : : : : : : 5.2.5 Resumen QuickSort : : : : : : : : : : : : : : : : : : : 5.3 El problema de los puntos mas cercanos en el plano. Parte 3. 5.3.1 Algoritmos aleatorizados incrementales : : : : : : : : : 5.3.2 El algoritmo : : : : : : : : : : : : : : : : : : : : : : : 5.3.3 Analisis : : : : : : : : : : : : : : : : : : : : : : : : : : 5.4 Testeando primalidad : : : : : : : : : : : : : : : : : : : : : : 5.4.1 Algoritmo por fuerza bruta : : : : : : : : : : : : : : : 5.4.2 Algoritmo por fuerza bruta mejorado : : : : : : : : : : 2
65
5.4.3 El pequenio teorema de Fermat : : : : : 5.4.4 Algoritmo aleatorizado tipo Montecarlo 5.5 Resumen y puntos clave : : : : : : : : : : : : : 5.6 Ejercicios : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : :
77 77 79 80
6 Sort
6.1 Algoritmos de Sort : : : : : : : : : : : : 6.1.1 Algoritmos no recursivos : : : : : 6.1.2 Algoritmos recursivos : : : : : : 6.2 HeapSort : : : : : : : : : : : : : : : : : 6.2.1 Heaps : : : : : : : : : : : : : : : 6.2.2 HeapSort : : : : : : : : : : : : : 6.3 Limite inferior de los algoritmos de sort 6.4 Algoritmos de sort lineales : : : : : : : : 6.4.1 Counting Sort : : : : : : : : : : : 6.4.2 Radix Sort : : : : : : : : : : : : 6.5 Resumen y puntos clave : : : : : : : : : 6.6 Ejercicios : : : : : : : : : : : : : : : : :
: : : : : : : : : : : :
: : : : : : : : : : : :
: : : : : : : : : : : :
: : : : : : : : : : : :
81
81 81 82 82 82 85 87 89 89 91 92 93 95 95 95 96 96 96 97 97 97 98 98 98
7 Grafos
7.1 Grafos : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 7.2 Introduccion a la Teoria de Grafos : : : : : : : : : : : : : : : : : 7.2.1 Grafos y Grafos Dirigidos : : : : : : : : : : : : : : : : : : 7.2.2 Vertices adyacentes : : : : : : : : : : : : : : : : : : : : : 7.2.3 Grafos pesados : : : : : : : : : : : : : : : : : : : : : : : : 7.2.4 Grado de un vertice : : : : : : : : : : : : : : : : : : : : : 7.2.5 Caminos y ciclos : : : : : : : : : : : : : : : : : : : : : : : 7.2.6 Ciclo Hamiltoniano : : : : : : : : : : : : : : : : : : : : : : 7.2.7 Ciclo Euleriano : : : : : : : : : : : : : : : : : : : : : : : : 7.2.8 Grafo acilico : : : : : : : : : : : : : : : : : : : : : : : : : 7.2.9 Grafo conexo : : : : : : : : : : : : : : : : : : : : : : : : : 7.2.10 Componentes conexos, arboles y bosques : : : : : : : : : 7.2.11 Grafos fuertemente conexos y componentes fuertemente conexos : : : : : : : : : : : : : : : : : : : : : : : : : : : : 7.2.12 Grafo dirigido aciclico. (GDA) : : : : : : : : : : : : : : : 7.2.13 Grafos Isomorfos : : : : : : : : : : : : : : : : : : : : : : : 7.2.14 Subgrafos y grafo inducido : : : : : : : : : : : : : : : : : : 7.2.15 Grafos completos. Cliques. : : : : : : : : : : : : : : : : : 7.2.16 Conjunto independiente : : : : : : : : : : : : : : : : : : : 7.2.17 Grafo bipartito : : : : : : : : : : : : : : : : : : : : : : : : 7.2.18 Grafo complemento, Grafo transpuesto : : : : : : : : : : : 7.2.19 Grafos planares, Formas planares : : : : : : : : : : : : : : 7.2.20 Tamanios : : : : : : : : : : : : : : : : : : : : : : : : : : : 7.2.21 Grafos dispersos : : : : : : : : : : : : : : : : : : : : : : : 7.3 Representacion de Grafos : : : : : : : : : : : : : : : : : : : : : : 7.3.1 Matriz de adyacencias : : : : : : : : : : : : : : : : : : : : 3
95
99 99 99 100 101 101 101 101 102 103 103 103 103
7.3.2 Lista de Adyacencias : 7.4 Algoritmos para Grafos : : : 7.4.1 BFS : : : : : : : : : : 7.4.2 DFS : : : : : : : : : : 7.5 Resumen y puntos clave : : : 7.6 Ejercicios : : : : : : : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
: : : : : :
8 Programacion dinamica
8.1 Introduccion a la programacion dinamica : : : : : : : : : : : : : : 8.1.1 Caracteristicas de un problema de programacion dinamica 8.2 Multiplicacion en cadena de matrices : : : : : : : : : : : : : : : : 8.2.1 Aproximacion por fuerza bruta : : : : : : : : : : : : : : : 8.2.2 Solucion por programacion dinamica : : : : : : : : : : : : 8.3 El problema de todas las distancias minimas en un grafo : : : : : 8.3.1 De nicion del problema : : : : : : : : : : : : : : : : : : : 8.3.2 Solucion por programacion dinamica : : : : : : : : : : : : 8.3.3 Primera solucion : : : : : : : : : : : : : : : : : : : : : : : 8.3.4 Algoritmo de Floyd-Warshall : : : : : : : : : : : : : : : : 8.4 El problema de la subsecuencia comun de maxima longitud : : : 8.4.1 Solucion por programacion dinamica : : : : : : : : : : : : 8.4.2 Analisis : : : : : : : : : : : : : : : : : : : : : : : : : : : : 8.5 Resumen y puntos clave : : : : : : : : : : : : : : : : : : : : : : : 8.6 Ejercicios : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 9.1 Algoritmos golosos, principios. : : : : : : : : : : : : : : 9.1.1 Fundamentos : : : : : : : : : : : : : : : : : : : : 9.2 El problema de la seleccion de actividades : : : : : : : : 9.2.1 Ejemplo : : : : : : : : : : : : : : : : : : : : : : : 9.2.2 Algoritmo goloso para la seleccion de actividades 9.2.3 Analisis : : : : : : : : : : : : : : : : : : : : : : : 9.3 El problema de los caminos minimos (parte II) : : : : : 9.3.1 Algoritmo de Dijkstra para caminos minimos : : 9.3.2 Mecanismo de relajacion : : : : : : : : : : : : : : 9.3.3 El algoritmo de de Dijkstra : : : : : : : : : : : : 9.3.4 Validacion : : : : : : : : : : : : : : : : : : : : : : 9.3.5 Analisis : : : : : : : : : : : : : : : : : : : : : : : 9.4 El problema del arbol generador minimo : : : : : : : : : 9.4.1 Algoritmo de Kruskal : : : : : : : : : : : : : : : 9.4.2 Analisis : : : : : : : : : : : : : : : : : : : : : : : 9.5 Resumen y puntos clave : : : : : : : : : : : : : : : : : : 9.6 Ejercicios : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
120
120 120 121 122 123 128 129 129 130 133 135 135 136 138 139 140 141 141 141 141 142 143 143 143 144 145 146 147 149 151 151 151
9 Algoritmos Golosos
140
10.1 Revision de alguna estructuras de datos simples : 10.1.1 Pilas : : : : : : : : : : : : : : : : : : : : : 10.1.2 Colas : : : : : : : : : : : : : : : : : : : : 10.1.3 Listas : : : : : : : : : : : : : : : : : : : : 10.1.4 Arboles Binarios : : : : : : : : : : : : : : 10.1.5 Arboles AVL : : : : : : : : : : : : : : : : 10.2 Analisis amortizado : : : : : : : : : : : : : : : : 10.2.1 Metodo de agregacion : : : : : : : : : : : 10.2.2 Metodo contable : : : : : : : : : : : : : : 10.2.3 Metodo de los potenciales : : : : : : : : : 10.3 Heaps Binomiales : : : : : : : : : : : : : : : : : : 10.3.1 Arboles Binomiales : : : : : : : : : : : : : 10.3.2 Estructura de un Heap Binomial : : : : : 10.3.3 Implementacion : : : : : : : : : : : : : : : 10.3.4 Implementacion de las operaciones : : : : 10.4 Heaps de Fibonacci : : : : : : : : : : : : : : : : : 10.5 Resumen de Heaps : : : : : : : : : : : : : : : : : 10.6 Union-Find : : : : : : : : : : : : : : : : : : : : : 10.6.1 Implementacion usando listas : : : : : : : 10.6.2 Segunda Implementacion usando listas : : 10.6.3 Implementacion usando Forests : : : : : : 10.7 Hash-Tables : : : : : : : : : : : : : : : : : : : : : 10.7.1 Operaciones : : : : : : : : : : : : : : : : : 10.7.2 Resolucion de colisiones : : : : : : : : : : 10.7.3 Funciones de Hashing : : : : : : : : : : : 10.8 Resumen y puntos clave : : : : : : : : : : : : : : 10.9 Ejercicios : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
152
152 152 153 153 154 154 155 155 157 158 159 160 161 161 163 165 165 166 167 168 169 175 175 176 182 182 183 185 186 188 189 190 190 190 191 192 193 193 194 196 196 197 198
11 Complejidad
11.1 Introduccion : : : : : : : : : : : : : : : : : : : : : : 11.1.1 Reducciones : : : : : : : : : : : : : : : : : : 11.2 Fundamentos : : : : : : : : : : : : : : : : : : : : : 11.2.1 El problema del viajante : : : : : : : : : : : 11.3 Complejidad : : : : : : : : : : : : : : : : : : : : : : 11.3.1 La clase P de problemas : : : : : : : : : : : 11.3.2 La eclase NP de problemas : : : : : : : : : 11.3.3 La clase de problemas NP-Completos : : : : 11.3.4 La clase de problemas No-NP : : : : : : : : 11.4 Reducciones : : : : : : : : : : : : : : : : : : : : : : 11.4.1 Ejemplo I : : : : : : : : : : : : : : : : : : : 11.4.2 Ejemplo II. : : : : : : : : : : : : : : : : : : 11.5 Algunos problemas NP-Completos : : : : : : : : : 11.5.1 SAT : : : : : : : : : : : : : : : : : : : : : : 11.5.2 3-SAT (SAT / 3-SAT) : : : : : : : : : : : : 11.5.3 Programacion Lineal Entera (SAT / PLE) 5
185
11.5.4 Vertex Cover (3-SAT / VC) : : : : : : : : : : : : : : 11.5.5 Conjunto Independiante (VC / IS) : : : : : : : : : : : 11.5.6 Clique (IS / Clique) : : : : : : : : : : : : : : : : : : : 11.5.7 Particion Entera (VC / IP) : : : : : : : : : : : : : : : Tecnicas paera demostrar que un problema es NP-Completo : 11.6.1 Restriccion : : : : : : : : : : : : : : : : : : : : : : : : 11.6.2 Reemplazo local : : : : : : : : : : : : : : : : : : : : : 11.6.3 Recomendaciones : : : : : : : : : : : : : : : : : : : : : Teoria formal del los problemas NP-Completos : : : : : : : : 11.7.1 Maquinas de Turing No-Deterministicas : : : : : : : : 11.7.2 El teorema de Cook : : : : : : : : : : : : : : : : : : : Resumen y puntos clave : : : : : : : : : : : : : : : : : : : : : Ejercicios : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : :
: : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : : :
199 202 202 203 204 204 204 204 205 206 206 207 207
12.1 Sort utilizando varios procesadores : : : : 12.2 Sorting Networks : : : : : : : : : : : : : : 12.2.1 Principio de paralelismo : : : : : : 12.2.2 Analisis de redes de ordenamiento 12.2.3 Ejemplo I : : : : : : : : : : : : : : 12.2.4 Ejemplo II : : : : : : : : : : : : : 12.3 El principio del 0-1 : : : : : : : : : : : : : 12.4 Construccion de una red de ordenamiento 12.4.1 Secuencias Bitonicas : : : : : : : : 12.4.2 Bitonic-Sorter : : : : : : : : : : : : 12.4.3 Merger : : : : : : : : : : : : : : : : 12.4.4 Sorting Network : : : : : : : : : : 12.5 Resumen y puntos clave : : : : : : : : : : 12.6 Ejercicios : : : : : : : : : : : : : : : : : :
208
208 209 210 210 211 211 212 212 212 213 214 216 217 217
13 Aproximacion y Heuristicas
13.1 Introduccion : : : : : : : : : : : : : : : : : : : : : : : : : : : : : 13.2 Algoritmos de aproximacion : : : : : : : : : : : : : : : : : : : : 13.2.1 Tasa de aproximacion : : : : : : : : : : : : : : : : : : : 13.2.2 Tasa de error : : : : : : : : : : : : : : : : : : : : : : : : 13.2.3 Aproximando problemas de optimizacion : : : : : : : : : 13.3 Vertex Cover (VC) : : : : : : : : : : : : : : : : : : : : : : : : : 13.3.1 La heuristica del dos por uno : : : : : : : : : : : : : : : 13.3.2 La heuristica golosa : : : : : : : : : : : : : : : : : : : : 13.4 Aproximaciones y reducciones : : : : : : : : : : : : : : : : : : : 13.5 El problema del centro equidistante : : : : : : : : : : : : : : : : 13.5.1 Aproximacion golosa : : : : : : : : : : : : : : : : : : : : 13.5.2 Tasa de aproximacion : : : : : : : : : : : : : : : : : : : 13.6 El problema del viajante : : : : : : : : : : : : : : : : : : : : : : 13.6.1 Aproximacion para el problema Euclideano del viajante 13.7 Otros problemas : : : : : : : : : : : : : : : : : : : : : : : : : : 6
218
218 219 219 219 220 220 220 221 222 223 224 225 226 226 227
13.7.1 K-Coloring : : : : : 13.7.2 Arboles de decision : 13.7.3 Clique : : : : : : : : 13.7.4 Set-Cover : : : : : : 13.8 Resumen y puntos clave : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
: : : : :
14 Indices
230
Chapter 1
Introduccion
1.1 Algoritmos
\Un algoritmo es un conjunto de pasos claramente de nidos que a partir de una cierta entrada (input) produce una determinada salida (output)."
algoritmo es independiente del lenguaje en el cual se programa, de la maquina en la cual se implemente y de otras restricciones que hacen a la puesta en operacion del algoritmo. Desde el punto de vista del estudio de los algoritmos los mismos pueden considerarse como entidades matematicas abstractas independientes de restricciones tecnologicas.
produce siempre resuelve un determinado problema a partir de una entrada valida. Demostrar ya sea en forma rigurosa o intuitiva que un algoritmo es correcto es el primer paso indispensable en el analisis de un algoritmo. A esta fase del analisis de algoritmos se la conoce como validacion del algoritmo. Los algoritmos que no son correctos a veces pueden ser utiles si, por ejemplo, producen una respuesta aproximada a un problema particularmente dificil en forma e ciente.
Importante El primer paso en todo analisis de un algoritmo es validarlo, es decir, demostrar que el algoritmo es correcto.
no-deterministico cuando no es deterministico. Que un algoritmo sea o no sea deterministico no aporta dato alguno sobre la correccion del algoritmo. El estudio de los algoritmos se puede dividir en dos grandes categorias: el analisis de algoritmos y el disenio de algoritmos.
El analisis de algoritmos mediante el uso de herramientas como por ejemplo la evaluacion de costos intenta determinar que tan e ciente es un algoritmo para resolver un determinado problema. En general el aspecto mas interesante a analizar de un algoritmo es su costo.
Costo de tiempo
Suele ser el mas importante, indica cuanto tiempo insume un determinado algoritmo para encontrar la solucion a un problema, se mide en funcion de la cantidad o del tamanio de los datos de entrada.
Costo de espacio
Mide cuanta memoria (espacio) necesita el algoritmo para funcionar correctamente. Consideremos por un momento un programa que juega al ajedrez, el programa funcionar en base a un unico algoritmo que se llama SearchBestMove. Que devuelve para una posicion de piezas dada cual es la mejor movida para un cierto bando (blancas o negras). Con este algoritmo el programa es sencillo, dependiendo de quien empiece lo unico que debe hacer es aplicar el algoritmo cada vez que le toque, esperar la movida del contrario y luego aplicar el algoritmo a la nueva posicion. Pensemos ahora en el algoritmo SearchBestMove, este algoritmo para ser optimo deberia analizar todas las posibles jugadas, todas las posibles respuestas a dichas jugadas y asi sucesivamente, formando un arbol de jugadas posibles del cual selecciona la jugada que produce mejor resultado nal. Este aunque funcione en forma ultra-veloz tiene un grave problema de espacio, no hay memoria su ciente en ninguna maquina construida por el hombre para almacenar todas las posibles combinaciones de jugadas.
El disenio de algoritmos se encarga de encontrar cual es el mejor algoritmo para un problema determinado, en general existen algunos paradigmas basicos que pueden aplicarse para encontrar un buen algoritmo. Es claro que esta es una tarea dificil que requiere de conocimientos especi cos y de una habilidad particular. Algunas de las tecnicas mas utilizadas en el disenio de algoritmos son las siguientes: Dividir para conquistar. Algoritmos aleatorizados. Programacion dinamica. Algoritmos golosos (Greedy). Algoritmos de heuristicos. Reduccion a otro problema conocido. Uso de estructuras de datos que solucionen el problema.
La maquina de Turing es el mas basico de los modelos computacionales y fue propuesta por el celebre matematico Alan Turing. La maquina de Turing maneja tres simbolos que llamaremos \a" , \b" y \espacio". Ademas cuenta con una memoria in nita que puede verse como una cinta in nita. La maquina es capaz de leer en cada paso un simbolo de la cinta, escribir el mismo u otro simbolo en la cinta y avanzar o retroceder una posicion. Por lo tanto podemos representar un programa para la maquina de Turing como un automata de estados nitos donde cada estado tiene la siguiente notacion.
Ver gura 1
10
Donde s1 es el simbolo que la maquina lee de la cinta \a" , \b" o \ " , s2 es el simbolo que la maquina escribe en la cinta en la misma posicion donde leyo s1 y el +1 o -1 indica si se avanza o se retrocede una posicion en la cinta. La echa indica cual es el estado hacia el cual se desplaza la maquina, que podria ser el mismo estado donde estaba antes. Aunque en principio pueda parecer increible cualquier problema que pueda resolver una computadora puede ser resuelto en la maquina de Turing. Esto fue demostrado oportunamente por Alan Turing y no es tema de esta materia.
aaab
00
Ejemplo 2 Ejemplo 2: Copiar todas las"a" de la memoria a continuacin de las \b". Ej : \ aaa bbbbb ! \ bbbbbaaa
00 00
11
Considerando al simbolo \a" como un \1" y al smbolo \b" como un \0" la maquina de Turing es capaz de realizar operaciones aritmeticas. Como sumar, restar multiplicar, dividir etc. Si representamos los caracteres de alguna forma binaria la maquina es capaz de trabajar con strings, nalmente podriamos llegar a conjeturar que la maquina es capaz de realizar cualquier operacion con una memoria binaria y por ende es capaz de operar como cualquier computadora.
iza la maquina), contando tambien aquellas transiciones que parten de un estado y vuelven al mismo. Por ejemplo nuestro programa para la mquina de Turing del ejemplo 1. Para el caso: \aaaaa insume 5 transiciones y la cinta queda de la forma: \aaaab
00 00
de un algoritmo. Nuestro primer algoritmo para la maquina de Turing tiene 4 estados. En la maquina de Turing el analisis de la e ciencia de un algoritmo pasa por averiguar si no existe un programa que pueda resolver el mismo problema en forma mas rapida (empleando menos transiciones) o bien utilizando menos espacio (menor cantidad de estados). La maquina de Turing ofrece varias ventajas desde el punto de vista del estudio de algoritmos. En primer lugar es un ambiente computacional completo ya que como demostrara Alan Turing cualquier problema computacionalmente solucionable puede resolverse usando una maquina de Turing. En segundo lugar el modelo que plantea es ideal para el calculo de costos de algoritmos ya sea en tiempo como en espacio. Lamentablemente la maquina de Turing es un modelo poco practico ya que al ser demasiado abstracta programar resulta extremadamente complejo. Ademas no es una buena referencia para la obtencion de programas \reales" ya que un programa hecho para la maquina de Turing no tiene relacion alguna con un programa realizado en un lenguaje de proposito general. Es por esto que destacando la nobleza de la maquina de Turing pasamos a otros modelos un tanto mas cercanos a la realidad.
La maquina RAM proviene de Random Access Memory. Y es una maquina ideal muy similar a una computadora actual aunque con algunas simpli caciones. Los programas de la maquina RAM se almacenan en memoria (en la maquina de Turing los programas no se almacenan 1 . Puede suponerse que todos los accesos a memoria tienen el mismo costo (en tiempo) y que todas las instrucciones tienen un costo constante e identico (en tiempo). El set de instrucciones de la mquina RAM esta compuesto por la gran mayoria de las instrucciones que podemos encontrar en un lenguaje de alto nivel. Un programa para la maquina RAM se escribe en un pseudocodigo especial en el cual vamos a adoptar algunas convenciones basicas. Las llaves solo son necesarias si su ausencia afecta la claridad del codigo. Los pasajes de parametros a una funcion se hacen por valor. El acceso a un elemento de un arreglo cuesta lo mismo que el acceso a una variable. El algoritmo 2 tiene 5 instrucciones de las cuales se ejecutan unicamente 4. Por lo tanto el costo del programa en tiempo es de C 4, siendo C una constante que indica cuanto tiempo tarda en ejecutarse una instruccion. El espacio que necesita el programa es el espacio coupado por las variables A y B. Es decir 2 Ec siendo Ec el espacio que ocupa una variable.
1
13
Algorithm 1 Este es un ejemplo y ( 1 /* Asignacion A 2] ( 1 /* Asignacion a un elemento de un vector /* if-then-else if n < 1 then X (w else X (y end if /* ciclo while while n 0 do n (n;1 end while /* ciclo for for i = 0 to 10 do A i] ( 0 end for
Algorithm 2 Ejemplo simple a(3 b(a 5 if b == 5 then a(2 else a(1 end if
14
La mquina RAM de costo jo es mucho mas realista que la maquina de Turing ya que puede verse como, claramente, a partir de un algoritmo escrito para la maquina RAM podemos desarrollar un algoritmo escrito en C , Pascal u otro lenguaje para una computadora actual. Este acercamiento a la realidad en el codigo de la maquina RAM no se evidencia en cuanto al costo ya que el modelo planteado en el cual todas las instrucciones tienen un costo jo no es real ya que hay algunas instrucciones que son claramente mas costosas que otras, por ejemplo una multiplicacion insume mas tiempo que una suma en cualquier computadora.
En la maquina RAM de costo variable cada instruccion Ii tiene asociado un costo Ci que le es propio y que depende del costo de implementar dicha instruccion.
El costo del espacio en la mquina RAM de costo variable es igual al de la maquina RAM de costo jo. Claramente observamos que pese a ganar claridad en cuanto a la escritura de programas perdemos precision y se hace msa complejo el calculo de costos. Mas adelante veremos algunas herramientas que permitan simpli car el calculo de dichos costos. 15
Algorithm 4 Potencia(x,y) Calcula xy Require: x > 0 if y == 0 then else result ( x for I = 1 to y ; 1 do result ( result x end for end if
return result Cuanto cuesta el algoritmo en tiempo ?. La comparacion del if se ejecuta siempre, y supongamos que y no es cero (lo cual va a pasar la mayoria de las veces). Se ejecuta una asignacion y luego y ; 1 veces una multiplicacion. T = C 1 + C 2 + (y ; 1) (C 3 + C 1)
C1 = costo de comparar dos nmeros. C2 = costo de realizar una asignacion. C3 = costo de realizar una multiplicacion.
return 1
Observemos que el algoritmo no es e ciente ya que realiza demasiadas multiplicaciones, por ejemplo si queremos hacer 28 el algoritmo calcula. 28 = 2 2 2 2 2 2 2 2 Cuando podria hacer 16
A = 2 2(22 ) B = A A(24) C = B B (28 ) Con lo cual en lugar de 7 multiplicaciones se utilizan solamente tres. Podemos escribir entonces el siguiente algoritmo.
Algorithm 5 Potencia2(x,y) Calcula xy Require: x > 0 if y == 0 then return 1 end if r(1 if y > 1 then r ( Potencia2(x y=2) r(r r end if if y mod 2 == 1 then r(r x end if
return r Veamos que pasa con Potencia2(2,7)
r = 1 7 > 1 r = Potencia2(2,3) r2 = 1 3 > 1 r2 = Potencia2(2,1) r3 = 1 y <= 1 y % 2 = 1 r3 = 1*2 return 2 r2 = 2*2 y % 2 = 1 r2 = 4 * 2 return 8
17
r = 8 * 8 y % 2 = 1 r = 64 * 2 return 128
Como podemos ver el algoritmo es recursivo y en nuestro ejemplo para y = 7 realizo 5 multiplicaciones. Contra las seis que hariamos en el algoritmo tradicional. Para valores mas grandes de y la diferencia entre el primer y el segundo algoritmo se hace mucho mas notoria. Calcular el costo de nuestro nuevo algoritmo es uno de los temas a desarrollar en las proximas clases.
18
1.5 Ejercicios
1. Realizar en la maquina de Turing un programa que sume dos numeros binarios almacenados en memoria (suponerlos de igual longitud). 2. Realizar en la maquina de Turing un programa que reste dos numeros binarios almacenados en memoria (suponerlos de igual longitud). 3. Utilizar el segundo algoritmo de potencia para calcular 28, 29, 210, intentar deducir cuantas multiplicaciones realiza el algoritmo en funcion del numero y (Aproximadamente). Probar con mas valores si hace falta. 4. (Para pensar) Cual es la cantidad minimade multiplicaciones que podemos emplear para calcular X N , por ejemplo para X = 11 X = 23 X = 18 etc. En base a esto es optimo nuestro segundo algoritmo o puede existir un algoritmo mejor? 5. Escribir una version iterativa del algoritmo potencia2.
19
Chapter 2
Algoritmos iterativos
2.1 Algoritmos iterativos
Los algoritmos iterativos son aquellos que se basan en la ejecucion de ciclos que pueden ser de tipo for,while,repeat,etc.La gran mayoria de los algoritmos tienen alguna parte iterativa y muchos son puramente iterativos. Analizar el costo en tiempo de estos algoritmos implica entender cuantas veces se ejecuta cada una de las instrucciones del algoritmo y cual es el costo de cada una de las instrucciones.
Uno de los temas importantes del curso es el analisis de algoritmos de sort, por lo que vamos a empezar con uno de los mas simples: el burbujeo.
Algorithm 6 Burbujeo(A,n). Ordena el vector A. for i = 1 to n ; 1 do for j = i + 1 to n do if A j] < A i] then Swap(A i] A j ]) end if end for end for
Ejemplo. 3 3 1 1 1 4 4 4 4 4 1 1 3 3 3 5 5 5 5 5 2 2 2 2 2 7 7 7 7 7 6 6 6 6 6 4 4 4 4 4 Compara Compara Compara Compara Compara A A A A A 1] 1] 1] 1] 1] con con con con con A A A A A 2] 3] 4] 5] 6]
20
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
4 4 4 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
3 3 3 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4
2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 5 5 5 5 5 5 5 4 4 4 4
7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 5 5
6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 6
4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 6 7
Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara Compara FIN.
A A A A A A A A A A A A A A A A A A A A A A A A A
1] 1] 2] 2] 2] 2] 2] 2] 2] 3] 3] 3] 3] 3] 4] 4] 4] 4] 4] 5] 5] 5] 6] 6] 7]
con con con con con con con con con con con con con con con con con con con con con con con con con
A A A A A A A A A A A A A A A A A A A A A A A A A
7] 8] 3] 4] 5] 5] 6] 7] 8] 4] 5] 6] 7] 8] 5] 5] 6] 7] 8] 6] 7] 8] 7] 8] 8]
Para un vector de 8 elementos el algoritmo insumio 32 comparaciones. Antes de analizar en forma generica el costo en tiempo del burbujeo veamos ejemplos mas simples.
C1
21
Donde C1 es el costo de la instruccion que se ejecuta en el ciclo. El resultado de la sumatoria es n C 1, lo cual es evidente porque la instruccion dentro del ciclo se ejecuta n veces.
Ciclos anidados independientes for I = 1 to n do for J = 3 to m do Instruccion end for end for
Podemos calcular el costo nuevamente usando sumatorias de la forma:
n X m X i=1 j =3
C1 =
n X i=1
(m ; 2) C 1 = n (m ; 2) C 1
Ciclos anidados dependientes Cuando los ciclos dependen uno del otro
podemos aplicar la misma tecnica.
n X n X
= C1
i=1 j =i n X i=1
C1 =
n X i=1
(n ; i + 1) C 1 =
n X i=1
n ; i + 1 = C1 (
n;
n X i=1
i+
n X i=1
1) =
c=c n 22
n X
+ 1) i = n (n 2 i=1
n 1 X i=1
i = ln n + 1
for i = 1 to n ; 1 do for j = i + 1 to n do if A j] > A i] then Swap(A i] A j ]) end if end for end for
El costo del algoritmo lo vamos a calcular suponiendo que C1 es el costo de efectuar la comparacion y que C2 es el costo de efectuar el SWAP1 . Sin embargo el SWAP no se hace siempre sino que se hace unicamente cuando la comparacion da A j ] > A i], por ello debemos particionar el analisis en tres casos: el peor caso, el mejor caso y el caso medio.
Peor caso
En el peor caso el algoritmo siempre hace el Swap.Esto ocurre por ejemplo cuando el vector viene ordenado en el orden inverso al que utilizamos. T (n) = T (n) =
n ;1 X i=1 n ;1 n X X i=1 j =i+1
C1 + C2
(C 1 + C 2) (n ; 1 ; (i + 1) + 1)
n ;1 X i=1
T (n) =
(C 1 + C 2) (n ; 1 ; i)
n ;1 X
i=1 i=1 i=1 1 Para economizar espacio Swap(x,y) puede hacerse como x xor y y xor x x xor y. Ver para creer!
T (n) = (C 1 + C 2) (
n;
n ;1 X
1;
n ;1 X
i)
23
1) n ) T (n) = (C 1 + C 2) ((n ; 1) n ; (n ; 1) ; (n ; 2
Por ejemplo para N = 8 el peor caso nos da: (c1 + c2) (32 ; 12 + 1) = (c1 + c2) 21 un total de 20 comparaciones y 20 Swaps.
Mejor caso
En el mejor caso el vector ya viene ordenado por lo que nunca efectua ningn swap, el costo total es entonce el costo de las comparaciones que es igual a lo calculado antes. 2 ; 3 n + 1) T (n) = C 1 ( 1 n 2 2
Caso medio
En el caso medio el algoritmo efectua todas las comparaciones y solo la mitad de los Swaps. 3 1 1 2 3 2 T (n) = C 1 ( 1 2 n ; 2 n + 1) + C 2 2 ( 2 n ; 2 n + 1)
2.1.2 Insert-sort
Quienes hayan jugado alguna vez algun juego de cartas en el cual se juegue con mas de 5 o 6 cartas en la mano habran observado que algunos jugadores ponen sus cartas boca abajo en la mesa y luego van tomando de a una carta y la colocan en la posicion que le corresponde en la mano. Esto es precisamente lo que hace el algoritmo de sort por insercion. 2
24
Algorithm 7 InsertSort(A,n). Ordena un vector. for J = 2 to n do key ( A j ] /* Insertar A j] en la secuencia ya ordenada A 1..j-1] */ i (j ;1 while i > 0 and A i] < key do A i + 1] ( A i] i (i;1 end while A i + 1] ( key end for
Veamos el codigo.
Ejemplo. 3 4 1 5 2 7 6 4 3 4 1 5 2 7 6 4 A 3] A 2] 3 4 4 5 2 7 6 4 i=1 A 2] A 1] 3 3 4 5 2 7 6 4 i=0 sale del while A 1]1 1 3 4 5 2 7 6 4 1 3 4 5 2 7 6 4 A 5] A 4] 1 3 4 5 5 7 6 4 i=3 A 4] A 3] 1 3 4 4 5 7 6 4 i=2 A 3]A 2] 1 3 3 4 5 7 6 4 i=1 sale del while A 2] 2 1 2 3 4 5 7 6 4 1 2 3 4 5 7 6 4 A 7] A 6] 1 2 3 4 5 7 7 4 i=5 j=2, key=4, i=1 j=3, key=1, i=2 no entra al while. A 2]=4. entra al while.
2 En particular suelo jugar al bridge, y los jugadores que hacen esto tardan siglos en acomodar sus cartas. Evidentemente no conocen mejores metodos de sort!
25
sale del while A 6] 6 1 2 3 4 5 6 7 4 j=8,key=4,i=7 entra al while A 8] A 7] 1 2 3 4 5 6 7 7 i=6 A 7] A 6] 1 2 3 4 5 6 6 7 i=5 A 6] A 5] 1 2 3 4 5 5 6 7 i=4 sale del while A 5] 4 1 2 3 4 4 5 6 7 fin.
Como podemos observar el algoritmo trabaja mejor si el vector ya esta ordenado en cuyo caso el ciclo while no se ejecuta. En el peor caso por el contrario el ciclo while se ejecuta j ; 1 veces. Entonces podemos plantear.
Peor caso
Sea C1= Costo de asignar a una variable un elemento de un vector. C2= Costo de asignar a una variable el valor de otra menos 1. C3= Costo de asignar a un elemento de un vector el valor de otro elemento de un vector. C4= Costo de decrementar una variable. C5= Costo de comparar un elemento de un vector con un numero C6= Costo de comparar dos numeros.
T (n) = T (n) =
n X
j =2 n X j =2
(C 1 + C 2 + C 5 + C 6 +
j 1 X
;
i=1
(C 3 + C 4) + C 3)
(C 1 + C 2 + C 5 + C 6 + (C 3 + C 4)(j ; 1) + C 3)
n X j =2 n X j =2
T (n) = N C 1 + N C 2 + N C 5 + N C 6 + (C 3 + C 4)
(j ; 1) + N C 3 j;
n X j =2
T (n) = N (C 1 + C 2 + C 5 + C 6 + C 3) + (C 3 + C 4) (
1)
1) n ; (n ; 1)) T (n) = N (C 1 + C 2 + C 5 + C 6 + C 3) + (C 3 + C 4) ( (n ; 2 26
1 n2 ; 3 n + 1) T (n) = N (C 1 + C 2 + C 5 + C 6 + C 3) + (C 3 + C 4) ( 2 2 Como podemos ver las formulas se van haciendo mas complejas debido a que no podemos eliminar las constantes C1..CN que nos impiden calcular con mayor simplicidad el tiempo del algoritmo.
Mejor caso
En el mejor caso el ciclo while no se ejecuta, notemos que para el mejor caso el algoritmo de insercion es mejor que el burbujeo ya aquel de todas formas tiene que efectuar todas las comparaciones. T (n) =
n X j =2
(C 1 + C 2 + C 5 + C 6 + C 3)
T (n) = N (C 1 + C 2 + C 5 + C 6 + C 3)
Caso medio
Caso medio.En el caso medio el ciclo while se ejecuta (j ; 1)=2 veces por lo que la formula es muy similar a la del peor caso aunque dividiendo por dos los terminos que multiplican a C 3 y C 4.
2.3.1
O g n
( ( )) :
Sea una funcin f (n), decimos que f (n) pertenece a la familia de funciones O(g(n)) si existe una constante C 1 y un numero n0 tales que. 0 f (n) C 1 g(n) para n no Es decir que a partir de un cierto n la funcin f (n) queda por debajo de la funcion g(n) multiplicada por una constante.
2.3.2
(g (n)) :
Sea una funcion f (n), decimos que f (n) pertenece a la familia de funciones (g(n)) si existe una constante C 1 y un numero no tales que. C 1 g(n) f (n) para n no Es decir que a partir de un cierto punto la funcion siempre es mayor que g(n) multiplicada por una constante.
2.3.3
(g (n)) :
Sea una funcion f (n), decimos que f (n) pertenece a la familia de funciones (g(n)) si existen constantes C 1 y C 2 y un numero no tales que. C 1 g(n) f (n) C 2 g(n) para n no Es decir que a partir de un cierto numero la funcion queda atrapada entre C 1 g(n) y C 2 g(n).
28
Esta notacion que pretende clasi car el crecimiento de las funciones es apropiada para el estudio de los algoritmos ya que nos permite realizar algunas simpli caciones. Por ejemplo: C 1 f (n) = (f (n)) Ademas los sumandos de orden inferior se pueden obviar. O(f (n)+ O(g(n)) = O(f (n)) sii f(n) crece mas rapido que g(n). Por ejemplo: n2 + n 2 (n2 ) La notacion (g(n)) sirve para acotar el mejor caso de un algoritmo. La notacion O(g(n)) sirve para acotar el peor caso de un algoritmo. Si (f (n)) = O(f (n)) entonces podemos decir que el algoritmo pertenece a (f (n)) Cuando (g(n)) es distinto a O(g(n)) debemos usar O(g(n)) para expresar el caso medio. Como un abuso de notacion usaremos. f (n) = (g(n)) f (n) = O(g(n)) f (n) = (g(n)) Donde el signo igual debe leerse como \pertenece". Aplicando esta notacion a los algoritmos estudiados podemos ver que el mejor caso del burbujeo es (n2 ). El peor caso es O(n2) por lo tanto el algoritmo de burbujeo es (n2 ). Es decir que es un algoritmo de orden cuadratico. El sort por insercion en cambio en el mejor caso es (n) y en el peor caso es O(n2 ). En el caso medio el algoritmo es O(n2). Por lo que tambien tenemos un algoritmo de orden cuadratico pero que en el mejor de los casos es lineal. Cuanto mas rapido sea el crecimiento de una funcion peor ser el rendimiento del algoritmo. Por eso analizando el orden del algoritmo podemos determinar cual es mas e ciente. Algunas funciones tipicas ordenadas por orden de crecimiento.
Luego de analizar el orden del algoritmo de insercion y el burbujeo podemos llegar a la conclusion de que ambos son iguales en e ciencia, aunque como hemos visto trabajan en forma completamente distinta. 29
La solucion iterativa a este problema es simple, se recorre el vector de puntos y por cada ppunto se lo compara contra todos los demas calculando la distancia como (x1 ; x2)2 + (y1 ; y2)2 , cada vez que una distancia sea inferior al minimo anterior actualizamos cuales son los puntos mas cercanos. Cuando ya comparamos todos los puntos contra todos el resultado es un par de puntos cuya distancia seguro que es minima.
2.4.2 Analisis
n Claramente el tiempo de este algoritmo se puede calcular como n i=1 j =1 C Siendo C un cierto tiempo que es constante y que es lo que se tarda dentro del ciclo en hacer la comparacion entre i y j , en calcular la distancia, realizar la nueva comparacion y eventualmente a min la nueva distancia. El P C n (nasignar +1) resultado de la sumatoria es n = C n (n+1) = C (n2 +n) = O(n2) i=1 2 Como ademas es evidente que tanto en el mejor como en el peor caso el algoritmo compara todos los puntos contra todos podemos decir que el algoritmo es (n2 ).
P P
En general los algoritmos iterativos son sencillos y no muy e cientes, como punto de partida o como referencia de comparacion siempre es bueno tener 30
Algorithm 8 Distmin(A,n). Dado un arreglo (A) de n puntos calcula la distancia minima entre dos puntos cualesquiera min ( +MAX for I = 1 to n do for J = 1 to n do if i 6= j then dist ( Distancia(A j ] A i]) if dist < min then min ( dist soli ( i solj ( j
return (A soli] A solj ]) presente la solucion iterativa a un problema. Mas adelante veremos como conociendo otras tecnicas de disenio de algoritmos vamos a poder mejorar el orden de este algoritmo para hacerlo mucho mas e ciente.
2.6 Ejercicios
1. 2. 3. 4. Analizar el orden del sort por seleccion. Analizar la version iterativa del algoritmo Potencia2. Sugerir una mejora al algoritmo de burbujeo y analizar el nuevo algoritmo. Implementar el algoritmo de burbujeo y el insert-sort. Probarlos para conjuntos de datos generados al azar y luego para conjuntos de datos que ya estn ordenados. Realizar una tabla que indique el tamanio de los datos y el tiempo que insumio cada algoritmo en los distintos casos. Probar con conjuntos de datos de distintos tamanios. 5. Listar las siguientes funciones en orden de crecimiento creciente. Indicar cual es el orden de cada una de las funciones. (a) n (b) 2n (c) nk con k > 5 (d) n log n (e) 192 (f) n ; n3 + 7n5 (g) log n (h) pn (i) log log n (j) 1 (k) n3 (l) (log n)2 (m) n! (n) n( 1 + epsilon) con 0 < epsilon < 1
la introduccion. La notacion \Big-Oh" se encuentra de nida en el texto de la misma forma en que la de nimos aqui. El libro tiene una seccion sobre como resolver sumatorias que puede resultar de utilidad.
32
Chapter 3
Algoritmos Recursivos
3.1 Algoritmos recursivos
Una tecnica bastante poderosa a la hora de realizar algoritmos consiste en programar en forma recursiva. Un algoritmo recursivo es aquel que en algun momento durante su ejecucion se invoca a si mismo. Por ejemplo el siguiente programa sirve para calcular el factorial de un numero. En general el programar
Algorithm 9 Fact(n). Calcula el factorial de n en forma recursiva. if n < 2 then return 1 else return n*Fact(n-1) end if
en forma recursiva permite escribir menos codigo y algunas veces nos permite simpli car un problema dificil de solucionar en forma iterativa. En cuanto a la e ciencia un programa recursivo puede ser mas, menos o igual de e ciente que un programa iterativo. En primer lugar hay que senialar que los programas recursivos consumen espacio en memoria para la pila ya que cada una de las llamadas recursivas al algoritmo son apiladas una sobre otras para preservar el valor de las variables locales y los parametros utilizados en cada invocacion. Este aspecto lo vamos a considerar un costo en espacio ya que lo que estamos consumiendo es memoria. En cuanto al tiempo de ejecucion del programa debemos calcularlo de alguna forma para poder obtener el orden del algoritmo. Para ello se recurre a plantear el tiempo que insume un programa recursivo de la siguiente forma.
1 si n < 2 1 + T (n ; 1) sino 33
Las ecuaciones planteadas de nen una recurrencia. Una recurrencia es un sistema de ecuaciones en donde una o mas de las ecuaciones del sistema estan de nidas en funcion de si mismas. Para calcular el orden de un algoritmo recursivo es necesario resolver la recurrencia que de ne el algoritmo.
3.2 Recurrencias
Tomemos por ejemplo la siguiente recurrencia muy comun en algoritmos recursivos. si n = 1 T (n) = 1 2T ( n ) + n sino 2 Estudiaremos tres tecnicas que suelen utilizarse para resolver recurrencias. El metodo de sustitucion El metodo iterativo El teorema maestro de las recurrencias.
En metodo de sustitucion se utiliza cuando estamos en condiciones de suponer que el resultado de la recurrencia es conocido. En estos casos lo que hacemos es sustituir la solucion en la recurrencia y luego demostrar que el resultado es valido por induccion. Para nuestro ejemplo sabemos que T (n) = (n log n) + n Veamos que pasa cuando n = 1 Si n = 1 ) T (1) = 1 1 log1 + 1 = 0 + 1 = 1 V erifica Hipotesis inductiva si n > 1 ) T (n ) = (n log n ) + n si n < n Demostracion
0 0 0 0 0
T (n) = 2T ( n 2 ) + n (1) 34
n n T (n) = 2(( n 2 ) log ( 2 ) + ( 2 )) + n (n log n 2 + n) + n n log n ; log2 + 2n n log n ; n + 2n n log n + n = (n log n) Por lo que queda demostrado. El metodo de sustitucion es simple pero requiere que uno ya conozca cual es la solucion de la recurrencia. A veces sin embargo en recurrencias muy complejas para resolver por otros metodos puede llegar a ser conveniente intentar adivinar la solucion y luego demostrarla usando este metodo.
El metodo iterativo es un forma bastante poderosa de resolver una recurrencia sin conocer previamente el resultado. En este metodo se itera sobre la recurrencia hasta que se deduce un cierto patron que permite escribir la recurrencia usando sumatorias. Luego realizando una sustitucion apropiada y resolviendo las sumatorias se llega al resultado de la recurrencia. Utilizando el metodo iterativo en nuestro ejemplo observamos lo siguiente. T (1) = 1 T (n) = 2T ( n 2) +n n n T (n) = 2(2T ( n 4 ) + 2 ) + n = 4T ( 4 ) + n + n n n T (n) = 4 2(T ( n 8 ) + 4 ) + n) + n = 8T ( 8 ) + n + n + n n ) + kn T (n) = 2k T ( 2 k 35
Como T (1) = 1 hacemos 2nk = 1 ) n = 2k ) k = log n Reemplazando n ) + n log n T (n) = 2log n T ( 2log n log n T (n) = 2 T (1) + n log n T (n) = n + n log n T (n) = (n log n) Como podemos ver el metodo iterativo requiere de varios calculos y puede llegar a requerir de ciertas manipulaciones algebraicas que resultan molestas pero en general puede aplicarse practicamente a cualquier tipo de recurrencia con resultado favorable.1
Mediante este teorema podemos contar con una poderosa herramienta para resolver algunas recurrencias. El teorema dice lo siguiente.
k T (n) = aT ( n b)+n Si a 1 y b > 1 Entonces Caso 1 Si a > bk ) T (n) 2 (nlogb a ) Caso 2 Si a = bk ) T (n) 2 (nk log n) Caso 3 Si a < bk ) T (n) 2 (nk ) Por ejemplo para T (n) = 2T ( n 2 ) + n tenemos que a = 2, b = 2, k = 1 luego a = bk y estamos en el caso 2. Por lo que el algoritmo es (n1 log n)
Lamentablemente no todas las recurrencias tienen la forma que requiere el teorema maestro por lo que a veces no queda mas remedio que aplicar el metodo iterativo.
Ademas de los tres metodos nombrados que son los basicos en cuanto a la resolucion de recurrencias hay otras herramientas que pueden resultar utiles al resolver recurrencias.
1 Algunas recurrencias en particular no son apropiadas para el metodo iterativo como veremos luego
36
En algunas recurrencias puede ser conveniente realizar un cambio de variables de forma tal de resolver una recurrencia ya conocida, luego aplicando la inversa del cambio podemos obtener la solucion de la recurrencia original. Supongamos que tenemos la siguiente recurrencia: si n = 1 p T (n) = 1 T ( (n)) + 1 sino Sea m log n. Entonces n = 2m . Reemplazando 2m por n en la recurrencia de T (n) tenemos: T (2m ) = T (2m=2 ) + 1 Llamamos S (m) = T (2m ). Entonces la recurrencia queda: S (m) = S (m=2) + 1 Usando el teorema maestro sabemos que S (m) = (log m). Por lo tanto T (n) = T (2m ) = S (m) = (lgm) T (n) = (log log n)
Cambio de variables
El metodo de los multiplicadores permite resolver recurrencias particularmente di ciles en donde podemos suponer que la forma de la solucion se ajusta a T (n) = n . Esto es valido en recurrencias de la forma T (n) = a1T (n ; x1) + a2T (n ; x2) + : : : + amT (n ; xm) Sea la recurrencia: 80 9 si n = 0 = < T (n) = : 4 si n = 1 8T (n ; 1) ; 15T (n ; 2) sino Suponiendo que T (n) = n reemplazamos:
n = 8 n;1 ; 15 n;2 n;2(
2
Como la recurrencia es lineal podemos plantear la forma: T (n) = c0 0n + c15n + c2 3n Como c0 es 0 podemos calcular c1 y c2 de T (0) y T (1). T (0) = c1 50 + c2 30 37
n;2(
; 8 + 15) = 0 ; 5)( ; 3) = 0
0 = c1 + c2 T (1) = c1 51 + c2 31 4 = 5c1 + 3c2 Resolviendo el sistema de 2x2 obtenemos c1 = 2 y c2 = ;2 por lo que la forma nal de la recurrencia es: f (n) = 2 5n ; 2 3n
3.3 Ejemplos
Sea la siguiente recurrencia (Messier) si n = 1 T (n) = 1 ) + n sino 3T ( n 4 De acuerdo al teorema maestro a = 3, b = 4, k = 1. a < bk . Por lo tanto se aplica el caso 3. (n) Veri quemos el resultado del teorema maestro usando el metodo iterativo. T (n) = 3T ( n 4)+n n ) + n ) + n = 9T ( n ) + 3( n ) + n T (n) = 3(3T ( 16 4 16 4 n ) + n ) + 3( n ) + n = 27T ( n ) + 9( n ) + 3( n ) + n T (n) = 9(3T ( 64 16 4 64 16 4 n n k k 1 T (n) = 3 T ( 4k ) + 3 ( 4k 1 ) + : : : + 1 i X n )+k 3n T (n) = 3k T ( 4 k i i=0 4
; ; ;
T (1) +
log4 n;1
T (n) = nlog4 3 + 38
X 3i 4i n X 3i 4i n
T (n) = n
log4 3
+n
log4 n;1
;1 ( ) T (n) = nlog4 3 + n ( ) ; 1 log4 n = nlog4 3 4 = nlog4 3 log4 4 ) (3 4) log4 3 )= nlog4 3 1 = n n n log4 3 ; 1 T (n) = nlog4 3 + n ( n 3) ; 1 ) (4 3;1 T (n) = nlog4 3 + n log4 1
; ;
i=o 3 log4 n 4 3 4
X 3i (4)
;4
(3.1)
Como vemos la solucion nal tiene un termino que es (n) y otro que es (nk ), cuando k < 1 ocurre que la funcion lineal crece mas rapido que la exponencial y por eso la recurrencia pertenece a (n).
3.3.2 Ejemplo II
Esta recurrencia tambien es bastante comun en algunos algoritmos recursivos y lamentablemente no podemos escribirla en la forma en la cual vale el teorema maestro. Como queremos averiguar la solucion tenemos que aplicar el metodo iterativo. T (n) = 2T ( n 2 ) + n log n n n T (n) = 2T (2T ( n 4 ) + ( 2 ) log 2 ) + n log n n + n log n T (n) = 4T ( n ) + n log 4 2 n ) log n ) + n log n + n log n ) + ( T (n) = 4(2T ( n 8 4 4 2 39
Como T (1) = 1 ) 2nk = 1 ) k = log n Reemplazando T (n) = 2log nT (1) + n T (n) = n + n T (n) = n + n(
log n;1 log n;1 log n;1
X
i=0
X
i=0
n log 2 i
log n ; log 2i
log n;1
X
i=0
log n ;
X
i=0
log 2i) i)
T (n) = n + n(log2 n ;
log n;1
X
i=0
n ; 1) )) T (n) = n + n(log2 n ; ( log n(log 2 2 log n ; log n )) 2 T (n) = n + n(log n ; ( 2 2 log n ) T (n) = n + n( log n ; 2 1 2 T (n) = n + 2 n log n ; 1 2 n log n T (n) = (n log2 n) En este caso el termino de mayor crecimiento es n log2 n.
Algorithm 10 Fib1(n). Calcula el `n'esimo numero de la serie de Fibonacci. if n < 3 then return 1 else return Fib(n ; 1) + Fib(n ; 2) end if
Para calcular el costo de este algoritmo planteamos la siguiente recurrencia. 1 si n < 3 T (n) = T (n ; 1) + T (n ; 2) sino T (n) = T (n ; 1) + T (n ; 2) T (n) = T (n ; 2) + T (n ; 3) + T (n ; 3) + T (n ; 4) = T (n ; 2) + 2T (n ; 3) + T (n ; 4) T (n) = T (n ; 3) + T (n ; 4) + 2T (n ; 4) + 2T (n ; 5) + T (n ; 5) + T (n ; 6) T (n) = T (n ; 3) + 3T (n ; 4) + 3T (n ; 5) + T (n ; 6) T (n) = T (n ; 4) + 4T (n ; 5) + 6T (n ; 6) + 4T (n ; 7) + T (n ; 8) .. . La expansion de la recurrencia tiene la forma del binomio de newton. Y no parece haber una sumatoria conocida que nos permita expresar la recurrencia de forma tal de poder resolverla. En estos casos en los cuales la recurrencia resulta di cil de resolver por los metodos vistos puede ser util utilizar un arbol para visualizar como funciona el algoritmo. Aplicando el metodo iterativo obtenemos lo siguiente.
41
Como vemos en la gura para calcular Fib(5), necesitamos hacer Fib(4) y Fib(3), luego para Fib(4) necesitamos Fib(3) y Fib(2). Etc: : : . Los nodos internos del arbol insumen dos instrucciones (la comparacion y la suma), mientras que las hojas del arbol solo insumen una instruccion. Por lo tanto la cantidad de instrucciones que se ejecutan para Fib(n) es. T (n) = 2 nodos + hojas Observando el arbol podemos ver que cada nodo indica la cantidad de hojas debajo de dicho nodo, por eso para calcular Fib(n) necesitamos un arbol con Fib(n) hojas. Ademas podemos usar una formula util para arboles binarios que indica que la cantidad de nodos internos es igual a la cantidad de hojas menos 1. (Esto se puede probar facilmente por induccion). Entonces. T (n) = 2(Fib(n) ; 1) + Fib(n) T (n) = 3(Fib(n)) ; 2 T (n) = (Fib(n)) Como se puede ver obtenemos que el orden de nuestro algoritmo es el orden de la serie de Fibonacci. Para saber cual es el orden de la serie de Fibonacci podemos usar la siguiente propiedad. Fib(n) = n p Con = 52; 1 = 0 61803 : : : Entonces. T (n) = ( n ) = (xn) Como podemos ver el algoritmo pertenece al orden exponencial, por lo tanto es bastante lento. Para n = 45 por ejemplo ejecuta mil millones de instrucciones!. Una forma de ver que nuestro algoritmo es ine ciente es observando como para calcular F (5) calculamos varias veces F (1),F (2),etc. Cuando en realidad solo necesitariamos hacerlo una vez. En este caso el algoritmo recursivo es claramente ine ciente por lo que planteamos una version iterativa.
Para la solucion iterativa usamos un vector de n + 1 posiciones e inicializamos v 1] = 1,v 2] = 1. Luego con un ciclo simple calculamos cada posicion del vector como la suma de las dos anteriores hasta llegar a la posicion n que es la que buscamos. 42
end for
return Fib n] Como podemos ver claramente este algoritmo es (n) lo cual implica una gran mejora con respecto a la version recursiva. La tecnica que usamos es sencilla, pero tiene el problema de requerir de un vector de tamanio n + 1 para poder funcionar. Es decir que hemos reducido el costo en tiempo pero hemos aumentado el costo en espacio. En nuestra proxima version tratamos de aplicar la misma tecnica pero sin utilizar el vector.
Algorithm 12 Fib3(n). Calcula el n'esimo numero de Fibonacci a(1 b(1 for i = 3 to n do c (a+b a(b b(c end for
return a Esta nueva version sigue siendo (n) pero en lugar de utilizar un vector de n+1 posiciones usamos tres variables a,b,c. El algoritmo insume mas instrucciones que la version que usaba el vector pero sigue siendo lineal por lo que asintoticamente es equivalente a Fib2 pero con un costo espacial mucho menor.
Una de las curiosidades de la serie de Fibonacci es que existen varias formas curiosas de calcular un numero de Fibonacci. Una de las formas es sumando los dos numeros anteriores de la serie que es el metodo que estuvimos usando hasta ahora tanto en la version recursiva como en la version iterativa. Otra forma es calculando n . Esta forma tiene la di cultad de que , el numero aureo, es irracional lo cual nos obligaria a calcular Fib(n) en forma aproximada. Otra forma de calcular Fib(n) utilizando potencias es usando la siguiente matriz. 1 M= 1 1 0 43
Para utilizar este nuevo metodo tenemos que disponer de un algoritmo veloz para elevar una matriz a una potencia. En la primera clase del curso habiamos obtenido el siguiente algoritmo para elevar un numero a una potencia.
Algorithm 13 Potencia2(x,y) Calcula xy Require: x > 0 if y == 0 then return 1 end if r(1 if y > 1 then r ( Potencia2(x y=2) r(r r end if if y mod 2 == 1 then r(r x end if
return r Claramente el algoritmo se puede adpatar facilmente para elevar una matriz en lugar de un numero a cualquier potencia. Este es un algoritmo recursivo de nido por la recurrencia. T (n) = 1 si n = 0 f T (n) = T(n ) + 1 sino 2 No hace falta tener en cuenta para expresar la recurrencia que el costo di ere agregando una multiplicacion o no segun n sea par o impar. Aplicando el teorema maestro (caso 2). Tenemos que T (n) = (log n). Por lo tanto sabemos que disponemos de un algoritmo que calcula una potencia de orden logaritmico (que crece menos que una funcion lineal). Por lo que podemos mejorar el calculo de los numeros de Fibonacci de la siguiente forma.
3.4.4 Conclusiones
Como hemos visto calcular un numero de Fibonacci no es tan sencillo como parece, la version recursiva del algoritmo era de orden exponencial, luego pudimos construir una version iterativa de orden lineal y nalmente una version de orden logaritmico. En general se requiere de mucho analisis, esfuerzo e investigacion para encontrar la forma mas e ciente de resolver un determinado problema.
3.6 Ejercicios
1. Demostrar usando el metodo de sustitucion que la recurrencia 1 si n = 1 T (n) = T ( n ) + T 3n ) + n ( sino 5 4 Es (n) 2. Resolver las siguientes recurrencia usando el metodo iterativo y aplicando el teorema maestro cuando corresponda. Para todas las recurrencias vale que T (n) = 1 si n = 1. 2 (a) T (n) = 3T ( n 2) + n 45
46
Chapter 4
47
Algorithm 15 MergeSort(A,p,r). Ordena un vector desde p hasta r. if p < r then q ( (p + r)=2 end if
MergeSort(A,p,q) MergeSort(A,q+1,r) Merge(A,p,q,r)
El tiempo que insume este algoritmo queda de nido por la siguiente recurrencia. si n = 1 T (n) = 1 ) + ( Merge ) sino 2T ( n 2 Para calcular la recurrencia necesitamos el algoritmo que realiza el merge.
merge requiere de dos pasadas a los vectores a mergear, una para realizar el merge y otra para copiar del vector auxiliar al vector original. El tiempo del algoritmo de merge es T (n) = 2n por lo tanto.
. Por lo que la recurrencia del algoritmo de MergeSort es: si n = 1 T (n) = 1 2T ( n ) + n sino 2 Esta es una recurrencia conocida. Aplicando el teorema maestro podemos obtener que MergeSort = (n log n) Lo cual mejora el orden de los algoritmos de sort que habiamos estudiado que es cuadratico. 48
else B k] ( A j ] j (j +1 end if k(k+1 end while /* Copiamos elementos que quedaban de la parte izquierda */ while i q do B k ] ( A i] i (i+1 k(k+1 end while /* Copiamos elementos que quedaban de la parte derecha */ while j r do B k] ( A j ] j (j+1 k(k+1 end while/* Copiamos desde el vector auxiliar */ for i = p to r do A i] ( B i] end for
49
4.2.1 Propiedades
El algoritmode MergeSort es un algoritmode sort que presenta algunas propiedades interesantes. En primer lugar el orden del algoritmo es (n log n), y esto ocurre tanto en el peor caso, como en el mejor caso, como en el caso medio ya que el tiempo que insume el algoritmo no depende de la disposicion inicial de los elementos del vector. En segundo lugar es un algoritmo que requiere de un espacio extra de almacenamiento para poder funcionar. A los algoritmos que realizan el sort dentro del vector mismo se los denomina algoritmos de sort \in-situ". Claramente el algoritmo de MergeSort no pertenece a esta familia.
El orden del algoritmo de MergeSort no se puede mejorar, lo que si podemos hacer es aplicar algunas mejorar para que los factores constantes que inciden en el tiempo que insume el algoritmosean menores. Uno de los trucos mas utilizados consiste en evitar el tener que copiar todo el vector auxiliar en el original cada vez que se hace el procedimiento de Merge. Esto se logra utilizando dos vectores de tamanio `n'. En los niveles pares de la recursion Mergeamos desde A hacia B. En los niveles impares mergeamos desde B hacia A. Si la cantidad de niveles es en total impar hacemos una copia de mas para volver a tener los datos en el vector original pero lo que se ahorra es siempre mas de este gasto extra. Otra mejora habitual consiste en no aplicar MergeSort si el tamanio del vector es mas chico que un numero constante de elementos, por ejemplo 20, en ese caso se aplica un sort por insercion o cualquier otro. Como la cantidad de elementos es constante esto solo agrega una cantidad constante de tiempo al algoritmo y en general permite obtener una mejora en el rendimiento. Un detalle importante del algoritmo de MergeSort es el siguiente: En el proceso de Merge debido a comparamos A i] A j ] tenemos como consecuencia que cuando los elementos son iguales copiamos desde la lista izquierda primero y luego desde la derecha. Podriamos haber copiado desde la derecha primero y el resultado seria el mismo ya que si los numeros o lo que sea que estamos ordenando son iguales o tienen la misma clave no importa en que orden aparecen en el vector ordenado. Sin embargo frecuentemente ordenamos un conjunto de datos por mas de una clave, si los elementos del vector ya estaban ordenados por alguna otra clave es importante que al ordenarlo por otra clave cuando la nueva clave es igual mantengamos el orden de la clave anterior. Esto se logra precisamente copiando primero de la parte izquierda del vector al mergear. A los algoritmos de sort que preservan el orden original de los elementos que tienen claves iguales se los denomina estables. La estabilidad es una propiedad bastante deseable en los algoritmos de sort. Por ultimo es conveniente analizar si es posible implementar el algoritmo de MergeSort `in-situ', es decir sin utilizar espacio auxiliar de memoria. La 50
respuesta es que si es posible pero nadie sabe como hacerlo sin destruir por completo la e ciencia del algoritmo.
T (n) = (n log n). Mejor, peor y caso medio son iguales. Es estable. Requiere de un vector auxiliar para realizar el sort.
4.3.1
C ribas : : :
Hemos introducido este problema debido a que presenta un caso muy particular dentro de los algoritmos de tipo Divide and Conquer. En los algoritmos Divide and Conquer dividimos el problema en m problemas mas chicos para luego resolverlos y combinar las soluciones de ambos. Una criba se presenta cuando m = 1. Las cribas se aplican a problemas en los cuales estamos interesados en obtener un elemento determinado dentro de un conjunto de elementos de tamanio n. Este tipo de algoritmos trabaja eliminando un cierto conjunto de elementos del conjunto sabiendo que ninguno de ellos es el elemento buscado, el algoritmo se aplica recursivamente hasta que queda un unico elemento en el conjunto: el elemento buscado. 51
El algoritmo que vamos a describir funciona de la siguiente manera. Recibe un vector A,dos numeros p y r que indican desde donde hasta donde buscar en el vector (inicialmente p = 1 y r = n) y un numero k que es el rango que queremos encontrar en el vector. El algoritmo selecciona un elemento del vector que se denomina pivot (p), decidir cual es el pivot es importante, pero por el momento supongamos que el pivot es elegido de alguna manera. Luego particiona el vector en tres partes: A p..q-1] contiene todos los elementos menores que el pivot. A q] es el pivot. A q+1..r] contiene todos los elementos mayores que el pivot. Dentro de los vectores A p..q-1] y A q+1..p] los elementos pueden estar en cualquier orden. El ranking del pivot en cualquier fase del algoritmo es q ; p + 1, si k = q ; p + 1 entonces el pivot es el elemento buscado. Si k < q ; p + 1 aplicamos el algoritmo recursivamente a A p::q ; 1] si k > q ; p + 1 aplicamos el algoritmo recursivamente a A q +1::p], esto queda ejempli cado en la ilustracion.
Notemos que el algoritmo satisface los principios que enunciabamos sobre una criba, elimina ciertos elementos y luego se invoca recursivamente con el resto. Cuando el pivot es el buscado tenemos suerte y eliminamos todos los demas elementos, sino seguimos buscando. Tenemos aun que resolver el problema de 52
Algorithm 17 Seleccion1(A,p,r,k). Selecciona de A p..r] el `k'esimo elemento if p = r then return A p] else x ( ElegirPivot(A p r) q ( Particionar(A p r x) xrank ( q ; p + q if k = xrank then return x else if k < xrank then return Seleccion1(A,p,q-1,k) else return Seleccion1(A,q+1,r,k-q) end if end if end if
seleccionar el pivote y ademas el de particionar el vector. Supongamos por el momento que ambas cosas pueden hacerse en (n). Nos queda por averiguar que tan e ciente es nuestra criba, es decir cuantos elementos eliminamos cada vez. Si el pivote es el elemento mas chico o mas grande del vector eliminamos solamente un elemento, el mejor caso ocurre cuando el pivot es un elemento muy cercano al medio en cuyo caso eliminamos la mitad del vector y podemos escribir la siguiente recurrencia. si n = 1 T (n) = 1 ) + n sino T(n 2 Resolviendo por el teorema mestro tenemos que el algoritmo es (n), es decir que en el mejor caso insume una cantidad de tiempo lineal. Si el pivot que elegimos es malo de todas formas el algoritmo se va a mantener dentro del orden lineal, por ejemplo. T (n) = 1 si n = 1 n T (n) = T ( 99 100 ) + n sino Tambien da como resultado (n), por lo tanto podemos decir que el algoritmo es (n)
Como vimos aun eligiendo el pivote malamente el algoritmo tiende a ser lineal, sin embargo cuanto mejor elijamos el pivote mejor va a funcionar. Otra de las cosas que sabemos es que el pivote debe ser elegido en (n) para que valga la recurrencia que habiamos planteado anteriormente. 53
el algoritmo de seleccion consiste en encontrar un pivote de forma tal que podamos asegurar que el pivote seleccionado siempre funcionara relativamente bien. Floyd, Tarjan, Pratt y Rivest encontraron un algoritmo que garantiza que n=4 q 3n=4, lo cual es mas que su ciente para aseverar la validez del pivote.
El algoritmo de Floyd-Pratt-Rivest-Tarjan Primer paso: dividir en grupos de 5 En primer lugar se particiona el vector en grupos de 5 elementos. Por ejemplo A 1..5],A 6..10],A 11..15],etc. La cantidad de grupos es m = d n 5 e Todos los grupos tendran 5 elementos salvo el
ultimo que puede tener menos. Esto puede hacerse facilmente en tiempo (n).
se computa la mediana de cada grupo. Como todos los grupos tienen tamanio constante=5 no necesitamos un algoritmo e ciente para encontrar la mediana, podemos aplicar burbujeo a cada grupo y luego elegir el elemento del medio de cada vector ordenado. Esto tarda una cantidad de tiempo constante (1) y se repite m = d n 5 e veces, por lo tanto este paso insume tiempo (n). Una vez calculadas las m medianas se las copia a un vector auxiliar B . en hallar la mediana de las m medianas anteriores, para esto tenemos que llamar al algoritmo Seleccion1 en forma recursiva de la forma Seleccion1(B 1 m k) m+1 donde m = d n 5 e y k = b 2 c. El elemento resultante: la mediana de medianas es el que se elige como pivot. Debemos demostrar ahora que el pivot no deja menos de n/4 elementos a izquierda o derecha de el.
54
para calcular el pivote sabemos que por lo menos eliminamos a 1=4 de los elementos en cada llamada, sin embargo tenemos que recalcular el orden del algoritmo. La llamada recursiva a Seleccion1(: : :) se hace para no mas de 3n/4 elementos, pero para lograr esto dentro de ElegirPivote tenemos que volver a llamar a Seleccion1(: : :) para el vector B (el que tiene las medianas de los grupos de 5) y sabemos que B tiene n=5 elementos. Todo lo demas como hemos mostrado se hace en (n). El tiempo que insume el algoritmolo podemos calcular resolviendo la siguiente recurrencia. si n = 1 T (n) = 1 T (n=5) + T (3n=4) + n sino Esta es una recurrencia realmente rara que involucra una mezcla de fracciones (n/5) y (3n/4). Por lo que resulta imposible aplicar el teorema maestro, el metodo iterativo es tambien de resolucion compleja por lo que sabiendo que pretendemos que el algoritmo sea lineal vamos a tratar de aplicar el metodo de sutitucion para demostrarlo.
Teorema: Existe una constante c tal que T (n) n. Demostracion por induccion en n Sea n = 1. T (n) = 1,por lo tanto
T (n) cn si c 1 55
Hipotesis inductiva: T (n ) cn para n < n Queremos probar que T (n) cn Por de nicion sabemos que T (n) = T (n=5) + T (3n=4) + n Como n=5 y 3n=4 son ambos menores que n podemos aplicar la hipotesis inductiva obteniendo. 3n 1 3 T (n) c n 5 + c 4 + n = cn( 5 + 4 ) + n 19c T (n) cn 19 20 + n = n( 20 + 1)
0 0 0
Por lo tanto c (19c=20)+1 Despejando c vemos que se cumple para c 20. Como necesitabamos c 1 y ahora necesitamos c 20. Podemos decir que con c = 20. T (n) cn Por lo que queda demostrado. A proposito: Esta demostracion era un ejercicio de uno de los apuntes anteriores, espero que la resolucion sea igual o muy parecida a la planteada por Ud. Algunos podran preguntarse porque el algoritmode Floyd etc : : : utiliza grupos de 5 elementos, ocurre que por la demostracion anterior el algoritmo funciona para n > 4 y por eso eligieron n = 5. El otro problema que nos restaba resolver sobre el algoritmo \Seleccion1 es como particionar el vector una vez elegido el pivote. Este procedimiento lo vamos a dejar para mas adelante ya que no hay tiempo ni espacio su ciente en esta clase.
00
4.3.3 Particionando
56
Trataremos de aplicar el paradigma de Dividir para Conquistar al problema de como multiplicar dos numeros para ver si podemos obtener un algoritmo que sea mejor que (n2 ). Aplicando el paradigma de dividir para conquistar hacemos lo siguiente: sean dos numeros A y B de n digitos cada uno (si tienen distinta cantidad de digitos completamos con ceros). Dividimos el numero A en sus n=2 digitos mas signi cativos:w y sus n=2 digitos menos signi cativos:x. Hacemos lo mismo con B obteniendo y y z . De acuerdo a la particion que hicimos sabemos que A = w 10n=2 + x y B = y 10n=2 + z . Una vez realizada la particion podemos realizar la multiplicacion de acuerdo al siguiente esquema.
Como vemos podemos hacer el producto de la forma: Sea m = n=2 mult(A b) = mult(w y)102m + (mult(w z ) + mult(x y))10m + mult(x z ) La operacion de multiplicar por 10m consiste simplemente en shiftear el numero m posiciones y por lo tanto no es realmente una multiplicacion, las sumas implican sumar numeros de n=2 digitos y cuesta por lo tanto (n) cada una. Podemos expresar entonces el producto entre dos numeros como 4 productos de numeros de la mitad de digitos de los originales mas una cantidad constante de sumas y shifts de costo (n). Por lo tanto el costo del algoritmo se puede 57
calcular a partir de la siguiente recurrencia. si n = 1 T (n) = 1 4T ( n 2 ) + n sino Aplicando el teorema maestro vemos que a = 4 b = 2 k = 1 y a > bk por lo que estamos en el caso uno y el orden es (nlog 4 ) = (n2 ). Desafortunadamente no hemos podido, aun, mejorar el algoritmo. Pese a no haber podido mejorar el algoritmo podemos apreciar un detalle importante: la parte critica del algoritmo es la cantidad de multiplicaciones de numeros de tamanio n=2. El numero de sumas, siempre y cuando sea constante, no afecta el orden del algoritmo. Por lo tanto nuestro objetivo va a ser reeemplazar algunas multiplicaciones por una cantidad constante de sumas. Obviamente no podemos multiplicar mediante sumas sucesivas puesto que la cantidad de sumas dependeria de n y no seria constante.
La clave para mejorar el algoritmo reside en un truco algebraico descubierto por Gauss. Gauss encontro lo siguiente. C = mult(w y) D = mult(x z ) E = mult((w + x) (y + z )) ; C ; D = (wy + wz + xy + xz ) ; wy ; xz = (wz + xy) Finalmente tenemos mult(A B ) = C 102m + E 10m + D En total hacemos 3 multiplicaciones, 4 sumas y 2 restas de numeros de n=2 digitos, ademas se requieren algunos corrimientos. El tiempo del algoritmo aplicando el truco de Gauss es el siguiente. si n = 1 T (n) = 1 3T (n=2) + n sino Aplicando el teorema maestro nuevamente tenemos a = 3, b = 2 y k = 1. Lo cual nos da T (n) = (nlog 3 ) ' (n1:585). Debemos analizar si esto es realmente una mejora. Este algoritmo arrastra una cantidad grande de factores constantes como el overhead debido a la recursion y las operaciones aritmeticas adicionales, pero debido a nuestros conocimientos del orden de los algoritmos sabemos que a partir de un cierto n el nuevo algoritmo sera mejor ya que el tradicional tiene una tasa de crecimiento mayor. Por ejemplo si suponemos que en el algoritmo de Gauss las constantes son 5 veces mayores que en el algoritmo simple tenemos (5n1:585 versus n2), y el n a partir del cual Gauss es mejor es 50, si las constantes fuesen 10 veces mayores tendriamos n 260. En de nitiva en general para multiplicar numeros de gran cantidad de digitos el algoritmo mejorado resulta superior. 58
Para este problema disponiamos de una solucion iterativa de orden (n2 ), tal vez podamos aplicar nuestros conocimientos en algoritmos de tipo Dividir para Conquistar para resolver el problema en forma mas e ciente.Tenemos que aplicar las tres fases del paradigma: dividir, conquistar y combinar.
P . Si P contiene pocos puntos, entonces simplemente aplicamos el algoritmo de fuerza bruta iterativo que conociamos gasta el momento (Tarda una cantidad de tiempo constante). Si hay mas de n puntos entonces trazamos una linea vertical l que subdivide la lista P en dos conjuntos de aproximadamente el mismo tamanio, Pl y Pr.
lo que obtenemos l y r distancias minimas para los dos conjuntos. De ambas distancias obtenemos = min( l r ).
de la linea, cuya distancia sea menor a . En primer lugar observemos que no necesitamos chequear todos los puntos, si un punto esta a mayor distancia de l que entonces seguro no hay un vecino del otro lado de la linea que pueda formar una distancia menor que . Creamos una lista P de puntos que estan a menos de de cada lado de l. Determinamos entonces la distancia minima para los puntos de P que llamaremos y devolvemos el minimo entre y como resultado. En realidad no devolvemos la distancia sino los dos puntos que estan a dicha distancia uno del otro.
0 0 0 0
El problema consiste en analizar como encontrar la distancia minima en P , lo cual requiere un poco de astucia. Para optimizar el algoritmo queremos encontrar la distancia minima en P en tiempo (n) vamos a demostrar que esto puede hacerse suponiend que los puntos de P estan ordenados por la coordenada y, luego tendremos que resolver como ordenar los puntos.
0 0 0
Supongamos que los puntos de P estan ordenados de acuerdo a su coordenada y. Consideremos un punto P i] en la lista ordenada. Cual es el punto mas cercano a P i]. Podemos restringir la busqueda a los puntos que tienen indice j mayor que i ya que el proceso lo vamos a hacer para todos los puntos (si el vecino mas cercano esta arriba, entonces esto ya fue analizado cuando buscamos el vecino de dicho punto). Podriamos pensar que necesariamente el vecino mas cercano es P i + 1], pero esto no es verdad, dos puntos que tienen distancia minima en cuanto a la coordenada y no necesariamente tienen distancia minima en el plano, queremos saber que tan lejos tenemos que buscar hasta P i + 2]?,
0 0 0 0 0
60
P i + 8]? tal vez podamos acotar la busqueda a una cantidad constante de puntos.
0
Resulta que podemos limitar la cantidad de puntos a buscar y ademas resulta que no nos hace falta analizar mas de 7 puntos en la lista P para cada punto. Esto lo vamos a tener que demostrar.
0
Teorema: Si P i] y P j] (i < j) son la dupla mas cercana en P y la distancia entre ellos es menor que , entonces j ; i 7.
0 0 0
P y su distancia es menor que . Por estar en P estan a distancia menor que de l. Por ser su distancia menor que sus coordenadas y di eren en menos de . Por lo tanto ambos puntos residen en un rectangulo de ancho 2 y altura centrado en la linea l. Dividamos este rectangulo en 8 cuadrados del mismo tamanio de lado =2. Observemos que la diagonal de cada uno de esto cuadrados tiene como longitud: p 2 2 = p2 < Como cada cuadrado esta completamente a un lado de la linea l ningun cuadrado puede contener dos puntos de P ya que si los tuviera entonces ambos puntos estarian a distancia menor que y no seria la distancia minima entre las distancias minimas a ambos lados de P . Por lo tanto puede haber a lo sumo 8 puntos de P en este rectangulo y por lo tanto j ; i 7.
0 0 0
Por lo tanto por cada P i] solo debemos chequear una cantidad constante de numeros y por eso podemos buscar la distancia minima en P en O(n).
0 0
El unico asunto que nos queda pendiente es como ordenar los puntos de P , dado que queremos hacerlo en O(n) nos vemos impedidos de usar un algoritmo de sort generico que tardaria O(n log n) en general. Hay una solucion ingeniosa a este problema: Pre-ordenar los puntos. En particular almacenamos los puntos (redundantemente) en dos listas P X y PY ordenando por las coordenadas x y y respectivamente. Esto lo hacemos llamando a un algoritmo de sort dos veces antes de comenzar el algoritmo de dividir y conquistar.
0
Ya no necesitamos la lista P , cuando hacemos el split de P en Pl y P r lo que hacemos es dividir P X y PY en Xl, Xr, Y l, Y r respectivamente. Esto es facil de hacer en O(n). Dado que las listas originales estan ordenandas las listas resultantes tambien lo estan. Para armar la lista P simplemente extraemos los elementos de Y que estan a distancia de l y luego los copiamos a la lista Y en donde luego buscamos.
0 0
61
distancia minima, es sencillo aplicar los cambios pertinentes para devolver los puntos a dicha distancia. Presort: Dada la lista P , hacemos dos copias PX y PY . Ordenamos PX por la coordenada x de los puntos y la lista P Y por la coordenada y. Parte Recursiva: DistMin(X,Y) { Condicion de corte: Si la cantidad de puntos es menor que 4 entonces resolvemos el problema por fuerza bruta y devolvemos la distancia minima analizando todas las distancias posibles. { Dividir: Sino, sea l la mediana de las coordenadas x de PX . Dividimos ambas listas PX y PY por esta linea, manteniendo el orden, creando Xl, Xr, Y l, Y r. { Conquistar: l = DistMin(Xl,Yl), r = DistMin(Xr,Yr). { Combinar: = min( l r ).Creamos la lista Y copiando todos los puntos de Y que estan a distancia menor que de l. Para i desde 1 hasta la longitud de Y y para j desde i + 1 hasta i + 7 calcular la distancia entre Y i] y Y j ]. Sea la distancia minima entre dichas distancias. Devolver min( ).
0 0 0 0 0 0
4.5.1 Analisis
0
Debemos analizar cuanto tiempo tarda el programa en su peor caso, en el caso promedio en analisis es muy complejo porque depende de cuantos puntos tiene P en promedio y esto depende en cual es la distancia minima esperada en cada sublista. En el peor caso podemos suponer que P tiene a todos los puntos. La fase de pre-sort insume O(n log n). Para analizar la parte recursiva planteamos la recurrencia. si n 3 T (n) = 1 2T (n=2) + n sino Esta recurrencia ya es conocida y nos permite obtener que el algoritmo es O(n log n).
0
De esta forma obtenemos un algoritmo mas e ciente que el algoritmo de fuerza bruta que era (n2 ).
nos permite resolver un problema en forma e ciente. La forma basica de un algoritmo de tipo dividir para conquistar contiene los siguientes puntos. Condici'eon de corte: Controla en que momento se detiene la recursividad, usualmente consiste en aplicar algun metodo no muy e ciente cuando el problema alcanza un tamanio chico. Dividir: Se divide el problema en n sub-problemas mas chicos. Conquistar: Aplicando recursivamente el algoritmose resuleven los problemas mas chicos. Combinar: Se combina la solucion de los problemas resueltos para encontrar la solucion al problema original. El algoritmo de MergeSort es un algoritmo de sort interno importante, que ilustra claramente el paradigma. El problema de la Seleccion o de la Mediana fue durante muchos anios de muy dificil resolucion en forma e ciente, el algoritmo que explicamos aqui permite resolver el problema en forma e ciente utilizando un algoritmo de tipo dividir para conquistar. En este problema utilizamos una forma especial de algoritmos \Dividir para Conquistar" en la cual la cantidad de subproblemas es 1. A estos algoritmos se los conoce como cribas. Otra aplicacion conocida del paradigma permite multiplicar dos numeros de gran cantidad de digitos en forma mas e ciente que el metodo tradicional, aqui utilizamos un truco algebraico sugerido por Gauss para reducir el numero de multiplicaciones que debiamos realizar. Por ultimo vimos como pudimos aplicar este paradigma al problema de encontrar los puntos a distancia minima en un plano en forma mas e ciente que nuestra solucion anterior.
4.7 Ejercicios
1. Dado un vector A de n numeros encontrar un algoritmo que permita encontrar el elemento mayoritario en A, es decir aquel que aparece con mayor frecuencia en el vector. 2. Una compania petrolera ha construido n torres de extraccion de petroleo en la patagonia, la compania desea construir un oleoducto de direccion NorteSur que permita trasnportar el petroleo extraido por las torres. Para este problema vamos a suponer que el oleoducto esta formado por una unica linea recta. La empresa debe construir ademas tramos perpendiculares al oleoducto que lleguen a cada torre. Describir un algoritmo que permita 63
encontrar la mejor ubicacion para el oleoducto de forma tal de minimizar la cantidad de kilometros a construir en tramos perpendiculares. El algoritmo debe ser lineal o mejor. 3. El metodo de Floyd, Rivest, Pratt y Tarjan toma grupos de 5 elementos para la seleccion del pivote. Indicar que ocurriria si se tomaran 7 o 3 elementos para dicha seleccion. 4. Dados dos vectores X n] y Y n] ordenados dar un algoritmo de orden O(log n) que permita calcular la mediana de los 2n elementos en conjunto.
64
Chapter 5
Algoritmos Aleatorizados
5.1 Algoritmos aleatorizados
Los algoritmos aleatorizados son aquellos que en algun momento de su ejecucion utilizan la generacion de un numero aleatorio para tomar una determinada decision. Sorprendentemente aleatorizar un algoritmo haciendolo tomar decisiones al azar a menudo sirve para que el algoritmo sea mas e ciente. Los algoritmos aleatorizados se dividen en dos grandes familias: los algoritmos de tipo Montecarlo y los algoritmos de tipo Las Vegas.
Los algoritmos de tipo Las Vegas siempre resuelven correctamente un problema, sin embargo pueden (con baja probabilidad de ocurrencia) demorar mucho mas tiempo que en el caso medio para encontrar la respuesta.
Los algoritmos de tipo Montecarlo siempre son e cientes en cuanto a la cantidad de tiempo necesaria para encontrar la respuesta. Sin embargo en algunos casos (con baja probabilidad de ocurrencia) pueden producir una respuesta no correcta al problema.
Quick Sort es tambien un algoritmoaleatorizado ya que utiliza un generador de numeros al azar para tomar decisiones. Como es de esperar Quick Sort siempre produce un vector correctamente ordenado por lo que lo podemos clasi car como un algoritmo de tipo Las Vegas.
El funcionamiento basico de Quick Sort lo podemos describir mediante el esquema clasico de los algoritmos \Divide & Conquer". Condicion de corte: Si la cantidad de elementos del vector a ordenar es 0 o 1 entonces return. Dividir: Seleccionar un elemento x al azar del vector que llamaremos pivote. Esta es la parte aleatorizada del algoritmo. En base al pivote dividimos el vector en tres partes. A 1..q-1], A q] y A q+1..n]. Conquistar: Recursivamente invocar Quick Sort para A 1::q ; 1] y A q +1::n] Combinar: No es necesario realizar ningun proceso extra ya que A 1..n] queda ordenado.
Algorithm 18 QuickSort(A,p,r). Ordena el vector A desde p hasta r if r p then return end if i ( random(p::r) Swap(A i],A p]) q ( Particionar(p r A)
QuickSort(p,q-1,A) QuickSort(q+1,r,A)
Analisis En primer lugar es bastante obvio que el algoritmo ordena el vector, es decir que podemos a rmar que es correcto. El costo en tiempo depende
bastante del pivote elegido, si el pivote fuese un numero muy chico o muy grande llamariamos recursivamente al algoritmo para practicamente todos los numeros, si el pivote en cambio es un numero medio las particiones se balancean y el rendimiento es mejor. 1. Ademas para poder proceder al analisis del algoritmo necesitamos un algoritmo que realice la particion, este algoritmo tambien era solicitado por el algoritmo de Seleccion1 que ya estudiamos. Pasemos a ver, pues, como realizar el solicitado proceso de particion.
1 Las llamadas recursivas pueden ser vistas como un arbol, si el pivote esta en el medio el arbol es balanceado, si en cambio el pivote es un extremo una de las ramas del arbol sera mucho mas extensa que la otra y necesitaremos mas llamadas recursivas
66
El algoritmo que presentamos trabaja suponiendo que se recibe un vector A, dos indices p y r que indican desde donde hasta donde particionar y ademas supone que el Pivote es el elemento A p], o sea el primer elemento del vector. Es por esto que en el algoritmo de Quick Sort se Swapea el pivote con A p] antes de particionar. El algoritmo mantiene un invariante a lo largo de su ejecucion que indica que el vector esta dividido en 4 segmentos indicados por p, q, s y r. De la forma: 1. A p] = x es el pivote. 2. A p + 1::q] contiene elementos que son menores que x 3. A q + 1::s ; 1] contiene elementos que son mayores o iguales que x 4. A s::r] contiene elementos desconocidos.
67
Algorithm 19 Particionar(p,r,A). Particiona el vector A p..r] con pivote A p] x ( A p] q(p for s = p + 1 to r do if A s] < x then q (q+1 Swap(A q] A s]) end if end for
Swap(A p] A q]) returnq
recta, ademas esta es solo una implementacion, hay varias formas de realizar la particion y todas ellas son mas o menos iguales en e ciencia. El algoritmo tiene un unico ciclo que recorre todos los elementos por lo que se trata claramente de un algoritmo de orden lineal.
68
5.2.3 Analisis de
QuickS ort
En general el tiempo necesario por el procedimiento QuickSort esta dado por la recurrencia. si n = 1 T (n) = 1 T (q ; 1) + T (n ; q) + n sino Y como vemos el analisis depende del valor de q
El peor caso ocurre cuando el pivote es uno de los extremos del vector (extremo en cuanto a su valor: minimo o maximo), suponiendo por ejemplo que el pivote es el elemento mas chico del vector tendremos que q, la posicion del pivote es 1.Aplicando el metodo iterativo a la recurrencia. T (n) = T (0) + T (n ; q) + n T (n) = 1 + T (n ; 1) + n T (n) = T (n ; 1) + (n + 1) T (n) = T (n ; 2) + n + (n + 1) T (n) = T (n ; 3) + (n ; 1) + n + (n + 1) T (n) = T (n ; 4) + (n ; 2) + (n ; 1) + n + (n + 1) n 3 X .. .T (n) = T (n ; k) + n;i
;
T (n) = O(n2)
i=;1
Para analizar el caso medio debemos interpretar a T (n) como el tiempo promedio del algoritmo QuickSort para un vector de tamanio n. El algoritmo tiene n opciones equiprobables para elegir el pivot. Por lo tanto la probabilidad de elegir un elemento es 1=n. La recurrencia que tenemos que plantear es: si n = 1 ( T ( q ; 1) + T ( n ; q ) + n ) sino n q=1 Esta no es una recurrencia comun, por lo que no podemos aplicar el Teorema Maestro, el metodo iterativo es posible pero resulta complicado. Entonces vamos a intentar demostrar que el caso medio es (n log n). T (n) =
1
1 Pn
cn ln n para todo n 2. Notemos que hemos reemplazado log con ln, esto simpli ca nuestra demostracion sin afectar el orden, de hecho cualquier logaritmo sirve. 69
q=1
n vale que T (n ) cn ln n . Queremos probar que esto es valido para T(n). Expandiendo la de nicion de T(n) y sacando el factor n afuera de la sumatoria tenemos. n 1X T (n) = n (T (q ; 1) + T (n ; q) + n) q=1 n 1X (T (q ; 1) + T (n ; q)) + n T (n) = n q=1
0
Observemos que si dividimos la sumatoria en dos sumatorias ambas suman los mimso valores T (0) + T (1) + : : : + T (n ; 1), una desde 0 hasta n ; 1 y laP otra1 desde n ; 1 hasta 0. Entonces podemos escribir la sumatoria como 2 n q=0 T (q).T (0) y T (1) no responden a esta formula por lo que los vamos a tratar en forma especial. Aplicando esta sustitucion y aplicando la hipotesis inductiva a la sumatoria restante lo cual podemos hacer porque q n tenemos.
;
q=0
2 (1 + 1 + T (n) = n c( T 8n) = 2 n
n ;1 X q=2
q=2 n ;1 X q=2
(cq ln q)) + n
4 (cq ln q)) + n + n
Esta sumatoria no la hemos visto nunca hasta el momento, vamos a mostrar la solucion. n 1 2 X n2 S (n) = q ln q n ln n ; 2 4
;
q=2
Reemplazando c ( n2 ln n ; n2 ) + n 4 T (n) = 2 n 2 4 n 70
4 + n + T (n) = cn ln n ; cn 2 n c 4 T (n) = cn ln n + n(1 ; 2 ) + n Queremos que esta expresion sea menor o igual que cn ln n Cancelando cn ln n a ambos lados de la desigualdad vemos que se cumple si. c) + c 0 n(1 ; 2 n Esto se cumple si c = 3, como ademas necesitabamos c mostrado. 2:88 queda de-
el caso medio es (n log n). Aunque no lo hemos demostrado el peor caso no ocurre muy a menudo. Para valores grandes de n el tiempo puede considerarse (n log n) con una probabilidad alta. Para degenerar en el orden cuadratico el algoritmo tendria que elegir el pivote en forma defetuosa en cada uno de los pasos recursivos. Las elecciones malas del pivote son poco probables por lo que una secuencia de malas elecciones es aun menos probable. El algoritmo de QuickSort es ademas un algoritmo de sort in ; situ.
Existen versiones de QuickSort que no eligen el pivote en forma aleatoria sino que por ejemplo toman siempre el primer elemento del vector como pivote. Esta tecnica tiene como desventaja que, por ejemplo, si el vector ya se encontraba ordenado o bien se encontraba rebatido el algoritmo se encuentra automaticamente en el peor caso y adquiere orden cuadratico. Por eso es muy aplicable la eleccion aleatoria ya que ocasiona una ventaja fundamental Ninguna disposicion original de los datos ocasiona el peor caso, esto es facil de ver ya que la ocurrencia del peor caso no depende de los datos de entrada sino, de la eleccion del pivote que es aleatoria. Hemos dado con el principio clave de los algoritmos de tipo Las Vegas: el tomar una decision aleatoria para indpendizar el funcionamiento del algoritmo de los datos de entrada ocasionado que el peor caso ocurra con muy baja probabilidad (n malas elecciones de un numero aleatorio).
5.2.5 Resumen
QuickS ort
Es un algoritmo de tipo Divide & Conquer. Es un algoritmo aleatorizado de tipo Las Vegas. El peor caso es (n2 ) con muy baja probabilidad. 71
El caso medio que es altamente probable es (n log n). Ordena el vector in ; situ No es estable.
Una sub-division de los algoritmos aleatorizados son los algoritmos incrementales, en este caso vamos a presentar un algoritmo tipo Las Vegas incremental para resolver el problema de la distancia minima. La idea es la siguiente, los puntos van a ser insertados uno a uno en un conjunto inicialmente vacio y por cada punto que insertamos vamos a chequear si no es necesario actualizar cual es la distancia minima.
5.3.2 El algoritmo
El funcionamiento del algoritmo dependera del orden en que se inserten los puntos, algunos ordenamientos seran particularmente malos ya que necesitaremos actualizar la distancia minima por cada puunto insertado lo cual nos insumira tiempo O(n2). Sin embargo al insertar los puntos en orden aleatorio el tiempo esperado del algoritmo sera O(n). No lo vamos a demostrar pero la probabilidad de que el algoritmo insuma mas de O(n log n) es extremadamente chica. Para simpli car asumamos que los puntos han sido normalizados para pertenecer al cuadrado 0,1]. (Esta suposicion es solo para facilitar la explicacion del algoritmo). En primer lugar permutamos en forma aleatoria los puntos, y sea (p1 p2 : : :pn) la permutacion resultante. Inicialmente ponemos el primer punto en el conjunto y la distancia minima al haber solo un punto en el conjunto es 1. Uno por uno insertamos los puntos p2 p3 etc. Sea Pi 1 el conjunto
;
72
La gran pregunta es como encontrar el punto mas cercano a pi , seria demasiado lento considerar todos los puntos en Pi 1. En purmer lugar observemos que solo necesitamos considerar puntos de Pi 1 que estan a distancia de Pi, ya que ningun otro punto puede afectar la dupla mas cercana actual. Declaramos que esto se puede hacer en tiempo constante si los puntos son almacenados en una estructura apropiada.
; ;
Grilla de Buckets La estructura de datos que vamos a utilizar para guardar el conjunto de puntos se denomina grilla de buckets En particular subdividimos
el cuadrado unitario 0,1] en una grilla de cuadraditos de lado . Dentro de cada cuadradito mantenemos una lista enlazada de los puntos de Pi 1 que pertenecen al cuadradito, esta lista se denomina bucket.
;
Para determinar a que bucket corresponde un punto observemos que si dividimos el intervalo 0,1] en subintervalos de longitud tendremos d =e1= d subintervalos. Si asumimos que indexamos los subintervalos de la forma 0 1 : : : d ; 1 para determinar el subintervalo que contiene un cierto valor x dividimos x por y redondeamos hacia abajo al entero mas cercano es decir. I = bx= c Por ejemplo si = 0:3 entonces los subintervalos son 0 : 0 0:3], 1 : 0:3 0:6], 2 : 0:6 0:9], 3 : 0:9 1]. El valor x = 0:72 es mapeado al intervalo b0:72=0:3c = 2. Extendiendo esto a dos dimensiones mapeamos el punto (x y) al bucket I (x) I (y)]. Cuando el algoritmo comienza inicialmente = 1 y existe un unico bucket. Para almacenar los buckets necesitamos una hash table ya que no podemos usar una matriz porque para muy chicos tendriamos mas entradas en la matriz de las que podriamos manejar. Este es un buen ejemplo de como manejar el costo espacial de algoritmo. Aquellos que no conozcan como funciona una hashtable pueden despreocuparse y suponer que los buckets estan almacenados en una matriz. Un aspecto notable es que no podemos tener mas de 4 (cuatro) puntos en cada bucket (uno en cada esquina del bucket) ya que de lo contrario la distancia minima no seria sino que seria menor. Dado el punto a insertar pi , para determinar el vecino mas cercano o bien esta en el bucket correspondiente a pi o bien es uno de los 8 bucktes vecinos, por lo tanto a lo sumo revisamos 9 bucktes y cada uno de ellos tiene a lo sumo 73
4 puntos, por lo que la cantidad maxima de puntos a revisar es 4 9 = 36 y es constante. Si no hay un punto a distancia menor que entonces la distancia minima permanece inalterada e insertamos el nuevo punto en su bucket correspondiente. Si la distancia minima es modi cada debemos reconstruir la grilla para la nueva distancia minima, esto insume O(i) tiempo, ya que cada punto a ubicar insume una cantidad de tiempo contante.El algoritmo es el siguiente. 1. Permutar aleatoriamente los puntos. Sea fp1 p2 : : : png la permutacion resultante. Crear una grilla trivial de 1x1 e insertar p1 en la grilla. Sea = inf 2. Para i desde 1 hasta n hacer (a) Sea pi = (Xi Yi). Seleccionar el bucket I (Xi ) I (Yi )] y sus 8 buckets vecinos de la grilla. (b) Si no hay puntos en ninguno de estos buckets entonces continuar el ciclo. Sino computar la distancia minima y los (a lo sumo 36) puntos encontrados en los buckets. Sea esta nueva distancia. (c) Si < entonces = , destruir la grilla anterior y construir una nueva grilla a partir de Pi = fp1 : : : pig. Sino simplemente agregar pi a la grilla. 3. Dovolver
0 0 0
74
5.3.3 Analisis
Peor caso
Este es un algoritmo iterativo, por lo que para analizarlo no vamos a necesitar plantear, al n, ninguna recurrencia. En el pero caso cada punto que agregamos al conjunto nos obliga a actualizar la distancia minima y por lo tanto a reconstruir la grilla, el tiempo se calcula entonces como. n X T (n) = i = n(n2+ 1) 2 O(n2)
i=1
Mejor caso
En el mejor caso solo el segundo punto nos hace reconstruir la grilla y luego nunca mas actualizamos la distancia minima, el tiempo es entonces. T (n) =
i = 1n O(1) = O(n)
Caso medio
Si realizamos una simulacion del algoritmo veremos que la grilla es reconstruida una cierta cantidad de veces para los primeros puntos pero, a medida que el algoritmo procesa mas y mas puntos, la distancia minima es cada vez mas rme y la reconstruccion de la grilla se vuelve muy poco frecuente. Para analizar el caso medio asumimos que todas las permutaciones de los puntos son equiprobables. Sea i la probabilidad de que el iesimo punto ocasione un cambio en la distancia minima. Entonces con probabilidad i la grilla es reconstruida en la iesima iteracion con un costo de O(i). Con probabilidad (1 ; i ) el iesimo punto no afecta la distancia y la iteracion insume O(1). Para obtener el caso medio necesitamos el promedio ponderado del tiempo que insume cada ciclo de cuaerdo a su probabilidad es decir: T (n) =
n X i=1
(i i + 1 (1 ; ))
n X i=1
(i i ) + n
Necesitamos saber cuanto vale i , para ello recurrimos a un truco bastante usado en el analisis probabilistico que consiste en analizar el algoritmo funcionando de atras para adelante. Obseremos que Pi 1 es un conjunto de i ; 1 puntos, analizar que ocurre cuando un punto aleatorio es insertado en Pi 1 es simetrico a analizar que ocurre cuando eliminamos un punto al azar de Pi. De aqui obtenemos esta observacion.
; ;
75
actualice la distancia minima es igual a la probabilidad de que al eliminar un punto al azar de Pi estemos eliminando uno de los puntos que estan a distancia minima. Como hay exactamente dos puntos que participan de la distancia minima y en total hay i puntos la probabilidad de que uno de los i puntos este en la dupla a distancia minima es 2=i para i 2. Luego i = 2=i y calculamos. T (n) =
i = 1ni(2=i) + n =
n X i=1
2 + n = 3n = O(n)
Por lo tanto el tiempo medio es O(n) y podemos a rmar que con una probabilidad muy alta este algoritmo es de orden lineal.
minima en el plano, hemos visto como a medida que estudiabamos distintas tecnicas para el disenio de algoritmos podiamos resolver el problema en forma mas e ciente. Planteamos primero un algoritmo iterativo que usando fuerza bruta resolvia el problema en O(n2), luego mediante un algoritmo de tipo \Divide & Conquer" obtuvimos O(n log n), por ultimo mediante un algoritmo aleatorizado resolvimos el problema en O(n).
Basandonos en la de nicion podemos construir un algoritmo bastante simple. El algoritmo insume en el peor caso n ; 1 divisiones, es decir que es O(n) pero si analizamos el orden del algoritmo en funcion del tamanio del numero en bits podemos observar que la cantidad de bits de un numero n es k = log(n) por lo 76
Algorithm 20 EsPrimo1(n). Devuelve true si n es primo for k = 2 to n do if kmodn then return false end if end for
return true tanto n = 2k , con lo cual el algoritmo es O(n) = O(2k ) es decir que se trata de un algoritmo exponencial y claramente debe ser mejorado.
Teorema: Si n no es primo entonces n tiene un factor primo p tal que p pn. Demostracion: Supongamos que un numero n no es primo y su factorizacion prima es n = p1p2 : : :pk > (pn)k lo cual es falso salvo que k < 2 es decir que
n sea primo. Por lo tanto queda demostrado.
El orden del algoritmo es ahora O(pn) = O( 2k ) = O(2k=2 con lo cual pese a reducir notablemente la cantidad de divisiones a realizar seguimos teniendo un algoritmo de orden exponencial.
Como vemos este algoritmo sirve para decir si un numero no es primo pero no asegura que un numero sea primo, esto ocurre porque el teorema de Fermat no funciona en ambas direcciones, que dos numeros n y a satisfagan el teorema no quiere decir que n sea primo. 77
Algorithm 21 EsPrimo2(n). Devuelve true si n es primo n ( random(1::n ; 1) if an 1 = 6 1(modn) then return false else return true (aunque deberia ser maybe!) end if
;
Por ejemplo n = 341, a = 22, 2340 = (210)34 = 102434 = (3 341 + 1)34 Pero 341 = 11 31 Claramente estamos ante un algoritmo aleatorizado (usa un numero al azar para realizar el chequeo) y de tipo Montecarlo, ya que la aleatorizacion no afecta el tiempo de ejecucion del algoritmo sino el resultado. Eventualmente nuestro algoritmo EsPrimo2 puede devolver como primo un numero que no lo es. El orden del algoritmo es el orden que insume realizar an 1, como vimos en clases anteriores es (log n) es decir que pasamos de un algoritmo exponencial a uno sub-lineal (logaritmico).
;
Un detalle importante es calcular cual es la probabilidad de que un numero que no es primo sea considerado primo por el algoritmo, no la vamos a calcular en este apunte pero es inferior a 0:5. Si se elige mas de un numero al azar y se efectua el chequeo varias veces esta probabilidad disminuye, de hecho la probabilidad de devolver un resultado erroneo se puede hacer tan chica como se desee.
Algorithm 22 EsPrimo2(n). Devuelve true si n es primo for k = 1 to constante do n ( random(1::n ; 1) if an 1 = 6 1(modn) then return false end if end for
;
return true
El orden del algoritmo es (j log n) es decir que sigue siendo un algoritmo de orden logaritmico. Cuanto mayor sea j mas tiempo va a tardar pero eso no afecta el orden del algoritmo. Este comportamiento es tipico de los algoritmos de tipo Montecarlo, en donde se puede disminuir la probabilidad de devolver un resultado equivocado a costas de insumir mayor tiempo en el algoritmo. 78
Numeros muy raros Pese a que hagamos el chequeo una cantidad enorme de veces el algoritmo aun tiene un problema, existen numeros no primos que satisfacen el teorema de Fermat para todo 1 a n ; 1, estos numeros se conocen
como \Numeros Carmiqueleanos", nuestro algoritmo es incapaz de distinguir un numero primo de un numero Carmiqueleano. Afortunadamente estos numeros son muy raros, hay solamente 255 en los primeros 100 millones de numeros enteros, por lo tanto podemos testear si el numero es o no Carmiqueleano contra una tabla (Esto se hace en tiempo constante porque la tabla tiene longitud ja).
79
5.6 Ejercicios
1. El siguiente algoritmocreado por N.Lomuto particiona A p::r] en A p::i] A i+ 1::j ] de forma tal que cada elemento de la primera region es menor o igual a x = A r] y cada elemento de la segunda region es mayor o igual que x.
Algorithm 23 Lomuto-Partition(A,p,r).
x A r] i p;1 for j = p to r do if A j] x then i i+1 Swap(A i] A j ])
end if end for if i < r then return i else return i-1 end if
(a) Validar el algoritmo (b) Analizar el tiempo de ejecucion del algoritmo. (c) Analizar el tiempo de ejecucion de Quick ; Sort utilizando el algoritmo de Lomuto para particionar. (d) De nir un algoritmo aleatorizado que intercambie A r] con un elemento elegido al azar de A p::r] y luego invoque al algoritmo de Lomuto. Analizar el algoritmo. 2. Mostrar que el mejor caso de Quick ; Sort es (n log n) 3. Una forma interesante de mejorar el rendimiento de Quick ; Sort es aprovechando la e ciencia de Insert ; Sort para ordenar vectores que estan casi ordenados. De esta forma se puede modi car el algoritmo de Quick-Sort para que no ordene particiones de menos de k elementos. Luego se llama a Insert-Sort sobre el vector casi ordenado para nalizar el proceso. Mostrar que este algoritmo es O(nk + n log (n=k)). Indicar como deberia elegirse k tanto en teoria como en la practica.
80
Chapter 6
Sort
6.1 Algoritmos de Sort
El proposito de esta clase es revisar algunos algoritmos de sort ya vistos con anterioridad, estudiar algunos metodos de sort y realizar algunas conclusiones generales sobre metodos de sort. Para comenzar vamos a establecer algunas de niciones que seran utiles mas adelante.
que tienen la misma clave en el vector original mantiene el orden en el vector resultado. La estabilidad es una propiedad deseable en la mayoria de los algoritmos de sort y se vuelve necesaria cuando estamos ordenando por mas de una clave.
Los algoritmos de sort no-recursivos son los mas simples para programar y entender, hay muchos metodos pero tres de ellos son los mas conocidos: el burbujeo, la seleccion y la insercion. Es un algoritmo \in-situ", puede implementarse como un algoritmo estable y es de orden cuadratico (n2 ). El codigo y funcionamiento del algoritmo ya fue visto en clases anteriores, es un algoritmo que en general resulta demasiado lento ya que realiza una gran cantidad de intercambios entre los elementos del vector. 81
Seleccion
Este algoritmo es de tipo \in-situ". Lamentablemente no puede implementarse en forma estable sin grandes modi caciones. Es de orden cuadratico al igual que el burbujeo (n2 ). En general es mas e ciente que el burbujeo porque realiza menor cantidad de intercambios entre los elementos del vector.
Insercion
El isert-sort es un algoritmo \in-situ" que puede implementarse en forma estable, es tambien de orden cuadratico O(n2 ) pero en su mejor caso es de orden lineal O(n). Pese a ser considerado un algoritmo lento es en general mejor que el burbujeo y la seleccion.
Los algoritmos recursivos se basan en general en el paradigma \Divide & Conquer". Los algoritmos de este tipo mas populares son el QuickSort, el MergeSort y el HeapSort, los dos primeros ya fueron estudiados mientras que el tercero se ve en gran detalle en este apunte.
MergeSort
El algoritmo de MergeSort es un algoritmo estable de orden (n log n), lamentablemente no se puede implementar in-situ pues requiere de un vector auxiliar del mismo tamanio que el original para funcionar.
QuickSort
El algoritmos de QuickSort es un algoritmo in-situ pero no es estable, es un algoritmo aleatorizado de tipo Las vegas, en su peor caso es de orden cuadratico O(n2 ), sin embargo este caso se da con muy baja probabilidad. En la mayoria de los casos es O(n log n). Este algoritmo creado por \Hoare" es reconocido por ser en general el mas rapido de los algoritmos de sort, el truco del algoritmo consiste en que las comparaciones se hace siempre contra un elemento pivote que puede ser almacenado en un registro de la maquina, esto le da ventaja sobre algoritmos que son asintoticamente equivalentes como el HeapSort o el MergeSort.
6.2 HeapSort
El algoritmo de HeapSort se basa en la utilizacion de una estructura de datos especial denominada \Heap". Un Heap es una estructura especial que permite implementar una cola con prioridades, las operaciones principales de las colas con prioridad se pueden realizar 82
6.2.1 Heaps
usando un Heap en (log n). Un Heap es un arbol binario completo a izquierda, lo cual quiere decir que todos los niveles del arbol excepto el ultimo estan completos y que el ultimo nivel se completa de izquierda a derecha. Ademas los elementos se almacenan cumpliendo la propiedad de que la clave de un nodo siempre es mayor (o menor) que la clave de sus hijos. De esta forma la raiz del arbol es siempre el mayor elemento en todo el Heap.
Almacenamiento Un Heap tiene la interesante cualidad de poder ser almacenado en un vector sin ocasionar inconvenientes en el manejo de la estructura. Lo cual evita la necesidad de utilizar punteros. Esto es posible gracias a que un Heap siempre es un arbol completo a izquierda. El Heap se almacena en un vector A 1::n], ademas se usa una variable que indica cuantos elementos tiene el Heap: m, de esta forma el heap es el sub-array A 1::m].
Almacenamos el Heap en el vector copiandolo en el mismo nivel por nivel. El primer nivel tiene exactamente 1 elemento, el segundo 2, y asi sucesivamente hasta el ultimo nivel que es el que puede no estar completo, la cantidad de elementos en el ultimo nivel la conocemos por conocer la cantidad m de elementos en el Heap.
Para acceder a los elementos del Heap en el array basta con efectuar algunas operaciones aritmeticas basicas. Left(i) : return 2 i Right(i) : return 2 i + 1 83
la clave de un nodo siempre es mayor que la clave de sus hijos existe una operacion fundamental denominada Heapify. La idea es que dado un elemento del Heap que no esta en orden y suponiendo que el resto del Heap si lo esta se procede a colocar el elemento en cuestion en su posicion correspondiente realizando interacambios con sus hijos si fuese necesario y luego invocando el procedimiento en forma recursiva. Es decir que el elemento fuera de orden es empujado hasta el lugar que le corresponde.
end if if r m and A r] > A max] then max ( r end if if max = 6 i then end if
Swap(A i],A max]) Heapify(A,max,m)
Recomendamos realizar un seguimiento para ver como funciona este procedimiento. Ademas el procedimiento recursivo es intuitivo pero no es el mas e ciente de todos, la version iterativa puede ser un poco mejor y se deja como ejercicio.
cantidad de trabajo que se hace en cada nivel es constante, por lo tanto Heapify es O(log n). No es (log n) ya que para las hojas por ejemplo el tiempo es constante: O(1). 84
Construyendo un Heap El procedimiento Heapify se puede utilizar para Algorithm 25 BuildHeap(A,n). Construye un Heap a partir de A 1..n] for i = n=2 downto 1 do Heapify(A,i,n) end for Analisis de BuildHeap Como cada llamada insume O(log n) y se hacen
n=2 llamadas el tiempo es O((n=2) log n) = O(n log n) Luego veremos que en realidad es mucho mas rapido y tarda en realidad O(n).
construir un Heap de la siguiente manera, primero se empieza con un Heap completamente desordenado, luego se invoca Heapify para cada nodo comenzando desde el primer nivel de nodos no-hojas. El primer nodo no-hoja es A bn=2c].
6.2.2 HeapSort
Una vez resueltos todos los asuntos concernientes al Heap podemos ver el algoritmo de HeapSort. La idea es remover el mayor elemento que es siempre la raiz del Heap, una vez seleccionado el maximo lo intercambiamos con el ultimo elemento del vector, decrementamos la cantidad de elementos del Heap e invocamos Heapify a partir de la raiz.
Como vemos hacemos n ; 1 llamadas a Heapify, cada llamada es O(log n). Por lo tanto el tiempo total es O((n ; 1) log n) = O(n log n)
BuildHeap revisado Para analizar el procedimiento BuildHeap en mayor detalle supongamos que n = 2h+1 ; 1, donde h es la altura del arbol, es decir
que trabajaremos con un arbol binario completo. (nota si h es la altura del arbol, el arbol tiene h + 1 niveles). El ultimo nivel del arbol tiene 2h nodos, todas las hojas residen en el nivel h.La raiz esta en el nivel 0. 85
Al llamar a Heapify el tiempo de ejecucion depende de cuan lejos pueda ser llevado el elemento al ser empujado hacia abajo en el Heap, en el nivel mas bajo hay 2h nodos, para ellos no invocamos a Heapify por lo que el trabajo es cero. En el siguiente nivel hay 2h 1 nodos, y cada uno puede descender a lo sumo un nivel. En el tercer nivel hay 2h 2 nodos y cada uno puede descender dos niveles. En general en el nivel j hay 2h j nodos y cada uno puede descender j niveles. Si contamos desde abajo hacia arriba nivel por nivel podemos calcular el tiempo total como.
; ; ;
T (n) =
h X j =o
h h X j 2h j = j 2 2j
;
j =0
h j X j =0 2
Esta es una sumatoria que no habiamos visto nunca hasta el momento, para resolverla hay que aplicar algunos trucos. Sabemos que.
X
1
j =o
1 xj = 1 ; x
X
1
1 jxj 1 = (1 ; x)2 j =0
;
X
1
j =0
x jxj = (1 ; x)2
Xj
1
Que es la sumatoria que necesitabamos, en nuestro caso no vamos hasta in nito pero como la serie in nita esta acotada la podemos usar para una aproximacion muy valida. h X X T (n) = 2h 2jj 2h 2jj 2h 2 = 2h+1
1
Como n = 2h+1 ; 1 tenemos que T (n) n + q = O(n). Y claramente el algoritmo debe acceder por lo menos una vez a cada elemento del arreglo por lo que el tiempo de BuildHeap es (n). 86
j =0
j =0
Esta es la segunda vez que un algoritmo con dos ciclos anidados termina resultando lineal si lo analizamos detalladamente, el secreto de BuildHeap reside en que el procedimiento es mas e ciente para los niveles mas bajos del arbol ya que alli realiza menos trabajo y en un arbol binario el 87:5 por ciento de los nodos del arbol residen en los tres ultimos niveles.
HeapSort HeapSort es un algoritmo de orden (n log n), que funciona \insitu" pero no es estable.
87
Es de destacar que algunos de los nodos del arbol son inalcanzables, por ejemplo la cuarta hoja desde la izquierda indica que a1 a2 y que a1 > a2 lo cual es imposible en cicunstancias normales. Esto explica porque algunos algoritmos de sort son ine cientes, por realizar comparaciones de mas que resultan innecesarias. Sea T (n) la cantidad minima de comparaciones que un algoritmo de sort debe realizar, observemos que T (n) es exactamente la altura del arbol de decision e indica cuantas comparaciones se hicieron hasta llegar a determinar cual es el ordenamiento de los elementos del vector. Un arbol binario como sabemos tiene a lo sumo 2T (n) hojas distintas, que es la cantidad de resultados posibles del algoritmo de sort. A esta cantidad la llamaremos A(n). El resultado del algoritmo es una cualquiera de las permutaciones posibles para n elementos, y hay n! permutaciones posibles, por lo tanto el algoritmo debe ser capaz de generar n! permutaciones posibles por lo que debe satisfacer. A(n) = 2T (n) A(n) n! 2T (n) n! T (n) log n! Para aproximar n! se suele utilizar la aproximacion de Stirling. p n n! 2 n( n e) p n T (n) log( 2 n( n e) ) p T (n) log 2 n + n log n ; n log e 2 (n log n) 88
De acuerdo a lo anterior acabamos de probar que cualquier algoritmo de sort basado en la comparacion de elementos es (n log n), con lo cual los algoritmos de MergeSort, QuickSort y HeapSort son asintoticamente optimos, no se puede ordenar mediante comparaciones en un orden menor.
El algoritmo de counting sort se basa en la hipotesis de que los elementos son numeros enteros de rango 1::k el algoritmo funciona en orden (n + k) .Si k es (n) entonces el algoritmo resulta lineal. La idea basica es determinar para cada elemento en el vector de entrdanda cual sera su rango en el vector resultado. Una vez determinado el rango de cada elemento simplemente se lo copia en su posicion y se obtiene el vector ordenado. La clave reside en determinar el rango de un elemento si compararlo contra los demas.
El metodo
El algoritmo utiliza tres vectores: A 1::n] es el vector original, donde la clave de un elemento A j ] es A j ]:key. B 1::n] es un vector de registros donde almacenaremos el resultado (claramente el algoritmo no funciona in-situ). R 1::k] un vector de enteros, R x] es el rango de x en A donde x 2 1::k]. El algoritmo es notablemente simple e ingenioso en primer lugar se construye R, esto se hace en dos pasos. En primer lugar inicializamos R x] como la cantidad de elementos de A cuya clave es igual a x. Esto se logra inicializando R en cero y luego por cada j de 1 hasta n incrementamos R A j ]:key] en 1. Para determinar la cantidad de elementos menores o iguales a x reemplazamos luego R x] con la suma de los elementos en el vector R 1::x]. Una vez armado R el vector contiene el rango de cada elemento, luego recorriendo el vector original se toma cada elemento y se accede a R para determinar su posicion en el vector resultado, luego de posicionar el elemento x en B R x]] decrementamos R x] en una unidad. 89
end for for j = 1 to n do R A j ]:key] + + end for for x = 2 to k do R x] ( R x] + R x ; 1] end for for j = n downto 1 do x ( A j ]:key B R x]] ( A j ] R x] ( R x] ; 1 end for
Analisis
El algoritmo consta de cuatro ciclos no-anidados que insumen O(k + n + (k ; 1) + n) por lo que el orden del algoritmo es O(n + k) si kesO(n) entonces el algoritmo es (n). La gura muestra un ejemplo del algoritmo, se recomienda realizar un seguimiento para convencerse de su funcionamiento.
90
El algoritmo de CountingSort es un algoritmo estable, esto se debe al ultimo ciclo que corre desde n hasta 1, si se hiciera desde 1 hasta n no seria estable. Para ordenar enteros de tamanio chico el algoritmo funciona en orden lineal y es claramente recomendable.
La principal desventaja del algoritmo CountingSort es que solamente es aplicable, debido al costo espacial, para enteros de tamanio chico. Si tenemos enteros que pueden valer entre 1 y1000000 no vamos a querer contar con un vector auxiliar de un millon de posiciones. Radix sort provee un metodo para ordenar enteros grandes ordenando digito por digito. Una aplicacion tipica de RadixSort es ordenar strings considerando que cada digito es un caracter (1 byte) del string. El algoritmo es extremadamente simple y funciona en base al mismo principio que usaban las maquinas que servian para ordenar tarjetas perforadas hace ya algunos anios. En primer lugar ordena el vector de acuerdo al ultimo digito de cada clave, luego repite el proceso para los restantes digitos usando 91
end for
Analisis
El orden del algoritmo es (dn), por lo que se trata de un algoritmo de orden lineal. Hay que tener cuidado ya que el algoritmo se aplica unicamente cuando el tamanio de los numeros a ordenar es jo en cuanto a la cantidad de digitos, si no fuera asi la cantidad de digitos de un numero n es d = log n y el orden pasa a ser (n log n). Por lo que equivale al QuickSort, HeapSort, aunque con un overhead mucho mayor que estos ultimos. RadixSort usando CountingSort es extremadamente e ciente para ordenar numeros de una cierta cantidad ja de digitos o bien para ordenar strings de una cierta cantidad de caracteres. Ademaas es necesario contar con una buena cantidad de elementos a ordenar ya que los factores constantes que acompanian al algoritmo de RadixSort suelen ser grandes y las ventajas del metodo recien se hacen evidentes para colecciones de datos relativamente grandes.
se base en la comparacion de claves puede ordenar una secuencia en menos de (n log n). Para nalizar observamos que en situaciones especiales se pueden aplicar metodos de sort que no necesitan comparar claves para ordenar un conjunto de elementos. CountingSort y RadixSort son dos algoritmos de orden lineal que pueden aplicarse en algunas situaciones. Algoritmo Peor Caso Caso Medio Mejor Caso Estable In-Situ Burbujeo O(n2 ) O(n2 ) O(n2 ) Si Si 2 2 Seleccion O(n ) O(n ) O(n2 ) No Si Insercion O(n2 ) O(n2 ) O(n) Si Si MergeSort O(n log n) O(n log n) O(n log n) Si No QuickSort O(n2 ) O(n log n) O(n log n) No Si HeapSort O(n log n) O(n log n) O(n log n) No Si CountingSort O(n) O(n) O(n) Si No RadixSort O(n) O(n) O(n) Si No
6.6 Ejercicios
1. El siguiente algoritmo de sort fue creado por Howard.
Algorithm 29 STOOGE-SORT(A,i,j) if A i] > A j] then Swap(A i],A j]) end if if i + 1 j then return end if k b(j ; i + 1)=3c
STOOGE-SORT(A,i,j-k) STOOGE-SORT(A,i+k,j) STOOGE-SORT(A,i,j-k)
(a) Validar el algoritmo. (b) Dar una recurrencia para el peor caso de STOOGE-SORT. (c) Analizar el algoritmo y compararlo con los demas algoritmos estudiados, indicar si este algoritmo merece atencion. 2. Indicar como implementar una cola FIFO usando Heaps, indicar como implementar una pila usando Heaps. 3. Dar un algoritmo O(log n) para el procedimiento Heap-Increase-Key(A,i,k). Que asigna a A i] el maximo entre A i] y k y ademas actualiza la estructura del Heap en forma acorde. 93
4. Un Heap n-ario es similar a los heaps estudiados pero cada nodo en lugar de tener 2 hijos puede tener hasta n. (a) Indicar como se representaria este tipo de Heap en un vector. (b) Indicar cual es la altura de un Heap d-ario con n elementos. Expresarlo en funcion de n y d. (c) Indicar como implementar Extract-Max en un Heap d-ario, analizar el orden del algoritmo en funcion de d y n. (d) Indicar como implementar Insert en un Heap d-ario, analizar el orden del algoritmo en funcion de d y n.
94
Chapter 7
Grafos
7.1 Grafos
El proposito de esta clase es prestar atencion a aquellos algoritmos relacionados con grafos. Basicamente un grafo es un conjunto de \nodos" o \vertices" interconectados por un conjunto de \aristas". Los grafos son estructuras discretas muy importantes ya que permiten modelar un sinnumero de situaciones. Practicamente cualquier problema que presente una serie de objetos y relaciones inter-objeto puede representarse adecuadamente con un grafo. Las aplicaciones que involucran el uso de grafos son tan numerosas que no podriamos iniciar una lista sin que la misma quedara sumamente incompleta. Para comenzar vamos a revisar algunas de niciones sobre grafos.
95
En ungrafo no dirigido o simplemente, grafo, G = (V E ) las aristas estan representadas por un par no-ordenado de vertices distintos, por lo tanto los \loops" no estas permitidos. En la gura el grafo de la derecha es un grafo no dirigido caracterizado por V = f1 2 3 4g y E = f(1 2) (1 3) (1 4) (2 4) (3 4)g.
Decimos que un vertice w es adyacente a un vertice v si existe una arista de v hacia w. En un grafo no-dirigido decimos que una arista es incidente en un vertice si el vertice es punto de llegada o partida de una arista. En un grafo dirigido decimos que una arista entra o sale de un vertice por lo que hablamos de aristas entrantes o salientes.
Un grafo es pesado si sus aristas estan rotuladas con valores numericos lamados pesos. El signi cado del peso depende de la aplicacion, por ejemplo puede ser la distancia entre los vertices, la capacidad de la arista, etc.
En un digrafo el numero de aristas que salen de un determinado vertice determina el grado de salida (Grs) del vertice, a su vez las aristas que entran al vertice determinan el grado de entrada (Gre) vertice . En un grafo no dirigido se habla simplemente del grado (Gr) de un vertice como la cantidad de aristas incidentes en dicho vertice. El grado de un grafo es el grado maximo de sus vertices.
v2V
Gre(v) =
v 2V
Grs(v) = jE j
En un grafo dirigido un camino es una secuencia de vertices (v0 v1 : : : vn) en donde vale que (vi 1 vi) es una arista para i = 1 2 : : :n. La longitud del camino es la cantidad de aristas n del mismo. Decimos que w es alcanzable desde u si existe un camino donde v0 = u vn = w. Todo vertice es alcanzable desde si mismo por un camino de longitud cero. Un camino es simple si todos sus vertices (excepto a veces el primero y el ultimo) son distintos. Un ciclo es un camino que contiene al menos una arista y para el cual vale que vo = vn, un ciclo es simple si, ademas, v1 : : : vn son distintos. Un \loop" es un ciclo simple de longitud 1. En un grafo no-dirigido de nimos caminos y ciclos de la misma forma pero para un ciclo simple agregamos el requisito de que el ciclo visite al menos tres vertices distintos, esto sirve para eliminar el ciclo degenerado (v u v) que implicaria ir y venir siempre por la misma arista.
Un ciclo Hamiltoniano es un ciclo que visita todos los vertices de un grafo exactamente una vez. En un camino Hamiltoniano no hace falta regresar al vertice inicial. Un ciclo Euleriano es un ciclo (no necesariamente simple) qye visita cada arista del grafo exactamente una vez. En un camino Euleriano no hace falta regresar al vertice inicial.
Uno de los problemas que motivo tempranamente el interes por la teoria de grafos es el Problema del puente de Konigsberg. Esta ciudad sobre el rio Pregel esta unida por 7 (siete) puentes. El problema es si es posible cruzar los siete puentes sin pasar mas de una vez por cada puente. Euler mostro que tal cosa 97
era imposible enunciando las caraceristicas que debia tener un grafo para que exista un camino Euleriano.
Para que un grafo tenga un camino Euleriano todos salvo a lo sumo dos de los vertices deben tener grado par. En el grafo de Konigsberg los 4 vertices tienen grado impar.
Un grafo es conexo si todo vertice es alcanzable desde cualquier otro. Un grafo aciclico y conexo se denomina arbol, estos arboles se conocen tambien como arboles libres para destacar el hecho de que no poseen raiz. Un arbol es un grafo minimamente conexo ya que al eliminar una arista cualquiera del grafo el mismo deja de ser conexo. Ademas existe un unico camino entre dos vertices de un arbol. El agregado de una arista a un arbol implica la creacion de un ciclo.
La propiedad de vertice alcanzable es una relacion de equivalencia entre los vertices, es decir que es re exiva (un vertice es alcanzable desde si mismo), es simetrica y es transitiva. Esto implica que la propiedad particiona los vertices del grafo en clases de equivalencia. Estas clases se denominan componentes
98
Un grafo conexo tiene un unico componente conexo. Un grafo aciclico (no necesariamente conexo) consiste de una serie de arboles y se lo denomina bosque.
Un grafo dirigido es fuertemente conexo si para cualquier par de vertices u y v, u es alcanzable desde v y v es alcanzable desde u. Los grafos fuertemente conexos particionan sus vertices en clases de equivalencia denominadas componentes
Un grafo dirigido aciclico se denomina GDA (Grafo Dirigido Aciclico) y es distinto de un arbol dirigido.
Dos grafos G = (E V ) y G = (E V ) son isomorfos si hay una funcion biyectiva f : V ! V tal que (u v) 2 E si y solo si (f (u) f (v)) 2 E . Los grafos isomorfos son esencialmente \iguales" con la excepcion de que sus vertices tienen distintos nombres.
0 0
Determinar si dos grafos son isomorfos no es tan sencillo como parece. Por ejemplo consideremos los grafos en la ilustracion. Claramente (a) y (b) se parecen mas entre si que con respecto a (c) sin embargo esto es solo una ilusion optica. Observemos que los tres casos todos los vertices tienen grado 3 y que hay ciclos simples de longitud 4 en (a) pero que en (b) y (c) los ciclos simples mas chicos son de longitud 5. Esto implica que (a) no puede ser isomorfo a (b) ni a (c). Resulta que (b) y (c) si son isomorfos. Uno de los isomor smos 99
posibles es el que damos a continuacion, la notacion (u ! v) signi ca que el vertice u en el grafo (b) es mapeado al vertice v en el grafo (c).
Un grafo G = (E V ) es un subgrafo de G = (E V ) si V V y E E . Dado un subconjunto V V el subgrafo inducido por V' es el grafo G = (V E ) donde: E = f(u v) 2 Etalqueu v 2 V g
0 0 0 0 0 0
Es decir que se toman todas las aristas de G que inciden sobre vertices de V .
0
100
Un grafo no-dirigido que tiene el numero maximo posible de aristas se denomina grafo completo. Los grafos completos se suelen denotar con la letra K . Por ejemplo K5 es el grafo completo con 5 vertices. Dado un grafo G un subconjunto de vertices V V forma un clique su ek subgrafo inducido por V es completo. En otras palabras si todos los vertices de V son adyacentes unos con otros.
0 0
Un subconjunto de vertices V forman un conjunto independiente si el subgrafo inducido por V no tiene aristas. Por ejemplo en la gura (a) el subconjunto f1 2 4 6g es un clique y el f3 4 7 8g es un conjunto independiente.
Un grafo bipartito es un grafo no-dirigido en donde los vertices pueden ser particionados en dos conjuntos V 1 y V 2 de forma tal que todas las aristas van de un vertice de V 1 a un vertice de V 2. El grafo en la gura (b) es un grafo bipartito.
El complemento de un grafo G = (E V ) denotado G 1 es un grafo sobre el mismo conjunto de vertices pero en el cual las aristas han sido complementadas. La reversa de un grafo dirigido denotado GR es un grafo dirigido sobre el mismo conjunto de vertices en donde la direccion de las aristas ha sido cambiada, a este grafo se lo denomina tambien grafo transpuesto GT
101
Un grafo es planar si puede ser dibujado en el plano de forma tal que no se cruce ningun par de aristas. Los grafos planares son una familia importante de los grafos ya que se usan en muchas aplicaciones desde los sistemas de informacion geogra ca hasta circuitos y modelado de solidos. En general hay muchas formas de dibujar un grafo planar en dos dimensiones. Por ejemplo en la proxima gura mosramos dos dibujos esencialmente diferentes del mismo grafo. Cada uno de estos dibujos se denomina forma planar. Los vecinos de un vertice son los vertices adyacentes al mismo. Una forma planar esta determinada para el ordenamiento antohorario de los vecinos de cada uno de los vertices. Por ejemplo en la forma de la izquierda los vecinos del vertice uno en sentido antohorario son f2 3 4 5g pero en la forma de la derecha el ordenamiento es f2 5 4 3g y por eso ambas formas son diferentes.
Caras
Un hecho importante acerca de las formas planares de un grafo es que subdividen al grafo en regiones denominadas caras. Por ejemplo en la gura de la izquierda la gura triangular limitada por los vertices f1 2 5g es una cara. Siempre existe una cara denominada cara externa que rodea a todo el grafo. Las formas planares de las guras tienen por ejemplo 6 caras en ambos casos. Dos formas planares siempre tienen el mismo numero de caras, esto se desprende de la
formula de Euler
102
Formula de Euler
Una forma planar de un grafo conexo con V vertices, E aristas y F caras satisface: V ;E +F = 2 En los ejemplos ambos grafos tienen 5 vertices y 9 aristas y por lo tanto de la formula de Euler deben tener F = 2 ; V + E = 2 ; 5 + 9 = 6 caras.
7.2.20 Tamanios
Al referirnos a grafos y digrafos vamos a utilizar que n = jV j y e = jE j. Los tiempos de ejecucion de los algoritmos que operan con grafos suelen depender de n y e por lo que se bueno ver que relacion existe entre ambos numeros.
Observacion Para un grafo dirigido e n2 = O(n2). Para un grafo no n = n(n ; 1)=2 = O(n2 ) dirigido e 2
Decimos que un grafo es disperso cuando e es mucho mas chico que n2. Para el importante caso de los grafos planares e = O(n). En la mayoria de las aplicaciones los grafos muy grandes tienden a ser dispersos. Esto es importante al diseniar algoritmos que utilicen grafos porque, cuando n es realmente grande un tiempo de respuesta del orden de O(n2 ) a menudo es inaceptable.
Es una matriz de nxn de nida para 1 v w n donde. si(v w) 2 E A v w] = f 1 0 sino Si el grafo dirigido tiene pesos podemos almacenar los pesos en la matriz. Por ejemplo si (v w) 2 E entonces A v w] = W (v w) (el peso de la arista (v w)).
103
Si (v w)not 2 E entonces en general W (v w) no necesita ser de nido pero a menudo se inicializa en un valor especial como por ejemplo ;1 o 1 1
Es un vector Adj 1::n] de punteros donde para 1 v n Adj v] apunta a una lista enlazada que contiene los vertices que son adyacentes a v (los vertices que son alcanzables desde v usando una sola arista). Si las aristas tienen pesos los pesos pueden almacenarse en las listas.
Los grafos no-dirigidos se pueden representar de la misma forma pero estariamos guardando cada arista dos veces. En particular representariamos la arista (v w) como el par de aristas (v w)y(w v). Esto puede causar algunas complicaciones, por ejemplo un algoritmo que marca aristas de un grafo deberia tener cuidado de marcar dos aristas en lugar de una sola si el grafo es no-dirigido ya que las dos aristas en realidad representan la misma. Al almacenar la lista de adyacencias puede resultar poco e ciente recorrer todas las listas por lo que suelen incluirse cross links como se muestra en la ilustracion. 104
1 1 obviamente es un numero que es mayor que el peso maximo posible, o el valor maximo almacenable en una variable del tipo que se esta usando
Una matriz de adyacencias requiere (n2 ) 2 espacio, una lista de adyacencias requiere (n + e). Para grafos dispersos la lista de adyacencias es mas e ciente. En la mayoria de las aplicaciones tanto la matriz de adyacencias como la lista de ayacencias son representaciones utiles para un grafo.
7.4.1 BFS
El algoritmo BFS se basa en recorrer el grafo \a lo ancho primero", Dado un grafo G = (V E ) BFS comienza con un vertice cualquiera s y \descubre" todos los vertices que son adyacentes a s. En un momento cualquiera del algoritmo existe una \frontera" de vertices que han sido descubiertos pero no procesados.
2 3
Estamos usando tambien esta notacion para el costo en espacio! Recordemos los algoritmos para recorrer un arbol: pre-order, post-order e in-order
105
Inicialmente todos los vertices excepto el origen son pintados de color blanco, lo cual quiere decir que aun no han sido observados. Cuando un vertice es descubierto se lo pinta de color gris (y pasa a ser parte de la forntera), cuando un vertice ya fue procesado (se descubrio toda su frontera) se lo pinta de color negro.
end for color s] ( gris //Inicializamos el origen. encolar(Q s) while Q not empty do u ( desencolar(Q) for each v in Adj u] do if color v]==blanco then color v] ( gris encolar(Q v) end if end for color u] ( negro end while
106
BFS hace uso de una cola FIFO donde los elementos son obtenidos en el mismo orden en que fueron insertados. Mantienen tambien el vector color u] que contiene el color del vertice u (blanco=no observado, gris= observado pero no procesado, negro= procesado). El funcionamiento del algoritmo puede requerir realizar algunos seguimientos para poder comprenderlo en totalidad.
BFS suele ser muy utilizado para calcular la distancia minima desde un vertice dado hasta cualquier otro de un grafo no-dirigido, esto es muy sencillo de hacer porque BFS recorre los vertices en orden de distancia \creciente" a partir del origen. Esto se logra realizando las siguientes modi caciones al algoritmo.
end for color s] ( gris //Inicializamos el origen. d s] ( 0 encolar(Q s) while Q not empty do u ( desencolar(Q) for each v in Adj u] do if color v]==blanco then color v] ( gris d v ] ( d u] + 1 pred v] ( u encolar(Q v) end if end for color u] ( negro end while
Hemos agregado un vector de distancias que para cada vertice v indicara a que distancia esta dicho vertice del vertice origen s, ademas para saber cual es 107
el camino que debemos hacer para obtener dicha distancia almacenamos en cada vertice cual es el vertice anterior al mismo en el camino minimo que parte de s y llega a dicho vertice. Esto es valido debido al principio de optimalidad que dice que si un camino es minimo entonces todos sus sub-caminos tambien lo son.
En la gura podemos observar que los punteros predecesores de BFS de nen un arbol invertido. Si invertimos las aristas obtenemos un arbol desordenado denominado Arbol BFS de G para s.Las aristas de G que forman el arbol se denominan aristas del arbol y el resto de las aristas se denominan aristas de cruce. En la gura podemos observar el arbol BFS para nuestro ejemplo.
108
Analisis
El tiempo de ejecucion de BFS es muy similar al tiempo de ejecucion de muchos algoritmos que recorren grafos. Si n = jV j y e = jE j. Observamos que la inicializacion requiere (n). El ciclo que recorre el grafo es el nucleo del algoritmo. Como nunca visitamos un vertice dos veces la cantidad de veces que ejecutamos en el ciclo while es a lo sumo n (es exactamente n si todos los vertices son alcanzables desde s). El numero de iteraciones dentro del ciclo interior es proporcional a grado(u) + 14. Sumando para todos los vertices tenemos que el tiempo insumido es: T (n) = n +
v2V
(grado(u) + 1 = n +
v 2V
grado(u) + n = 2n + 2e = (n + e)
7.4.2 DFS
Una de las mayores di cultades al diseniar algoritmos para grafos es la falta de estructura de los mismos. Al ordenar un vector, por ejemplo, vimos que era facil particionar el problema para aplicar un algoritmo de tipo \Divide & Conquer". Sin embargo no es en absoluto claro como dividir un grafo en subgrafos. Lo importante de cualquier estrategia para recorrer grafos es la habiliad para imponer algun tipo de estructura al grafo. En BFS vimos como podemos ver al grafo como un arbol (el arbol BFS) mas un conjunto de aristas (aristas de cruce). Los arboles son objetos mucho mas estructurados que los grafos, por ejemplo los arboles se pueden subdividir elegantemente en sub-arboles para los cuales el problema es resuelto en forma recursiva. En un grafo aun nos quedaria el problema de como manejar las aristas que no forman parte del arbol BFS. El
4 El +1 es debido a que si grado(u) = 0 necesitamos una cantidad de tiempo constante para incializar el ciclo
109
algoritmo DFS (Depth rst search), tiene como propiedad inetersante que las aristas no-arboladas de nen una estructura matematicamente manejable. Consideremos el problema de buscar un tesoro en un castillo. Para resolver podriamos usar la siguiente estrategia. Cada vez que entramos a una habitacion del castillo pintamos un gra ti en la pared para saber que ya estuvimos alli 5 . Sucesivamente viajamos de habitacion en habitacion mientras lleguemos a habitaciones en las cuales nunca estuvimos. Una vez que volvemos a una habitacion en la cual ya estuvimos probamos con una puerta diferente a la usada anteriormente. Cuando todas las puertas han sido utilizadas volvemos a la habitacion anterior y repetimos el procedimiento. Notemos que este procedimiento es descripto en forma recursiva. En particular cuando entramos a una nueva habitacion estamos comenzando una nueva busqueda. Este es el principio de funcionamiento del algoritmo DFS.
mismo algoritmo funciona para grafos no dirigidos pero la estructura resultante es distinta). Vamos a usar cuatro vectores auxiliares. Como antes se mantiene un color por cada vertice, blanco signi ca vertice no descubierto (habitacion sin gra ti en la pared). Gris signi ca vertice descubierto pero no procesado (tiene gra ti pero aun no usamos todas sus puertas). Negro signi ca vertice ya procesado. Al igual que en BFS tambien almacenamos los punteros al vertice predecesor apuntando hacia atras hacia el vertice a partir del cual descubrimos el vertice actual. Tambien vamos a asociar dos numeros a cada vertice que denominamos time stamps. Cuando descubrimos un vertice u por primera vez guardamos un contador en d u] y cuando terminamos de procesar un vertice guardamos un contador en f u]. El proposito de estos contadores sera explicado mas adelante.
Algorithm 32 DFS(G). Recorre el grafo G en orden DFS. for each u in V do color u] ( blanco pred u] ( NULL end for time ( 0 for each u in V do if color u]==blanco then DFSVisit(u) end if end for
5
110
Algorithm 33 DFSVisit(G,u). color u] ( gris d u] ( time + 1 for each v in Adj(u) do if color v]==blanco then pred v] ( u DFSVisit(v) end if end for color u] ( negro f u] ( time + 1
Analisis
El tiempo de ejecucion de DFS es (n + e). Esto es bastante mas dificil de observar con respecto al analisis de BFS debido a la naturaleza recursiva del algoritmo. Normalmente una recurrencia es una buena forma de calcular el tiempo de ejecucion del algoritmo pero esto no ocurre aqui porque no tenemos nocion del tamanio de cada llamada recursiva. En primer lugar observemos que ignorando las llamadas recursivas DFS corre en O(n). Observemos que cada vertice es visitado exactamente una vez en la busqueda y que por lo tanto DFSV isit() es llamado exactamente una vez por cada vertice. Podemos analizar cada llamada individualmente y sumar sus tiempos de ejecucion. Ignorando el tiempo necesario para las llamadas recursivas podemos ver que cada vertice u puede ser procesado en O(1 + Gradosalida(u)). Por lo tanto el tiempo total del procedimiento es. X X T (n) = n+ (Gradosalida(u) + 1) = n+ Gradosalida (u) + n = 2n+e = (n+e)
uinV uinV
111
El algoritmo DFS impone en el grafo una estrutura de arbol (en realidad una coleccion de arboles). Este arbol no es mas que el arbol de recursion donde la arista (u v) surge cuando porcesando el vertice u llamamos a DFSV isit(v). Las aristas que forman parte del arbol de recursion son llamadas aristas del arbol DFS. Para grafos dirigidos las restantes aristas del grafo se pueden clasi car de la siguiente manera. Aristas de retroceso (u v) donde v es un antecesor (no necesariamente propio) de u en el arbol. Por lo tanto un \loop" es considerado una arista de retroceso. Aristas de avance (u v) donde v es un sucesor de u en el arbol. Aristas de cruce (u v) donde u y v no son ni antecesor ni sucesor entre si. Aun mas, la arista podria conectar dos arboles distintos. No es dificil clasi car las aristas de un arbol DFS analizando el valor del color de cada vertice y/o considerando los time-stamps. En un grafo no-dirigido no hay distincion entre aristas de avance y de retroceso por lo que las llamaremos aristas de retroceso en forma uniforme. Ademas no pueden existir aristas de cruce.
Estructuras a partir de los Time-Stamps Los time-stamps tambien imponen al grafo una estrutura interesante, en algunos libros esta estructura se conoce como estructura de intervalos. Teorema: (Teorema de los intervalos). Dado un digrafo G = (V E ) y un arbol DFS para G y dos vertices cualesquiera u v 2 V .
u es un sucesor de v si y solo si d u] f u]] d v] f v]] u es un antecesor de v si y solo si d u] f u]] d v] f v]] u no tiene relacion con v si y solo si d u] f u]] y d v] f v]] son disjuntos. Notacion: x,y] es un intervalo perteneciente a los numeros naturales.
112
muchos datos importantes sobre un grafo dirigido o no-dirigido. Por ejemplo podemos determinar si el grafo contiene o no ciclos. Esto lo podemos hacer gracias a los siguientes lemas.
surge directamente del teorema de los intervalos. (Por ejemplo para una arista de avance (u v) v es un sucesor de u, y por lo tanto el intervalo de v esta contenido dentro del de u implicando que f u] > f v] pues f v] fue rotulado antes que f u].). Para una arista de cruce (u v) sabemos que los intervalos son disjuntos. Cuando procesamos u, v no fue blanco (sino (u v) seria una arista del arbol), eso implica que v fue inspeccionado antes que u, y como los intervalos son disjuntos v tiene que haber nalizado antes que u.
Demostracion: (() Si hay una arista de retroceso (u v) entonces v es un predecesor de u y siguiendo las aristas del arbol DFS de v hacia u obtenemos un ciclo. ()) Supongamos que no hay aristas de retroceso. Por el teorema anterior
cada uno de los tipos de aristas restantes tienen la propiedad de que van de vertices con rotulo f mayor que el valor del vertice hacia el cual se dirigen. Por lo tanto siguiendo cualquier camino el tiempo disminuye monotonamente implicando que no puede haber un ciclo.
Consideremos un GDA (grafo directo aciclico). El cual es muy utilizado en aplicaciones en las cuales se muestra precedencia entre objetos (tareas, eventos, procesos, etc). En este tipo de aplicaciones una arista (u v) implica que la tarea u debe nalizarse antes de empezar la tarea v. Un ordenamiento topologico de un GDA es un ordenamiento lineal de los vertices del grafo de forma tal que para cada arista (u v) u aparezca antes que v en el ordenamiento. En general hay varios ordenamientos topologicos validos para un GDA. Obtener el ordenamiento topologico es sencillo utilizando DFS. Por el lema anterior para cada arista (u v) en un GDA el tiempo f de u es mayor que el tiempo f de v. Por lo tanto es su ciente devolver los vertices ordenados en orden decreciente de tiempo f . Esto puede hacerse en forma muy sencilla al mismo tiempo que se utiliza el algoritmo DFS . En el siguiente algoritmo hemos modi cado el algoritmo DFS para que solamente realice el ordenamiento topologico del grafo dejando de lado todo lo demas.
Ordenamiento Topologico
Algorithm 34 TopSort(G). Realiza el ordenamiento topologico del grafo G. Require: G no debe tener ciclos. for each u en V do color u] ( blanco end for L ( new(LinkedList) for each u en V do if color u]=blanco then TopVisit(u) end if end for
return L
Algorithm 35 TopVisit(u,G). color u] ( gris for each v in Adj(u) do if color v]=blanco then TopVisit(v) end if end for
Append u a la cabeza de L
114
Analisis Al igual que el algoritmo DFS TopSort es de orden (n + e) Componentes fuertemente conexos
Un problema importante realtivo a los grafos dirigidos es su conectividad, cuando este tipo de grafos son utilizados para modelar redes de comunicaciones o redes de transporte es deseable saber si dichas redes son completas en el sentido de que cualquier nodo es accesible desde cualquier otro. Esto se da cuando el digrafo que modela la red es fuertemente conexo. Es deseable tener un algoritmo que determine si un grafo es fuertemente conexo. De hecho vamos a resolver un problema un tanto mas dificil que consiste en calcular los componentes fuertemente conexos cfc de un digrafo. En particular vamos a particionar el grafo en subconjuntos de vertices de forma tal que el subgrafo inducido por cada subconjunto es fuertemente conexo. Estos subconjuntos deberan ser lo mayor posibles en tamanio. Si mergeamos los vertices de cada componente fuertemente conexo (cfc) en un super vertice y si juntamos dos supervertices (A B ) si y solo si existen vertices u 2 A y v 2 B tales que (u v) 2 E el digrafo resultante llamado digrafo componente es necesariamente aciclico. Si fuera ciclico entonces los dos cfc's podrian formar un cfc de mayor tamanio.
Hemos introducido este tema debido a que el algoritmo que presentamos como solucion es un verdadero ejemplo de disenio. Es extremadamente simple, e ciente e ingenioso, a tal punto que resulta dificil ver como funciona. Vamos a 115
dar algunas pautas intuitivas sobre el funcionamiento del algoritmo aunque, no vamos a llegar a demostrar formalmente su correccion. Consideremos para entrar en tema el recorrido DFS del grafo ilustrado (izquierda). Observemos que cada cfc es un sub-arbol del bosque DFS. Esto no siempre es verdad para cualquier DFS pero siempre existe una forma de ordenar el DFS de forma tal que esto sea verdad.
Supongamos que conocemos el grafo componente por adelantado. 6, supongamos ademas que tenemos un ordenamiento topologico rebatido del grafo componente. Es decir que si (u v) es una arista en el grafo comoponente entonces v aparece antes que u en el ordenamiento. Ahora corramos DFS pero cada vez que necesitamos un nuevo vertice para iniciar la busqueda tomamos el proximo vertice disponible de la lista rebatida del ordenamiento topologico. Como consecuencia interesante de este procedimiento cada arbol en el bosque DFS sera un cfc (magia?).
cfc debe visitar cada vertice del componente ( y posiblemente otros) antes de nalizar. Si no empezamos en orden topologico inverso la busqueda puede \escaparse" hacia otros cfc colocandolos en el mismo arbol DFS. En la gura de la
6 Esto es ridiculo porque necesitariamos conocer los cfc y este es el problema que queremos resolver, pero permitanme la disgresion por ahora.
116
derecha, por ejemplo, la busqueda empezoen el vertice a, no solo se visita dicho componente sino tambien otros componentes. Sin embargo visitando componentes en orden inverso de acuerdo al ordenamiento topologico impedimos que la busqueda se escape hacia otros componentes. Esto nos deja con la intuicion de que si de alguna manera pudiesemos ordenar el DFS entonces tendriamos un algoritmo sencillo para calcular los cfc, sin embargo no sabemos el aspecto que tiene el grafo componente. El truco es que podemos encontrar un ordenamiento de los vertices que tiene la propiedad necesaria sin realmente calcular el grafo componente. Desafortunadamente es dificil explicar como funciona este criterio. Presentaremos el algoritmo y sepan disculpar la falta de formalidad en su explicacion. Recordemos que GR es el grafo transpuesto.
Algorithm 36 StrongComp(G)
Run DFS(G), computando f u] para cada vertice u R ( Reverse(G) //R es el grafo transpuesto. Ordenar los vertices de R (counting sort) en orden decreciente de f u] Run DFS(R) utilizando dicho ordenamiento de los vertices. Cada arbol DFS es un CFC
Todos los pasos del algoritmo son sencillos de implementar, y trabajan en O(n + e)
117
fuertemente conexos en un grafo sirve como ilustracion de un algoritmo extremadamente elegante en cuanto a su disenio y nos permite observar que el manejo de grafos puede ser realmente delicado. Con esto ha sido su ciente por ahora, pero hay muchos problemas con grafos que aun no hemos estudiado y que vamos a analizar en los temas subsiguientes, por eso es recomendable mantener estos temas presentes.
7.6 Ejercicios
1. Dar un algoritmo de orden lineal o mejor que dado un grafo conexo G devuelva un vertice de forma tal que la eliminacion de dicho vertic no desconecte el grafo. 2. Dado el siguiente grafo.
(a) Indicar todas las propiedades que pueda sobre el grafo. (b) Realizar una busqueda BFS sobre el grafo a partir del nodo A. (c) Realizar una busqueda DFS sobre el grafo a partir del nodo A. Dar el ordenamiento topologico del grafo, indicar si el grafo tiene ciclos. (d) Indicar cuales son los componentes fuertemente conexos de un grafo. 118
119
Chapter 8
Programacion dinamica
8.1 Introduccion a la programacion dinamica
La programacion dinamica es una tecnica muy valiosa para el disenio de algoritmos, se aplica a problemas de optimizacion, es decir aquellos problemas en los cuales de un conjunto de soluciones posibles (que en general es un numero grande) se debe elegir aquella que maximiza o minimiza un cierto parametro.
los problemas que se pueden resolver utilizando programacion dinamica, dichas caracteristicas son: 1. Subestructura-optima 2. Superposicion de subproblemas.
Subestructura optima
Decimos que un problema exhibe subestructura optima si una solucion optima al problema en cuestion contiene soluciones optimas a los subproblemas involucrados.
Superposicion de subproblemas
La segunda caracteristica a observar en un problema para que el metodo de programacion dinamica sea aplicable es que la solucion de los distintos subproblemas implica la solucion de sub-sub-problemas de forma tal que un sub-sub-problema es requerido para solucionar mas de un subproblema. Almacenando el resultado de los sub-sub-problemas en una tabla es posible evitar recalcularlos cada vez. A continuacion veremos algunos problemas clasicos que se resuelven por programacion dinamica.
Dada una secuencia de n matrices hA1 A2 : : : Ani donde la cantidad de las de Ai es Ai 1 y la cantidad de columnas es Ai = Aj 1 (la cantidad de columnas de Ai debe ser igual a la cantidad de las de Ai+1 para poder multiplicarlas. Queremos calcular el producto. A1 A2 : : : An De forma tal de utilizar la menor cantidad de multiplicaciones posibles, como el producto de matrices es asociativo resulta que hay varias formas de multiplicar la cadena de matrices. Por ejemplo cuatro matrices pueden multiplicarse de las siguientes formas. (A1 (A2 (A3 A4 ))) (A1 ((A2 A3 )A4 )) ((A1 A2)(A3 A4 )) ((A1 (A2A3 ))A4 ) (((A1 A2)A3 )A4 ) 121
La forma en la cual asociamos la cadena de matrices puede tener un impacto dramatico en la cantidad de multiplicaciones que tenemos que hacer, por ejemplo con tres matrices hA1 A2 A3i de dimensiones 10x100 100x5y5x50. Si hacemos ((A1 A2 )A3 ) tenemos que realizar 10 100 5 = 5000 + 10 5 50 = 2500 = 7500 multiplicaciones en total. Si en cambio hacemos (A1 (A2 A3 )) hacemos 100 5 50 = 25000 + 10 100 50 = 50000 = 75000 multiplicaciones. Por lo tanto la primera asociacion es diez veces mas e ciente que la segunda. Notemos que para multiplicar A B insumimos F ilasA ColumnasA ColumnasB multiplicaciones. El algoritmo que multiplica dos matrices es el siguiente.
Algorithm 37 Matrix-Multiply(A,B). Multiplica las matrices A y B if columnas A] 6= las B] then error else for i = 1 to filas A] do for j = 1 to columnas B] do C i j] 0 for k = 1 to columnas A] do C i j ] C i j ] + A i k] B k j ] end for end for end for end if
return C
Antes de aplicar la tecnica de programacion dinamica es conveniente veri car si la solucion por fuerza bruta es ine ciente. En este caso la solucion por fuerza bruta consiste en un algoritmo iterativo que pruebe todas las formas posibles de asociar las matrices calculando cuantos productos son necesarios en cada caso. El algoritmo de esta forma luego de veri car todas las asociaciones puede decidir cual es la forma optima de asociar las matrices para multiplicarlas. Para saber si este algoritmo es e ciente necesitamos saber cuantas asociaciones posibles existen dado un conjunto de n matrices. Cuando solo hay dos matrices hay una sola forma de asociarlas. Cuando tenemos n matrices hay n ; 1 lugares en donde podemos poner el par de parenteisis mas externos. Por ejemplo con seis matrices tenemos: (A1A2 ) 122
(A1 A2A3 ) (A1 A2A3 A4 ) (A1 A2 A3A4 A5 ) Cuando el parentesis mas externo es puesto luego de la kesima matriz creamos dos sublistas a ser asociadas. Una con k matrices y otra con n ; k matrices. Entonces debemos resolver como asociar estas sublistas. Como ambas sublistas son independientes si hay L formas de asociar la sublista izquierda y R formas de asociar la lista derecha el total de asociaciones posibles es L R. Esto sugiere la siguiente recurrencia donde P(n) es la cantidad de formas de asociar n matrices. si n = 1 si n 2 Esta es una funcion muy conocida en calculo combinatorio porque de ne los numeros Catalanes (cantidad de diferentes arboles binarios con n nodos). En particular en nuestro caso P (n) = C (n ; 1). 1 2n C (n) = n + 1 n Aplicando la aproximacion de Stirling al calculo del factorial podemos saber que C (n) 2 (4n =n3=2). Como 4n es exponencial el crecimiento de P (n) es exponencial. P (n) =
n;1 P (k)P (n ; k) k=1
1 P
Dado que el crecimiento de P (n) es exponencial la solucion por fuerza bruta no es aplicable a menos que la cantidad de matrices sea extremadamente chica. Resulta interesante observar como para n matrices cada forma de asociarlas de ne un arbol binario que indica el orden en el cual deben realizarse los productos, de alli que la cantidad de asociaciones sea la cantidad de arboles binarios posibles para n ; 1 nodos.
Veamos como se aplican los cuatro pasos basicos de la tecnica de programacion dinamica a nuestro caso. El primer paso al aplicar programacion dinamica es caracterizar la estructura de una solucion optima. Para el problema del producto de la cadena de matrices podemos hacerlo de la siguiente manera.
123
Sea Ai::j el resultado de multiplicar Ai Ai+1 : : :Aj . Una asociacion optima del producto de n matrices necesariamente implica dividir la cadena en dos subcadenas, asociar las sub-cadenas y luego multiplicarlas. A1 A2 : : : An = Ak Ak+1 El costo de esta asociacion optima es el costo de Ak mas el costo de Ak+1 mas el costo de multiplicar ambas matrices resultantes. La clave consiste en observar que se cumple el principio de subestructura optima ya que si la asociacion Ak Ak+1 es optima entonces la asociacion de Ak tambien debe serlo de lo contrario la solucion no seria optima. Analogamente la asociacion de Ak+1 tambien debe ser optima. El segundo paso de un problema de programacion dinamica es de nir el valor de una solucion optima en forma recursiva. Sea m i j ] la cantidad de multiplicaciones optimas para realizar el producto Ai::j , para n matrices la cantidad de multiplicaciones minimas es m 1 n]. Podemos de nir m i j ] recursivamente de la siguiente manera. Si i = j entonces la cadena tiene solo una matriz y no hace falta ninguna multiplicacion por lo tanto m i i] = 0. Para calcular m i j ] supongamos que la asociacion optima de las n matrices es Ak Ak+1 entonces. m i j ] = m i k] + m k + 1 j ] + pi 1pk pj
;
La ecuacion recursiva que de nimos asume que conocemos el valor de k, lo cual no es cierto. Sin embargo solo hay j ; 1 valores posibles de k, como la asociacion optima necesariamente utiliza alguno de estos valores podemos chequearlos todos para encontrar el mejor. Por lo tanto nuestra de nicion recursiva para la asociacion optima de n matrices es. si i = j m i j] = 0 mini k<j fm i k] + m k + 1 j ] + pi 1pk pj g si i < j
;
Los m i j ] nos dan los valores de las soluciones optimas a los subproblemas.
El tercer paso para diseniar un algoritmo de programacion dinamica es poder diseniar una estrategia que construya la solucion optima de un problema a partir de las soluciones de los sub-problemas involucrados. Veamos como podemos hacer esto para el producto de la cadena de matrices. 124
Supongamos que tenemos 5 matrices: hA1 A2 A3 A4 A5 i.Los siguientes subproblemas ya los tenemos solucionados. m 1 2] = A1::2 m 2 3] = A2::3 m 3 4] = A3::4 m 4 5] = A4::5 Ya que solamente implica multiplicar dos matrices. Luego podemos pasar a los subproblemas con 3 (tres) matrices. Para m 1 3] tenemos que calcular el minimo entre ((A1 A2 )A3 ) y (A1 (A2 A3)). Esto implica calcular lo siguiente: m 1 3] = minfm 1 2] + p0p2 p3 m 2 3] + p0 p1p3 g Analogamente podemos calcular m 2 4] m 3 5]. Pasamos ahora a los subproblemas de longitud 4, por ejemplo para calcular m 1 4] k puede valer 1 2 3 por lo tanto tenemos que hallar el minimo entre:
0 m 1 1] + m 2 4] + p p p 1 0 1 4 m 1 4] = min @ m 1 2] + m 3 4] + p0 p2p4 A
m 1 3] + m 4 4] + p0 p3p4
Notemos que m 2 4] m 1 2] m 1 3] son subproblemas que ya resolvimos por lo que no necesitamos resolverlos de nuevo, simplemente si m i j ] es una tabla accedemos al valor correspondiente. Aplicamos la misma forma para m 2::5] que es el otro subproblema de longitud 4. Por ultimo podemos resolver el problemad de longitud 5 que es el problema completo,para ello aplicamos la misma tecnica que antes. 0 m 1 1] + m 2 5] + p p p 1 0 1 5 B m 1 2] + m 3 5] + p p2p5 C C m 1 5] = min B @ m 1 3] + m 4 5] + p0 0 p3p5 A m 1 4] + m 5 5] + p0 p4p5 De esta forma podemos observar como resolviendo los sub-problemas en el orden apropiado podemos ir llenando una tabla de forma tal que cada sub-problema sea solucionado utilizando la solucion a los subproblemas anteriores. El orden en el cual debemos llenar la tabla es de acuerdo al tamanio de los subproblemas en nuestro caso fue. m 1 1] m 2 2] m 3 3] 125 (8.1) (8.2) (8.3)
m 4 4] (8.4) m 5 5] (8.5) m 1 3] (8.6) m 2 4] (8.7) m 3 5] (8.8) m 1 4] (8.9) m 2 5] (8.10) m 1 5] (8.11) Conociendo la de nicion recursiva necesaria para calcular la solucion optima y el orden en que debemos resolver los subproblemas podemos escribir el siguiente algoritmo de programacion dinamica.
end for for l = 2 to n do for i = 1 to n ; l + 1 do j i+l;1 m i j] 1 for k = 1 to j ; 1 do q m i k] + m k + 1 j ] + pi 1 pk pj if q < m i j ] then m i j] q end if end for end for end for
;
Si realizamos un seguimiento del algoritmo podemos observar como se llena la tabla en el orden necesario para resolver el problema.
Hasta aqui el algoritmo permite calcular cual es el numero de multiplicaciones minimo para n matrices, sin embargo tambien necesitamos saber cual es la asociacion que nos permite realizar el producto en dicho orden. Para ello basta con utilizar una tabla auxiliar que llamaremos s i j ] en donde si s i j ] = k quiere decir que para Ai::j la asociacion optima es Ak Ak+1. De esta forma el algoritmo que calcula la cantidad de multiplicaciones puede calcular la tabla s con un sencillo cambio. Para calcular la asociacion optima tenemos que acceder a s en forma recursiva, el algroritmo nal es el siguiente. 126
m i j] q s i j] k
127
El algoritmo es un tanto rebuscado por lo que resultaria conveniente realizar un seguimiento hasta asgurarse de comprender su funcionamiento, la solucion nal del ejemplo es ((A1 (A2A3 ))A4 )
Sea G = (E v) un grafo dirigido con aristas pesadas. Si (u V ) 2 E es una arista de G entonces el peso de la arista es W (u v). Supongamos por el momento que las aristas siempre tienen pesos positivos, un peso positivo excesivamente grande W (u v) (equivalente a 1) indica que no existe camino entre ambos vertices. Dado un camino = hu0 u1 : : :uki el costo del camino es la suma de los pesos de las aristas. costo( ) = W (u0 u1) + W (u1 u2) + : : : + W (uk ; 1 uk) =
k X i=1
W (ui ; 1 ui)
El problema consiste en encontrar todos los caminos de costo minimo desde un vertice hasta cualquier otro del grafo. Para este problema vamos a suponer que el grafo se encuentra representado por una matriz de adyacencias en lugar de la mas comun en general lista de adyacencias 1. Los datos de entrada estan almacenados en una matriz de nx W que contiene los pesos de las aristas. Sea wij el elemento W i j ].
El resultado del algoritmo debe ser una matriz de nx D = dij donde dij = (i j ) el costo del camino mas corto desde i hasta j . Conocer cual es el camino mas corto implica el uso de una matriz auxiliar pred i j ]. El valor de pred i j ] es un vertice que pertenece al camino minimo entre i y j , es decir que pred i j ] = k implica que para ir desde i hasta j con costo minimo hay que pasar por k, luego para seguir construyendo el camino hay que observar pred i k] y pred k j ].
Por el momento nos vamos a concentrar en calcular el costo de los caminos minimos en lugar del camino en si. En primer lugar necesitamos descomponer el problema en subproblemas. Podemos por ejemplo adoptar el siguiente enfoque: (m) para 0 m n ; 1, de nimos Dij como el costo del camino minimo desde i hasta j usando a lo sumo m aristas. La idea es calcular D(0) D(1) : : :D(n 1) . Ya que ningun camino puede usarmas de n ; 1 aristas (en caso contrario deberia repetir un vertice), sabemos que D(n 1) es la matriz nal. De esta forma hemos caracterizado la estructura de la solucion optima. Esto se ilustra en la siguiente gura.
; ;
1 Para grafos dispersos la lista de adyacenciassuele ser mas e ciente,pero en estos problemas es raro encontrar un grafo disperso ya que en general desde un vertice hay caminos hacia casi todos los demas vertices
129
El siguiente paso consiste en de nir la solucion en forma recursiva. Para computar las matrices de distancias el caso basico es D(1) = W dado que las aristas del digrafo son caminos de longitud 1.
(1) Dij = Wij
Ahora queremos ver que es posible calcular D(m) a partir de D(m 1) para m 2. (m) Consideremos que Dij es la distancia minima desde i hasta j usando a lo sumo m aristas. Hay dos casos.
;
Caso 2: Si el camino minimo usa exactamente m aristas entonces el camino usa m ; 1 aristas para ir desde i hasta algun vertice k y luego usa la arista (k j )
de peso wkj para llegar hasta j . El camino desde i hasta k debe ser tambien optimo (subestructura optima), por lo que la longitud del camino resultante es (m:1) Dik + Wkj . Como no sebmos el valor de k debemos hallar el minimo probando con todos los valores posibles de k. Dij = min
(m)
A continuacion mostramos un algoritmo tentativo que utiliza esta regla recursiva. 130
Algorithm 41 Dist1(m,i,j). Calcula la distancia minima desde i hasta j if m = 1 then return W i j ] end if best 1 for k = 1 to n do best min(best Dist1(m ; 1 i k) + W k j ]) end for
return best Desafortunadamente el algoritmo es realmente lento, sea T (m n) el tiempo del algoritmo en un grafo de n vertices. El algoritmo hace n llamadas a si mismo con el primer parametro de m ; 1, cuando m = 1 tenemos T(1,n)=1. Sino hacemos n llamadas recursivas a T (m ; 1 n). Es decir: T ( m n) = 1 si m = 1 nT (m ; 1 n) + 1 sino El tiempo total es T (n ; 1 n), esto se resuelve en forma directa usando el metodo iterativo obteniendo como resultado T (n) = O(nn ). Y lamentablemente esta funcion es una de las funciones de crecimiento mas rapido que conocemos, es super-exponencial y crece incluso mas rapido que n!. Esto ocurre porque el algoritmo simplemente intenta con todos los caminos posibles desde i hasta j y la cantidad de caminos posibles entre dos vertices de un grafo es exponencial. Vemos este algoritmo para ilustrar la ventaja de la tecnica de programacion dinamica sobre los metodos simples o de fuerza bruta, el unico paso que nos faltaba para obtener un algoritmo de programacion dinamica era determinar como calcular la solucion en forma bottom-up reutilizando los subproblemas ya resueltos. El siguiente algoritmo implementaeste paso, el procedimiento principal ShortestPath(n w) recibe la cantidad de vertices n y la matriz de pesos W . La matriz D(m) es almacenada en la tabla D m]. Para cada m, D m] es una matriz de dos dimensiones, por lo que D es una matriz tridimensional. Inicializamos D(1) copiando W . Luego cada llamada a ExtendPaths calcula D(m) a partir de D(m 1) usando la formula que habiamos mostrado antes.
;
El procedimiento ExtendPaths consiste de tres ciclos anidados por lo que su orden es (n3 ). Es llamado n ; 1 veces desde el procedimiento principal y por lo tanto el tiempo total es (n4 ). 131
end for
return D n-1]
Algorithm 43 ExtendPaths(n,d,W).
matrix dd 1..n] 1..n]=d 1..n] 1..n] // copiamos d en dd for i = 1 to n do for j = 1 to n do for k = 1 to n do dd i j ] min(dd i j ] d i k] + W k j ])
132
El algoritmo de Floyd-Warshall surge en la decada del `60 y permite calcular todos los caminos minimos en (n3 ) lo cual mejora el orden de nuestro algoritmo anterior que era (n4 ). El algoritmo se basa en lo siguiente:
Algorithm 44 Floyd-Warshall(n,W)
array d 1..n] 1..n] for i = 1 to n do for j = 1 to n do d i j] W i j] pred i j ] null
end for end for for k = 1 to n do for i = 1 to n do for j = 1 to n do if (d i k] + d k j] < d i j ]) then end if end for end for end for
return d d i j ] d i k] + d k j ] pred i j ] = k
El algoritmo Analisis
Claramente el orden del algoritmo es (n3 ), la clave esta en el orden en el cual se construye la solucion, en primer lugar se recorre toda la matriz para el vertice intermedio 1, luego para el 2, etc. A continuacion mostramos el algoritmo en un ejemplo.
133
Para calcular cual es el camino minimo hay que usar la matriz pred i j ] que tambien calcula el algoritmo, en forma recursiva se puede calcular cual es el camino minimo de la siguiente manera.
134
Subsecuencias Dadas dos secuencias X = hX 1 X 2 : : :Xni y Z = hZ 1 Z 2 : : :Zki decimos que Z es una subsecuencia de X si existe un orden estrictamente creciente de indices hi1 i2 ::iki (1 i1 i2 < : : : < ik n) tales que Z = hXi1 Xi2 : : : Xiki. Por ejemplo Z = hAADAAi es una subsecuencia de X = hABRACADABRAi. Notemos que el hecho de que existan elementos
de X en el medio que no estan en Z no afecta el concepto de subsecuencia. secuencia de longitud maxima Z que es a su vez subsecuencia de X y Y .
Subsecuencia comun de longitud maxima Dados dos strings X y Y la subsecuencia comun de longitud maxima (SCM) de X y Y es la subPor ejemplo si X = hABRACADABRAi y Y = hY ABBADABBADOOi. La subsecuencia comun de maxima longitud es Z = hABADABAi
strings X y Y determinar cual es la subsecuencia comun de longitud maxima, si la misma no fuese unica basta con calcular una cualquiera.
La aproximacion por fuerza brute al problema consistiria en computar todas las subsecuencias de un string y luego veri car si las mismas son subsecuencias del segundo string, de todas las subsecuencias comunes luego se elige la de mayor longitud. Sin embargo esto es claramente ine ciente ya que en un string de n caracteres existen 2n posibles subsecuencias por lo que el algoritmo simple seria de caracter exponencial (o super-exponencial) lo cual es claramente ine ciente. Para aplicar el metodo de programacion dinamica necesitamos dividir el problema de la (SCM) en sub-problemas, esto lo vamos a hacer de niendo al pre jo Xi de una secuencia X como hX 0 X 1 : : :Xii .La idea es computar la (SCM) por cada posible par de pre jos. Sea c i j ] la longitud de la subsecuencia de longitud maxima de Xi y Yj . Eventualmente el resultado nal sera c m n]. Como veremos c i j ] puede calcularse si conocemos c i j ] para i i, j j (pero no ambos iguales.Empecemos con algunas observaciones.
0 0 0 0
135
Si los ultimos caracteres son iguales Sea xi = yj . Por ejemplo Xi = hABCAi y Yj = hDACAi. Como ambos terminan en \A" a rmamos que la
; ;
(SCM) tambien deber terminar en A, ya que sino agregando A al nal tendriamos una subsecuencia comun de mayor longitud. Una vez que sabemos que A es parte de la (SCM) entonces podemos hallar la LCS de Xi 1 = hABC i y Yj 1 = hDAC i. Entonces. Si xi = yj entonces C i j ] = c i ; 1 j ; 1] + 1
caso o bien xi no es parte dela SCM o bien yj no es parte de la SCM o bien ninguno es parte de la SCM. En el primer caso la SCM de Xi e Yj es la SCM de Xi 1 e Yj . En el segundo caso la SCM es la SCM de Xi y Yj 1. Entonces. Si xi 6= yj c i j ] = max(c i ; 1 j ] c i j ; 1]). El tercer caso queda cubierto por un primer caso seguido de un segundo caso. La regla recursiva para calcular la solucion optima es entonces la siguiente: 0 si i = 0 o j = 0 si i j > 9 y xi = yi c i j ] = f c i ; 1 j ; 1] + 1 max(c i j ; 1] c i ; 1 j ]) si i j > 0 y xi 6= yj La tarea a continuacion es implementar la formula recursiva en forma bottom-up reutilizando los subproblemas resueltos.
8.4.2 Analisis
El analisis es muy sencillo ya que el algoritmo consiste de dos ciclos de n y m iteraciones anidados, el orden es por lo tanto O(mn). El funcionamiento del algoritmo puede resultar un tanto misterioso por lo que se recomienda realizar un seguimiento para observar como funciona. La ilustracion es un ejemplo de como funciona el algoritmo.
136
c i j] b i j]
c i j ; 1] SKIP Y
137
El calculo de cual es la subsecuencia comun de longitud maxima es simple utilizando los punteros almacenados en la matriz auxiliar b i j ]
end if if b i j ] = SKIP X then i i;1 end if if b i j ] = SKIP Y then j j ;1 end if end while
return SCM
timizacion. Para que un problema sea solucionable mediante programacion dinamica debe presentar dos caracteristicas: Subsestructura optima El problema debe poder dividirse en sub-problemas de forma tal que si la solucion al problema es optima la solucion de los subproblemas que lo componen tambien debe serlo. Superposicion de subproblemas Un problema debe poder resolverse a partir de la resolucion de sus sub-problemas y asi sucesivamente en forma recursiva. Ademas un sub-problema debe formar parte de la solucion de mas de un problema de mayor orden de forma tal que si se almacena el reusltado del sub-problema en una tabla el mismo pueda ser usado para evitar resolver dos veces el mismo problema. La tecnica de programacion dinamica consiste en los siguientes pasos: De nir la estructura de la solucion Hallar una formula recursiva para calcular la solucion Implementar la solucion recursiva en forma botton-up reutilizando los subproblemas ya resueltos. Encontrar la solucion optima Como ejemplos de programacion dinamica vimos cuatro algoritmos importantes, el primero de ellos sirve para multiplicar una cadena de matrices en forma optima, el segundo y el tercero resuelven el problema de las distancias minimas en un grafo, el cuarto resuelve un problema con strings que tiene muchas aplicaciones en la actualidad. Comprender y dominar el metodo de programacion dinamica requiere tiempo y dedicacion pero permite encontrar soluciones e cientes donde de otra forma el costo para resolver el problema seria demasiado grande.
8.6 Ejercicios
1. Una empresa se encuentra organizando una esta para sus empleados, la empresa esta organizada en forma jerarquica con una estructura en forma de arbol. La comision de estas ha asignado a cada empleado un numero real que indica el \carisma" de cada uno de sus empleados. Ademas para que las conversaciones sean mas uidas y amenas se ha decidido que no pueden asistir a la esta un empleado y su superior inmediato juntos. La comision desea maximizar la sumatoria de carisma total. Dar un algoritmo de programacion dinamica que permita determinar quienes deben asistir a la esta. 139
Chapter 9
Algoritmos Golosos
9.1 Algoritmos golosos, principios.
El termino \algoritmo goloso" es una traduccion libre del ingles \greedy algorithms", en algunos textos se re ere a este tipo de algoritmos como algoritmos \avidos". Los algoritmos golosos se utilizan para resolver problemas de optimizacion y son extremadamente sencillos , se basan en un principio fundamental:
Principio: Un algoritmo goloso siempre toma la decision que parece mejor de acuerdo al estado actual del problema
Un algoritmo goloso funciona construyendo una solucion en base a una secuencia de decisiones golosas, se dice que la decision que toman estos algoritmos es golosa porque si bien son optimas en el estado actual del problema no implica que lleven necesariamente a una solucion optima del problema.
140
9.1.1 Fundamentos
Para que un problema pueda ser solucionable en forma optima mediante un algoritmo goloso debe presentar dos caracteristicas fundamentales: Subestructura optima El problema debe poder ser descompuesto en varios sub-problemas de forma tal que si la solucion del problema es optima la solucion de los subproblemas tambien lo es. (Este requisito tambien era necesario para el metodo de programacion dinamica). Opcion golosa Deber ser posible demostrar que una eleccion localmente optima lleva a una solucion globalmente optima. Esta es la clave de los algoritmos golosos y en general se demuestra por induccion, en primer lugar se prueba que si existe una solucion optima entonces existe tambien una solucion que parte de una decision golosa. Luego para el subproblema restante por induccion se prueba que se puede llegar a una solucion optima. En esta seccion observaremos algunos problemas nuevos y otros no tanto que pueden ser resueltos utilizando un algoritmo goloso.
9.2.1 Ejemplo
En nuestro ejemplo hay varios conjuntos de cantidad de actividades compatibles maxima, por ejemplo hS 2 S 4 S 1i o hS 3 S 4 S 5i o hS 3 S 4 S 1i, etc Son soluciones optimas.
El algoritmo goloso para resolver el problema se basa en lo siguiente: En primer lugar ordenar las actividades por orden de nalizacion creciente.
Elegir la primera actividad. Luego elegir la primera actividad compatible con la ultima elegida. Repetir hasta que no queden actividades.
Algorithm 48 Greedy-Selector(S). Recibe un conjunto de S actividades. Require: S debe estar ordenado por hora de nalizacion S i]:f S i + 1]:f n length(S ) A f1g j 1 for i = 2 to n do if S i]s S i]:f then A A fig j i end if end for
Para nuestro ejemplo el algoritmo elegiria hS 3 S 4 S 1i
9.2.3 Analisis
Es evidente que el algoritmo es e ciente, insume el tiempo necesario para el sort mas un tiempo que es (n) para el proceso de seleccion. A continuacion tenemos que demostrar que el algoritmo goloso para el problema de la seleccion de actividades es optimo.
Teorema: El algoritmo Greedy ; Selector produce soluciones optimas. Demostracion: Sea S = hS 1 S2 : : : Sni el conjunto de actividades a seleccionar ordenadas por tiempo de nalizacion de forma tal que la actividad 1 es la que naliza mas temprano. Queremos mostrar que existe al menos una solucion optima que contiene la actividad S 1. Supongamos que A S es una solucion optima para el problema y ordenemos las actividades de A por hora de nalizacion. Supongamos ademas que la primera actividad de A es Sk. Si k = 1 entonces la solucion contiene la actividad elegida por el algoritmo goloso. Si k 6= 1 queremos mostrar que existe otra solucion optima B que contiene S 1. Sea B = A ; fSkg f1g. Como S 1:f Sk:f las actividades en B son compatibles, y como B tiene la misma cantidad de actividades que A que era una solucion optima entonces B es tambien una solucion optima. Por lo tanto siempre existe una solucion optima que contiene la decision golosa.
142
Ademas una vez realizada la decision golosa de la actividad el problema se reduce a encontrar un conjunto optimo de actividades sobre el conjunto S siendo S las actividades de S compatibles con S 1. Es decir que si A es una solucion optima a S entonces A = A ; fS 1g es una solucion optima a S = fi 2 S : si s1:f g. Esto se debe a que si podemos encontrar una solucion B a S con mas actividades que A entonces agregando la actividad S 1 a B tendriamos una solucion al problema original S con mas actividades que A y por lo tanto A no seria una solucion optima. Por induccion sobre la cantidad de decisiones tomadas por el algoritmo tenemos que la solucion nal es optima.
0 0 0 0 0 0
La estructura basica del algoritmo de Dijkstra consiste en mantener un estimado de la distancia minima desde el origen s hasta cualquier otro vertice denominada d v]. Este valor va a ser siempre mayor o igual a la distancia minima desde s hasta v y ademas vamos a requerir que d v] siempre sea igual a la distancia de un cierto camino desde s hasta v (excepto cuando d v] = 1 indicando que no hay camino). Inicialmente d s] = 0 y todos los demas vertices d v] se setean en 1. A medida que el algoritmo progresa examina mas y mas vertices intentando actualizar d v] para cada vertice del grafo hasta que d v] converge al valor de la distancia minima.
El proceso por el cual se actualiza d v] se denomina relajacion. Intuitivamente si uno puede ver que la solucion aun no ha alcanzado un valor optimo se la presiona un poco mas hacia el optimo, en particular si se descubre un camino de
1 El algoritmo de Floyd-Warshall de programacion dinamica encuentra todos los caminos minimos desde cualquier vertice hasta cualquier otro en O(n3 ), ahora se trata de encontrar todos los caminos minimos desde solo un vertice hasta todos los demas lo cual deberia poder hacerse en menos tiempo. El orden a batir es entonces O(n3 ).
143
s hacia v mas corto que d v] entonces es necesario actualizar d v], este principio es comun en muchos algoritmos de optimizacion. Consideremos una arista desde un vertice u hacia un vertice v cuyo peso es W u v]. Supongamos que hemos calculado el estimado para d u] d v]. Sabemos que hay un camino desde s hacia u de costo d u], usando este camino y agregando la arista (u v) obtenemos un camino desde s hasta v de distancia d u]+ W u v]. Si este camino es mejor que el que ya teniamos entonces actualizamos d v], en caso contrario lo dejamos como esta.
No es dificil observar que si realizamos relajaciones sucesivas en todas las aristas del grafo entonces d v] eventualmente converge la verdadera distancia minima desde s hasta v. La inteligencia en un algoritmo de este tipo esta en realizar las relajaciones de forma tal que la convergencia sea lo mas rapida posible.
El algoritmo de Dijkstra se basa en la realizacion de sucesivas relajaciones. El algoritmo opera manteniendo un subconjunto de vertices S V para los cuales conocemos la verdadera distancia minima d v]. Inicialmente S = Null y d s] = 0 y d v] = 1 para los demas vertices. Uno por uno seleccionamos vertices de V ; S para agregarlos a S . El conjunto S puede implementarse como un vector de vertices coloreados, inicialmente todos los vertices son blancos y seteamos color v] = negro para indicar que v 2 S . Al seleccionar que vertice de V ; S se agregara a S entra en accion la decision golosa. Para cada vertice u 2 V ; S hemos computado la distancia estimada d u]. La decision golosa consiste en elegir el vertice u para el cual d u] es minimo, es decir el vertice sin procesar que de acuerdo a nuestra estimacion esta mas cercano a s. Para hacer esta seleccion mas e ciente almacenamos los vertices de V ; S en una cola de prioridades (Un Heap por ejemplo), donde el valor clave de cada vertice u es d u]. Este es el algoritmo de Dijkstra.
144
Algorithm 50 Dijsktra(G,w,s). Calcula todos los caminos minimos desde s. for each u in V do d u] 1 end for
color u] blanco pred u] null d s] 0 pred s] null Q construircolacontodoslosvertices while Non-empy(Q) do u Extract ; min(Q) for each v in Adj u] do Relax(u v) Decrease ; key(Q v d v]
end for
color u]
end while
negro
9.3.4 Validacion
Queremos ver ahora que el algoritmo de Dijkstra es correcto, es decir que calcula las distancias minimas desde s el vertice origen hasta cualquier otro vertice. Sea (s u) la distancia minima de s hasta u.
Demostracion: Supongamos que en algun momento el algoritmo intenta por primera vez agregar un vertice u a S para el cual d u] = 6 (s u). De nuestras
observaciones sobre la relajacion sabemos que d u] > (s u). Consideremos la situacion anterior a la insercion de u. Consideremos el verdadero camino minimo desde s hasta u. Como s 2 S y u 2 V ; S en elgun momento el camino debe tomar por primera vez un salto hacia afuera de S . Sea (x y) la arista que toma el camino donde x 2 S y y 2 V ; S . Podemos probar que y 6= u. Como x 2 S . Tenemos d x] = (s x) (porque u es el primer vertice que viola el principio). Entonces cuando aplicamos la relajacion a x tendriamos que d y] = d y] + W u x] = (s y). Por lo tanto d y] es correcto y por hipotesis d u] no es correcto entonces no pueden ser el mismo vertice. Observemos ahora que como y parece en algun punto del camino desde s hasta u tenemos que (s y) < (s u) y por lo tanto. d y] = (s y) < (s u) d u] Por lo tanto y deberia ser agregado a S antes que u lo cual contradice que u es el proximo vertice que se agrega en S y por lo tanto el algoritmo es correcto.
9.3.5 Analisis
2
El algoritmo de Dijkstra es O((n + e) log n). Si se usan heaps de Fibonacci 2 en lugar de Heaps binarios standard el tiempo es entonces O(n log n + e).
Los heaps de Fibonacci se estudiaran mas adelante en el curso
146
Un arbol generador minimo (AGM) es un arbol generador de costo minimo. Y puede no ser unico, pero si todos los pesos son distintos entonces el arbol optimo es unico (esto es un tanto sutil y no lo vamos a demostrar). La gura muestra dos arboles generadores minimos para el mismo grafo.
147
Presentaremos un algoritmo goloso para hallar el arbol generador minimo de un grafo. Antes tenemos que hacer algunas consideraciones generales que afortunadamente son faciles de probar.
Lema
Un arbol con n vertices tiene exactamente n ; 1 aristas. Siempre existe un unico camino entre dos vertices de un arbol. Agregar una arista cualquiera a un arbol crea un ciclo, eliminando cualquier arista de este ciclo se recupera el arbol.
Sea G = (V E ) un grafo no dirigido conexo cuyas aristas tienen pesos. La intuicion detras de los algoritmos golosos para el (AGM) es simple, mantenemos un subconjunto de aristas A incialmente vacia y agregamos una arista a la vez hasta que A es el (AGM). Decimos que un subconjunto A E es viable si A es un subconjunto de aristas en algun AGM. Decimos que una arista (u v) 2 E ; A es segura si A f(u v)g es viable. Es decir que la eleccion de (u v) es una eleccion segura de forma tal que A aun puede ser extendido hasta formar un AGM Un algoritmo goloso generico opera agregando en forma repetida cualquier arista segura al AGM actual. 148
El algoritmo de Kruskal trabaja agregando aristas a A en orden creciente de peso. (Las aristas menos costosas se agregan primero). Elige la arista mas economica y si la mismano produce un ciclo entonces la agrega al AGM . Cuando no quedan aristas por agregar (no hay mas o bien no se puede agregar ninguna) queda formado el arbol generador minimo. La unica parte intrincada del algoritmo es detectar la formacion de ciclos. Para ello podriamos aplicar DFS pero eso llevaria demasiado tiempo, el detectar si el agregado de una arista causa un ciclo en A puede hacerse empleando una estructura de datos denominada estructura Union-Find 3 la estructura soporta las siguientes operaciones. Create-Set(u). Crea un conjunto conteniendo u Find-Set(u). Encuentra el conjunto que contiene un elemento u. Union(u,v). Realiza la union de los conjuntos que contienen u y v en un unico conjunto. Usando esta estructura podemos crear un conjunto por cada arbol del bosque A, de esta forma detectamos que se forma un ciclo si agregamos una arista cuyos vertices pertenecen al mismo conjunto. Veamos como podemos usar esta estructura como una caja negra en la cual cada operacion se hace en O(log n)4 . En el algoritmo de Kruskal los vertices del grafo son los elementos a almacenar en los conjuntos y los conjuntos seran vertices en cada arbol de A. El conjunto A puede guardarse como una lista simple de aristas.
3 4
Esta estructura y su implementacion se estudian mas adelante en el curso de hecho hay implementaciones aun mas rapidas
149
Algorithm 51 Kruskal(G,W).Calcula el AGM del grafo G A fg for each u in V do Create-Set(u) end for Sort E de acuerdo al peso w. for each (u v) de la lista ordenada do if Find-Set(u) = 6 Find-Set(v) then end if end for
return A Add (u v) to A Union(u,v)
150
9.4.2 Analisis
El algoritmo de Kruskal recorre todas las aristas del grafo y por cada arista realiza operaciones que son O(log n) por lo tanto el orden del algoritmo es O((n+ e) log n)
9.6 Ejercicios
1. Una maquina expendedora de pasajes de tren necesita un algoritmo para dar el vuelto usando la menor cantidad de monedas posibles. (a) Describir un algoritmo goloso para solucionar el problema para monedas de 5,10,25 y 50 centavos. Demostrar que el algoritmo es optimo. (b) Suponer que las monedas son ahora de c0 c1 c2 : : : cn centavos para c > 1 y k 1 enteros. Demostrar que el algoritmo anterior es optimo. (c) Indicar si el algoritmo es siempre optimo independientemente del conjunto de monedas que se utilice, dar ejemplos en los cuales el algoritmo no es optimo.
151
Chapter 10
10.1.1 Pilas
Una pila es una estructura dinamica en la cual la entrada y salida de elementos se hace en orden LIFO (Last-in- rst-out). Una pila (stack) soporta las siguientes operaciones. Create-Stack() Crea una nueva pila vacia. O(1). Empty-Stack() Indica si la pila esta vacia. O(1). Top() Devuelve el elemento en el tope de la pila sin sacarlo de la pila. O(1). Push(e) Apila un elemento e. O(1). Pop() Desapila un elemento y lo devuelve. O(1). 152
La implementacion de pilas es una tarea en extremo sencilla, las pilas suelen ser estructuras muy utilizadas en gran cantidad de algoritmos.
10.1.2 Colas
Una cola es una estructura dinamica en la cual la entrada y salida de elementos se hace en orden FIFO (First-in- rst-out). Una cola soporta las siguientes operaciones. Create-Queue() Crea una nueva cola vacia. O(1). Enqueue(e) Agrega un elemento e a la cola. O(1). Dequeue() Extrae un elemento de la cola. O(1). Las aplicaciones de las colas son muchas.
Colas dobles
En una cola doble se puede encolar o desencolar ya sea desde la cabeza o desde la cola de la cola. Es decir que se pueden manejar los elementos en orden FIFO o en orden LIFO Las colas dobles soportan las siguientes operaciones. Create-Queue() Crea una nueva cola vacia. O(1). Enqueue-Head(e) Agrega un elemento e a la cabeza de la cola. O(1). Dequeue-Head() Extrae un elemento de la cabeza de la cola. O(1). Enqueue-Tail(e) Agrega un elemento e a la cola de la cola. O(1). Dequeue-Tail() Extrae un elemento de la cola de la cola. O(1).
10.1.3 Listas
Una lista enlazada es una coleccion dinamica de objetos de proposito general, en su uso mas basico una lista sirve para mantener una relacion entre una cantidad variable de objetos, pero hay muchas variantes de listas especializadas en otro tipo de aplicaciones. Una lista enlazada simple provee. Create-List Crea una lista enlazada vacia. O(1). Insert(e) Agrega un elemento e a la lista. O(1). Search(e) Busca un elemento e en la lista. O(n). Delete(e) Elimina el elemento e de la lista. O(n). Algunas variantes de las listas son las listas doblemente enlazadas, las listas ordenadas (en donde se inserta en forma ordenada), las listas circulares, listas dobles circulares, etc. 153
Un arbol binario es una estructura demasiado popular como para realizar alguna observacion, debemos suponer que el arbol se mantiene ordenado (es decir que no puede insertarse un elemento en cualquier lado). Las operaciones que dependen de la altura (h) del arbol son: Create-Tree() Crea un arbol vacio. O(1). Insert(e) Inserta el elemento e en el arbol. O(h). Recorrer() Lista los elementos del arbol en algun orden (inorder, preorder o postorder). O(n). Search(e) Busca un elemento e en el arbol. O(h). Delete(e) Elimina el elemento e del arbol. O(h). Las aplicaciones de los arboles binarios son innumerables.
En un arbol binario cuando el orden en el cual se insertan los elementos es aleatorio la altura del arbol tiende a ser h = log n, sin embargo si esto no ocurre un arbol binario puede degenerar en una lista enlazada siendo h = n, los arboles AV L tambien llamados Red ; Black ; trees se encargan de mantener al arbol balanceado independientemente de los elementos que se inserten y del orden en que se hagan las inserciones. Un arbol AVL es un arbol binario que ademas de mantenerse ordenado se mantiene balanceado, cuando esto ocurre podemos decir que h = log n por lo tanto las operaciones que son las mismas que en un arbol binario son: Create-Tree() Crea un arbol vacio. O(1). Insert(e) Inserta el elemento e en el arbol. O(log n). Recorrer() Lista los elementos del arbol en algun orden (inorder, preorder o postorder). O(n). Search(e) Busca un elemento e en el arbol. O(log n). Delete(e) Elimina el elemento e del arbol. O(log n). El costo de balancear el arbol es un factor constante que se agrega en cada operacion, pero precisamente por ser constante no afecta el orden de las primitivas. Los arboles AV L son herramientas utiles y bastante poderosas para resolver varias situaciones problematicas. Por ejemplo son muy e cientes para implementar la parte interna del algoritmo de ReplacementSelection para sort externo. 154
El analisis amortizado analiza el tiempo promedio de una operacion que se supone se realiza n veces.
Las tecnicas de analisis amortizado conocidas son las siguientes: Metodo de agregacion Metodo contable Metodo de los potenciales Vamos a estudiar en esta clase los dos primeros que son los mas simples, el tercero requiere de algunas herramientas que no estan a nuestro alcance en este curso. En el metodo de agregacion calculamos el peor caso de una serie de n operaciones como T (n) y luego calculamos el costo amortizado de una operacion como T (n)=n. n) T (n) = Costo de n operaciones ) T (op) = T ( n
Algorithm 52 Multipop(S,k). Elimina hasta k elementos de la pila S while not Empty-Stack(S) and k 6= 0 do Pop(S ) k k;1 end while
El costo de multipop depende de la cantidad de pops que se realicen, la cantidad de pops es el minimo entre k y la cantidad de elementos en la pila n, como pop es O(1) el algoritmo Multipop es O(min(k n)). 155
En el peor caso Multipop elimina n elementos de la pila (todos los elementos) y por lo tanto Op = O(n). Queremos analizar que ocurre si hacemos n operaciones Push, Pop y Multipop en orden aleatorio para n un numero grande. Como Push y Pop son O(1) y Multipop es O(n) podriamos decir que el costo de las n operaciones es n O(n) = O(n2) con lo que diriamos que T (n) = O(n2 ) y por lo tanto T (n)=n = n sin embargo esto no es cierto. Si analizamos el caso con mejor cuidado podemos ver que por tratarse de una pila un elemento puede ser Popeado a lo sumo tantas veces como fue pusheado, por lo tanto en n operaciones Pop Push Multipop la cantidad de Pops es menor o igual a la cantidad de Pushs, y esto incluye a la operacion Multipop que consiste en una cantidad entera de Pops, por lo tanto T (n) nunca puede ser O(n2 ) sino que es a lo sumo O(n), luego aplicando el metodo de agregacion T (n)=n = O(n)=n = O(1). Por lo tanto el costo amortizado de la operacion Multipop es O(1).
while i < len(A) and A i] = 1 do end while if i < len(A) then A i] 1 end if
A i] 0 i i+1
156
Este algoritmo es exactamente el mismo que se utiliza en hardware para incementar un registro. Queremos medir el costo de la operacion como la cantidad de bits que se cambian de estado, en el peor caso A = 1111111 y al incrementarlo hay que cambiar todos sus bits por lo que el costo es O(k). Si hacemos n operaciones, sin embargo, el costo no es O(nk). Observemos un seguimiento del contador y de la cantidad de bits que se cambiaron en forma acumulada. Valor Representacion Costo Acumulado 0 000000 0 0 1 000001 1 1 2 000010 2 3 3 000011 1 4 4 000100 3 7 5 000101 1 8 6 000110 2 10 7 000111 1 11 8 001000 4 15 Observemos que luego de incrementar n veces el contador el costo acumulado nunca es mayor que 2n, observemos tambien que el bit menos signi cativo cambia su estado n=2 veces, el segundo bit menos signi cativo cambia su estado n=4 veces la cantidad de bits que se cambian al incrementar el contador n veces es entonces. log n X n < n X 1 = 2n 2i 2i
1
Lo cual concuerda con nuestra observacion de que el costo acumulado nunca superaba 2n. De lo anterior el peor caso de n operaciones es T (n) = O(2n) por lo que aplicando el metodo de agregacion el costo de la operacion Increment es T (2n)=n = O(1)
i=0
i=0
En el metodo de agregacion suponiamos que todas las operaciones tenian un costo igual al realizarse n veces e igual a n por el costo de la operacion, en el metodo de contabilidad se asigna a cada operacion un valor que se denomina costo amortizado, y luego se realiza una simulacion de n operaciones, cada vez que se realiza una operacion se debe pagar lo que la operacion cuesta y se cuenta para ello con el costo amortizado, si el costo amortizado no alcanza para pagar la operacion hay que reajustar el costo y volver a comenzar. Si el costo sobra la estructura de datos recibe un cierto credito que puede ser usado si otras operaciones no pueden pagar su costo. 1 . Veamos un ejemplo para ver de
1
157
Un pop o un multipop no pueden hacerse si antes no se hizo al menos un push, cuando se hace un push que insume O(1) disponemos de 2 unidades, por lo que la estructura recibe 1 credito. A medida que se hacen mas y mas operaciones se acumula un credito por cada Push, cada pop insume O(1) por lo que consume un credito, Multipop consume una cantidad de creditos variable pero podemos demostrar que siempre hay creditos su cientes para Multipop, por lo tanto como Pop es O(1) y tiene costo amortizado cero Multipop tambien debe ser O(1). El metodo contable es mas \fantastico" que el metodo de agregacion y requiere mucha practica.
El metodo de los potenciales es mas complejo que los dos anteriores pero es tambien mas poderoso. La idea es de nir una funcion potencial que mapea una estructura de datos a un numero real. mide la cantidad de esfuerzo que hemos aplicado a la estructura de forma tal de poder aprovecharlo en futuras operaciones, el concepto es analogo al usado en fisica cuando hablamos de energia potencial. Es importante destacar que el potencial depende unicamente de la estructura de datos en si misma, no de las operaciones que han sido aplicadas sobre dicha estructura en el pasado. En algunas ocasiones sin embargo es posible modi car la estructura de datos de forma tal de llevar un registro en algun lugar de almacenamiento de las operaciones que le fueron aplicadas, en estos casos la funcion potencial podria depender de dicho lugar de almacenamiento. El costo amortizado de la operacion i se de ne como c (i) donde:
0
Siendo Di la estructura de datos que resulta luego de la operacion i ; 1 y Donde c(i) es le costo verdadero asociado a la operacion, por lo tanto:
i = 1nc (i) =
0
n X i=1
c (i)
0
Por lo tanto C (i) esta acotado por 2 y ese es el limite del costo amortizado por operacion.
0
El arte reside en encontra la funcion potencial en forma ingeniosa, por ejemplo que funcion deberiamos usar para el ejemplo del contador binario?
general el costo amortizado representa en forma mas practica y real el costo de una primitiva y cuando de utilizar primitivas se trata debemos ser sumamente precisos en sus costos para no afectar en forma innecesaria el costo de los algoritmos que las utilizan.
En un Heap comun las operaciones Make ; Heap y Minimum son O(1) y las restantes excepto la union son O(log n). La operacion de union en un heap comun es O(n). Los Heaps Binomiales y los Heaps de Fibonacci tienen como objetivo realizar estas operaciones en forma mas e ciente.
arbol binomial.
Un arbol binomial es un arbol binario. Un arbol binomial de orden k esta formado por dos arboles binomiales de orden k ; 1 en donde la raiz del arbol de orden k es la raiz del arbol derecho de orden k ; 1. El gra co muestra algunos arboles binomiales.
k nodos en el nivel i. i 4. La raiz de Bk tiene grado k y no hay otro nodo en Bk con grado mayor o igual que k 5. Si los hijos de Bk son rotulados de izquierda a derecha como k ; 1 k ; 2 k ; 3 : : : 0 entonces el hijo i es la raiz de un Bi 3. Bk tiene exactamente
Todas las propiedades se pueden probar en forma simple por induccion. 160
Un Heap binomial es una estructura de datos que consiste en una lista de arboles binomiales (las raices de los arboles forman una lista) con las siguientes propiedades. 1. Cada arbol binomial esta en Heap-order es decir que la clave de un nodo en uno cualquiera de los arboles es mayor que la clave de sus hijos. 2. El heap no contiene dos arboles de igual orden. 3. La lista de arboles se mantiene en orden creciente de acuerdo al grado de la raiz.
10.3.3 Implementacion
* * * * *
Para implementar un Heap binomial se recurre a un nodo que presenta la siguiente estructura:
Puntero al nodo padre. Valor de la clave. Grado del nodo. (cantidad de hijos) Puntero al hijo izquierdo. Puntero al hermano derecho.
Esta estructura es su ciente para todos los nodos de un heap binomial, el puntero al hermano derecho en los nodos que son raiz del arbol binomial sirven para mantener la lista de arboles. La ilustracion muestra un heap binomial y su implementacion.
161
162
Union
Para hacer la union de dos heaps binomiales tomamos la lista de raices de cada Heap y realizamos un merge de acuerdo al grado de cada raiz. Luego hacemos un merge de los arboles binomiales de igual grado. En primer lugar presentamos un algoritmo que dados dos arboles binomiales de grado k representados por sus nodos raices construye un nuevo arbol binomial de grado k + 1 donde z es la raiz del arbol. Como podemos ver Binomial-Link es O(1). El algoritmo que realiza la Union es complejo pues se deben manejar varios casos, el algoritmofunciona en O(log n).
163
Algorithm 56 Union(H1,H2). Realiza la union de dos heaps binomiales. h Make ; Binomial ; Heap() head H ] Binomial ; Heap ; Merge(H 1 H 2) Liberar los objetos H1 y H2 pero no las listas if head H ] = NULL then return H end if prev ; x NULL x head H ] next ; x sibling x] while next ; x = 6 NULL do if degree x] = 6 degree next ; x] or (sibling next ; x] = 6 NULL and degree sibling next ; x]] = degree x]) then prev ; x x x next ; x else if key x] key next ; x] then sibling x] sibling next ; x] Binomial ; Link(next ; x x) else if prev ; x = NULL then head H ] next ; x else sibling prev ; x] next ; x end if Binomial ; Link(x next ; x) x next ; x end if end if next ; x sibling x] end while
164
Insert
Para insertar un elemento en un Heap-Binomial simplemente construimos un Heap vacio, lo inicializamos con el elemento a insertar y luego hacemos la Union entre el Heap creado y el Heap donde queremos insertar el elemento. Como solo agregamos tiempos constantes a la union este algoritmo es O(log n).
Extract-Min
Hay que encontrar la raiz con clave minima (de la lista de raices). Remover esta raiz y su arbol asociado de la lista, tomando el arbol que estaba asociado a esta raiz y quitando la raiz obtenemos k arboles binomiales que ponemos en una lista enlazada. Luego hacemos la Union (primitiva de Union) par juntar esta lista con la lista anterior.
Decrease-Key Delete
que su padre.
Se decrementa la clave (decrease-key) hasta ;1 de esa forma el nodo llega a la lista raiz, una vez alli usamos Extract ; Min para eliminarlo.
10.6 Union-Find
La estructura Union-Find es una estructura de datos que implementa una serie de conjuntos disjuntos. Las operaciones que se permiten son: Make-Set(x) Crea un nuevo conjunto que contiene el elemento x es decir fxg. S=Union(x,y) Toma dos conjuntos como parametros y realiza la union , el resultado es un nuevo conjunto cuyo nombre es el nombre de algunos de los conjuntos que lo originaron , los conjuntos anteriores son destruidos. Find(x) Devuelve el nombre del conjunto que contiene al elemento x. El nombre en la mayoria de las implementaciones es un elemento representativo del conjunto2 de forma tal que Find(x) = Find(y) si x e y pertenecen al mismo conjunto.
Aplicacion La estructura Union-Find ya habia sido mencionada en el algoritmo de Kruskal, y tiene varias aplicaciones importantes mas. Por ejemplo supongamos que G = (V E ) es un grafo no dirigido. Decimos que el vertice v esta en el mismo componente conexo que el vertice w si hay un camino usando ejes de E que conecta v y w. Dado un par de vertices queremos saber si estan en el mismo componente conexo. Podemos resolver el problema de la siguiente forma:
Algorithm 57 Connected-Components(G) for each v in V do Make ; Set(v) end for for each (u v) in E do if Find-Set(u) <> Find-Set(v) then Union(u v) end if end for
166
Algorithm 58 Same-Component(u,v) if Find-Set(u)=Find-Set(v) then return TRUE else return FALSE end if
En nuestra primera implementacion (simple). Cada conjunto va a estar representado por una lista enlazada y el nombre del conjunto va a ser el primer elemento de la lista. Cada elemento de la lista tiene un puntero al primer elemento de la lista y al siguiente.
Las operaciones se implementan de la siguiente manera. Make-Set(x) Creamos una nueva lista que contiene x. O(1) Find-Set(x) Usamos el puntero a la cabeza de la lista desde x. O(1) Union(x,y) Hay que realizar la union de dos listas y actualizar los punteros a la cabeza (ver gura). Esta operacion se denomina splice, el splice de L1 en L2 implica hacer que la cabeza de L1 apunte al primer elemento de L2 y el ultimo elemento de L2 apunte al elemento que antes era la cabeza de L1, esto engancha ambas listas. Ademas hay que actualizar todos los punteros a la cabeza de L2 para que apunten ahora a la cabeza de L1. O(n).
167
Para mejorar el tiempo necesario para realizar la operacion de Union, agregamos un campo a cada nodo de la lista. Para los elementos a la cabeza de una lista el campo contiene la cantidad de elementos en la lista, para el resto de los elementos este campo no se usa. Con esta implementacion las operaciones Make ; Set y Find ; Set, no cambian.
Union(x,y) Para realizar el splice el mismo se hace de la lista que tiene menos
elementos en la otra. De esta forma tenemos que actualizar menor cantidad de punteros, veamos como afecta esto el orden de la Union. Realicemos un analisis amortizado para m operaciones,en el viejo algoritmo de Union esto insumia (n2 ). Consideremos cuantas veces cambia el nombre de un objeto (el puntero a la cabeza). Cada vez que el nombre de un objeto x cambia sabemos que x pertenecia a un conjunto de menos elementos de los dos que participan en la Union. La primera vez que x es actualizado el conjunto resultante pasa a tener 168
dos conjuntos, la segunda vez pasa a tener al menos 4 elementos (en caso contrario el conjunto de x no seria el mas chico). Por lo tanto por cada vez que se actualiza un puntero el tamanio del conjunto por lo menos se duplica. Como un conjunto puede tener a lo sumo n elementos la cantidad de veces que el nombre de un objeto puede ser actualizado es log n. Como hay n objetos el costo total es O(n log n). Si consideramos m operaciones (Make-Set,Find-Set,Union) vemos que Make-Set y Find-Set son O(1) por lo tanto el total es O(m + n log n).
En esta implementacion cada conjunto del grupo Union;Find esta representado por un Up ; Tree3 , la implementacion cumple las siguientes propiedades. 1. Cada nodo de un arbol representa un elemento del conjunto. 2. Cada nodo apunta a su padre. 3. La raiz es padre de si mismo. 4. El nombre del arbol es su raiz.
Operaciones
Make-Set(x) Crea un nuevo nodo con un puntero a si mismo.
3
169
Union(x,y) Apunta la raiz del arbol de menor altura a la raiz del arbol mas
alto. Find(x) Desde el nodo x se siguien los punteros hasta la raiz del arbol que el nombre del conjunto.
Path-Compression Una optimizacion interesante es la compresion de caminos. Cada vez que hacemos un Find del nodo x y el Find recorre el arbol hasta
la raiz xr, pasando por los nodos x2 x3 : : :xr hacemos que xr sea el nuevo padre de todos estos nodos. De esta forma cualquier F ind que hagamos para estos nodos va a tardar O(1).
Algoritmos
170
Algorithm 60 Union(x,y)
LINK(Find-Set(x),Find-Set(y))
Algorithm 61 LINK(x,y) if rank x] > rank y] then p y] x else p x] y if rank x] = rank y] then rank y] rank y] + 1 end if end if
Algorithm 62 Find-Set(x), sin path compression if x = 6 p x] then p x] Find ; Set(p x]) end if
return p x]
Notemos que cada nodo tiene un campo rank. El \rank" de un nodo es una cota superior de la altura del nodo. Inicialmente es cero y solo se incremente en 1 cuando el nodo es una raiz y otra raiz de igual rango es linkeada como un nuevo hijo.
Analisis Puede probarse que si hacemos m operaciones fMakeSet Union Findg y n de ellas son Make ; Set el tiempo total es O(m (m n)), donde (m n) es
la funcion inversa de Ackermann, que se de ne de la siguiente forma: (m n) = minfi 0 : A(i bm=nc) > log ng Esta de nicion depende de A(m n) que es la funcion de Ackerman, que se de ne de la siguiente forma: A(1 j ) = 2j para j 1 171
A(i 1) = A(i ; 1 2) para i 2 A(i j ) = A(i ; 1 A(i j ; 1)) para i j 2 Esta funcion crece mas rapido que practicamente cualquier funcion que podamos imaginar. Por lo tanto (m n) es una funcion de crecimiento increiblemente lento, para cualquier caso practico podemos suponer que (m n) 4. No podemos4 probar que el costo de la estructura Union;Find es O(m (m n)), pero podemos probar un limite mas debil que es O(m log n). Donde la funcion log n es una funcion de crecimiento extremadamente lento y que de nimos informalmente como la cantidad de veces que debemos aplicar la funcion log a n para obtener un numero 1. Notemos que para todo n < 265535 log n < 5 Podemos asumir entonces que log n < 5 para cualquier caso practico5. .
Teorema 1 Tenemos que rank(x) < rank(p x]) salvo que x = p x]. Inicialmente rank(x) = 0 y rank(x) crece en forma monotona hasta x = 6 p x]. Demostracion Los nodos son linkeados de forma tal que el padre tiene mayor
rank o si ambos tienen el mismo rango entonces el rango del padre se incrementa. Por lo tanto rank(x) < rank(p x]) salvo que x sea su padre (x es raiz). Cuando x ya no es una raiz su rango no puede cambiar. Lqqd.
Teorema 2 Sea Size(x) el numero de nodos en el sub-arbol cuya raiz es x. Entonces size(x) 2rank(x) Demostracion Por induccion en size(x). Claramente es verdad cuando size(x) =
1, ya que rank(x) = 0. Cuando y es linkeado a x entonces si rank(y) < rank(x), size(x) aumenta pero el rango no cambia por lo que la desigualdad se mantiene. Si rank(y) = rank(x) = n entonces antes de linkear la desigualdad se mantiene para x y para y, por lo tanto size(x) 2n y size(y) 2n . Luego de que los nodos son linkeados, tenemos size(x ) 2n+1 y rank(x ) = rank(x) + 1 donde x es el nuevo x. Por lo tanto la desigualdad se mantiene. Lqqd.
0 0 0
Teorema 3 En cualquier momento hay a lo sumo n=2r nodos de rango r. Demostracion Supongamos que cuando el nodo x recibe el rango r rotulamos
todos los nodos en el sub-arbol de x con el rotulo x. Notemos que ningun nodo es rotulado mas de una vez, por el Teorema 1. Por el teorema 2, size(x) 2r , por lo tanto cada vez por lo menos 2r nodos son rotulados. Si hubiera mas de n=2r nodos de rango r habria un total de (n=2r )(2r ) nodos rotulados lo cual seria una contradiccion. Lqqd.
No queremos, no debemos, no sabemos Es importante que quede claro que estamos hablando de funciones de crecimiento extraordinariamente lento
4 5
172
Corolario El rango mas grande que puede tener un nodo es log n Teorema 4 Supongamos que reemplazamos una serie S de m operaciones
0 0 0 0
Make-Set, Union y Find por una serie S de m operaciones Make-Set, Link y Find (reemplazando cada Union por dos Finds y un Link). Entonces si S insume O(m log n) operaciones S insume O(m log n) operaciones.
m 3m .
0
Por el Teorema 4 podemos contar la cantidad de operaciones en funcion de la cantidad de Make-Sets. Links y Finds e ignorar el costo de las uniones. Ahora estamos listos para probar que O(m log n) acota el tiempo de ejecucion de la estructura Union-Find. Recordemos que m es la cantidad de operaciones Make-Set, Link, Find y n es la cantidad de nodos en total (la cantidad de MakeSet). Cada Make-Set es O(1) y hay n de ellos entonces el tiempo de los Make-Sets es O(n) = O(m) ya que m n. Cada Link insume O(1) y hay O(m) Links, el tiempo total es por lo tanto O(m). Queda por contar el costo de los Finds. Cuando hacemos un Find de un nodo x0 recorremos el camino x0 x1 : : : xi desde x0 hasta la raiz xi. El costo total del Find es la cantidad de nodos del camino. Vamos a calcular este costo rotulando cada nodo en cada camino recorrido por un Find con una letra B (costo de bloque) o una letra P (costo de camino). Una vez que esto fue realizado sumamos el total de Bs y P s para obtener el costo total. De nimos un Bloque de la siguiente manera: ponemos a cada nodo de rango r en el bloque log r por ejemplo: rango bloque 0,1 0 2 1 2 3,4 5,6,: : : ,16 3 17,: : : ,65536 4 etc etc
173
Los costos se asignan de la siguiente forma: Si un nodo es el ultimo en el camino hacia la raiz con su rango en un bloque dado, rotulamos el nodo B . El hijo del nodo rotulado B tambien recibe una B , el resto de los nodos del camino son rotulados con P . Tal vez queda mas claro de esta forma: xi es rotulado con B si p xi] = xi (es la raiz o su hijo=, o si log rank(xi) < log rank(p xi]). (el nodo y su padre estan en distintos bloques). Los restantes nodos del camino se rotulan con P .
Ejemplo Supongamos que hacemos un Find desde el nodo x0 y los rangos son
los siguientes:
En el ejemplo x1 x4yx5 recibirian el rotulo B mientras que x0 x2yx3 recibirian el rotulo P . Para computar el costo notemos que por el corolario del Teorema 3 hay a lo sumo log n bloques diferentes, por lo tanto hay a lo sumo (log n) ; 1 nodos que perteneces a un bloque distinto que el de su padra. Sumando 2 por la raiz y el hijo de la raiz tenemos a lo sumo 1 + log n rotulaciones con B . El total de operaciones Find es O(m), por lo tanto el total de rotulaciones B es O(m log n) Para contar las veces que rotulamos con P la tecnica no la incluimos porque resulta demasiado extensa. De lo anterior y de la cuenta de los rotulos P se llega a O(m log n) 174
Como puede verse esta implementacion de la estructura Union-Find es extremadamente e ciente, el uso de estructuras de datos apropiadas implementadas en forma e ciente es un punto clave en el disenio de algoritmos, la estructura Union-Find es de utilidad en gran cantidad de algoritmos, sobre todo en aquellos de tipo constructivo como por ejemplo el algoritmo de Kruskal.
10.7 Hash-Tables
Una de las estructuras de datos mas importantes es la familia de los diccionarios. Un diccionario es una estructura de datos que permite almacenar elementos de la forma (clave descripcion) donde la clave identi ca univocamente a cada elemento y la descripcion puede ser absolutamente cualquier cosa. Las operaciones basicas sobre esta estructura son tres: insertar un nuevo elemento en el diccionario. Buscar un elemento en el diccionario y opcionalmente recuperar su descripcion y por ultimo eliminar un elemento del diccionario. Para implementar este tipo de estructura existen varias opciones. En primer lugar se puede usar una lista enlazada con lo cual logramos una buena utilizacion de espacio en memoria (solo usamos lugar para los elementos insertados) pero perdemos mucha e ciencia al recuperar datos. La busqueda en una lista enlazada es O(n), por ejemplo. La segunda opcion consiste en usar una tabla de acceso directo o Look-UpTable, donde cada clave tiene una posicion reservada en la tabla, de esta forma la mayoria de las operaciones son O(1) pero como desventaja necesitamos una tabla con capacidad de almacenar todas las claves posibles. Si la clave es por ejemplo un numero de documento este tipo de esquema es inpracticable. Las Hash-Tables combinan la buena utilizacion del espacio de las listas enlazadas con la velocidad de acceso de las tablas de acceso directo. Basicamente una Hash-Table es una tabla de acceso directo en donde el acceso a la tabla no se hace directamente por la clave sino aplicando a la clave una funcion de hashing. La funcion de hashing convierte la clave en una cierta posicion de la tabla, la tabla se reserva del espacio que se crea conveniente. Por ejemplo para una clave que es un numero de 5 digitos podemos reservar solamente 100 posiciones y usar como funcion de hashing la funcion f (x) = x mod 1006.
10.7.1 Operaciones
6
175
1. Insert(e,key). Inserta un elemento e en la tabla con clave key. O(1) 2. Search(key). Busca en la tabla el elemento cuya clave es key. O(1) 3. Delete(key). Elimina de la tabla el elemento cuya clave es key. O(1) La clave debe ser una parte del elemento que sirve para identi carlo en forma univoca con respecto al universo de elementos. Por ejemplo el padron de un alumno sirve como clave para todo el conjunto de alumnos. De namos una hash-table como un vector H 0 m] donde m << jU j cantidad de claves posibles. Es decir que el espacio utilizado es muy cercano al espacio necesario. Una funcion de hashing h mapea las claves a indices de H es decir que h : U ! 0::m] o sea x 2 U f (x) = z z 2 0::m]. El problema es que la funcion de hashing puede producir colisiones lo cual implica que la funcion de hashing genera el mismo valor para dos claves distintas. h(k1) = h(k2) con k1 6= k2. Por ejemplo si h(x) = x mod 7 los valores x = 7 y x = 14 producen una colision. A las claves que colisionan se las llama sinonimos. Es necesario contar con algun metodo de resolucion de colisiones que permita resolver este problema. Las tecnicas basicas para resolver colisiones son: 1. Encadenamiento Tambien llamado direccionamiento cerrado, los elementos que colisionan se encadenan en una lista enlazada. 2. Direccionamiento Abierto Cuando ocurre una colision se busca una posicion vacia de la tabla para el elemento en cuestion. Hay varias formas de buscar este lugar: Lineal Se prueba en forma sucesiva y lineal: x,x+1,x+2,: : : Cuadratico Se prueba mediante valores que se incrementan en forma cuadratica. Doble hashing Una segunda funcion de hashing determina donde probar. Mas adelante consideraremos las funciones de hashing, por el momento supongamos que nuestra funcion de hashing satisface:
Principio de Uniformidad Simple Cualquier clave tiene la misma probabilidad de ser hasheada a cualquier ubicacion de la tabla H. La probabilidad de que a una clave x le corresponda la posicion z en la tabla es la misma para todo
176
Todos los sinonimos se encadenan en una lista enlazada. De esta forma, cada posicion de la tabla es en realidad una lista enlazada que tiene tantos elementos como colisiones hayan ocurrido en dicha posicion.
Operaciones
Insert(x): Calcular h(x) e insertar x en la cabeza de la lista en H h(x)]. (1) siempre. Search(x): Calcular h(x) y buscar x en la lisra enlazada en H h(x)]. O(n) en el peor caso. Delete(x): Calcular h(x) y buscar x en H h(x)] para eliminarlo. O(n) peor caso.
Como en todo analisis de primitivas no estamos tan interesados en el peor caso sino en el caso medio si realizamos n operaciones.
(1) para
Nota: Si x es el iesimo elemento agregado a H entonces la longitud esperada de H h(x)] antes de agregar x es im1
;
(1) +
n X
n 1 i;1 X (1) + ( + 1)
(1) +
1 (1) + nm
n 1X (i ; 1) + n 1 i=1
i=1
1 (n ; 1)n + 1 n (1) + nm 2 n n 1 (1) + 2m ; 2m + 1 1 ; 1 + 1 = (1 + ) (1) + 2 2m Por lo tanto Search(x) es en promedio (1 + ) lo cual es (1) si = (1) lo cual ocurre si n < m.
H es un vector simple, todos los elementos van a alguna posicion de H . Sucesivamente se prueban distintas posiciones de H cuando ocurre una colision hasta encontrar un lugar para x. La funcion de hashing recibe como parametro adicional el numero de intento.
Secuencia de prueba: h(k 0) h(k 1) : : :mh(k m ; 1) establece las posiciones a probar si ocurre una colision. Si h(k 0) esta ocupado probamos con h(k 1) etc. En el peor caso se examina toda la tabla.
borrado.
libre: La posicion nunca fue ocupada. ocupada: La posicion esta ocupada por un elemento. borrada: La posicion fue ocupada alguna vez pero el elemento ha sido
Insert(x) Calculamos h(x i = 0). Si esta libre o borrado insertamos x alli. Si la posicion esta ocupada probamos con h(k i + 1) hasta encontrar un lugar libre o borrado. Marcamos el lugar donde insertamos x como ocupado. Delete(x) Calculamos h(x i = 0). Si la posicion esta ocupada por x eliminamos el elemento marcandolo como borrado. Si la posicion esta libre se produce un error x no inexistente. Si la posicion esta ocupada por otro elemento o esta borrada seguimos buscando con h(x i +1) hasta encontrar al elemento o a una posicion libre en cuyo caso la busqueda se detiene pues si el elemento existiera deberia estar alli. Search(x) Calculamos h(x i = 0). Si la posicion esta ocupada por x n. Si la posicion esta ocupada por otro elemento o esta borrada seguimos buscando con h(x i + 1), si al buscar nos topamos con una posicion libre termina la busqueda con x inexistente.
Operaciones
Ventajas y desventajas La principal ventaja de este metodo es que resulta sumamente sencillo de implementar. La desventaja reside en que genera un fenomeno conocido como clustering, largas zonas del vector ocupadas y otras
desocupadas, los datos tienden a distribuirse en forma despareja. 179
h(k 0) = h(k) h(k i) = (h(k) + c1 i + c2 i2 ) mod m Donde c1 y c2 son constantes. Al igual que en el metodo lineal hay tantas secuencias de prueba como posiciones tiene la tabla y por lo tanto el metodo no es uniforme.
todos las posiciones de H debemos tener h2(k) relativamente primo a m (sin divisores en comun). Esto es facil de conseguir asegurando que m sea un numero primo. Si m es una potencia de 2 y h1(k) = k mod m entonces h2(k) siempre debe ser impar. Para este caso hay mas de m secuencias de prueba pero de todas formas el metodo no es uniforme.
Demostracion
P ipruebas] = P 1] P 2] : : :P i ; 1] n n ; 1 : : : n ; (i ; 1) P i] = m m ; 1 m ; (i ; 1) n )i P i] ( m P i] i 180
X
1
i=0
X
1
1 1;
i=0
Por ejemplo si = :5 entonces el numero promedio de pruebas es 1 1:5 = 2. Si la tabla esta llena en un 90% el numero de pruebas promedio es 10.
;
=m n
n ;1 X i=0
m;i
181
=1
1 dx x j =m n = 1 ln m m ;n 1 = 1 ln 1 ;
;
1 Zm
j =m;n+1 j
m X 1
No vamos a realizar un estudio de funciones de hashing debido a que no es tema de la materia, basicamente hay dos metodos basicos para construir una funcion de hashing. h(k) = k mod m Para que la funcion sea buena debe ser lo mas uniforme posible, esto ocurre cuando m es un numero primo.
Metodo de la multiplicacion
h(k) = bm(k A mod 1)c Donde A es una constante tal que 0 < A < 1. kA mod 1 es la parte fraccional de kA es decir kA ; bkAc. El valor de m no es tan critico en este metodo como en el de la division. La distribucion depende mas del valor de A, un valor de A que suele tener buen resultado es = 52 1 . Otra ventaja de este metodo es que las multiplicaciones suelen costar menos que las divisiones en la mayoria de las arquitecturas conocidas.
p ;
En principio se repasaron ciertas estructuras de datos conocidas como ser pilas, colas, listas, arboles, etc. Tener conocimiento de estas estructuras es fundamental para el disenio de algoritmos. El analisis amortizado es la herramienta que debe utilizarse para analizar la e ciencia de las primitivas que implementan la funcionalidad de una cierta estructura de datos. Describimos el metodo de agregacion, el contable y el de los potenciales del analisis amorizado. Los Heaps-Binomiales presentan una implementacion sumamente e ciencia para una estructura generica de Heaps Mergeables, para describir esta estructura hacemos uso de los arboles binomiales que tambien son nuevos. La estructura Union-Find es necesaria para manejo de estructuras basadas en conjuntos disjuntos, describimos y analizamos diversas implementaciones de esta estructura. Por ultimo las hash-tables son la opcion habitualmente mas e ciente para implementar estructuras de tipo diccionario.
10.9 Ejercicios
1. Insertar las siguientes claves: 5 28 19 15 20 33 12 17 10 En una hash-table con resolucion de colisiones por encadenamiento con m = 9 y con h(k) = k mod m 2. Insertar las siguientes claves: 10 22 31 4 15 28 17 88 59 En una hash-table con m = 11 usando direccionamiento abierto usando h(k) = k mod m. Emplear: (a) Direccionamiento abierto lineal. (b) Direccionamiento abierto cuadratico con c1 = 1 y c2 = 3 (c) Doble-hashing con h2(k) = 1 + (k mod (m ; 1)) 3. La operacion Binomial-Heap-Minimum puede funcionar incorrectamente si las claves pueden tomar valor 1, explicar porque y re-escribir la primitiva para poder manejar esta situacion. 4. Mostrar paso a paso la estructura resultante luego de realizar las siguientes operaciones: 183
for i = 1 to 16 do Make-Set(x) end for for i = 1 to 15 by 2 do Union(Xi,Xi+1) end for for i = 1 to 13 by 4 do Union(Xi,Xi+2) end for
Union(X1,X5) Union(X11,X13) Union(X1,X10) Find-Set(X2) Find-Set(X9)
184
Chapter 11
Complejidad
11.1 Introduccion
A lo largo del curso hemos mencionado en algunas oportunidades problemas para los cuales no teniamos una solucion e ciente, problemas que requerian para ser solucionados algoritmos de orden exponencial o superexponencial. En particular teniamos problemas con algoritmos de tipo O(cn) O(nn) O(n!). En la decada del `70 cientos de problemas estaban en esta situacion de incertidumbre. La teoria de los problemas NP-Completos desarrollada por Stephen Cook y Richard Karp aporto las herramientas necesarias para poder comprender a este tipo de problemas y para poder clasi car a un problema de acuerdo con su di cultad para resolverlo algoritmicamente.
Es importante comprender cual es el verdadero problema de los algoritmos de orden exponencial, uno podria suponer que el problema es que dichos algoritmos son ine cientes y que el escollo puede salvarse usando hardware mas poderoso, este concepto es equivocado. En el gra co podemos ver el tiempo necesario para resolver un problema de acuerdo al orden del algoritmo utilizado en una computadora de primera linea. Como podemos ver los algoritmos que son exponenciales necesitan varios anios para resolver un problema en el cual n > 100, si el orden es n! para n > 20 necesitamos mas de un siglo de calculos!. El problema 185
de los algoritmos de este tipo es que para un cierto n no muy grande el problema sencillamente no se puede resolver.
11.1.1 Reducciones
Problema-X(Q)
Supongamos que tenemos un problema denominado problema X y que el problema X puede resolverse de la siguiente forma: 1. Convertir Q en R que es una instancia del problema Y . R = f (Q). 2. Usar una rutina que resuelve el Problema-Y de la forma Z = Problema ; Y (R) 3. Convertir Z en W aplicando la inversa de f . W = f 1 (Z ). 4. Devolver W como la solucion de Problema-X(Q). La transformacion de instancias de un tipo de problema en instancias de otro tipo de problema de forma tal que las soluciones se preservan se denomina Reduccion. Las reducciones fueron introducidas por Richard Karp y son la herramienta fundamental para manejarnos dentro del mundo de los problemas NP-completos.
;
Si nuestra reduccion traduce Q en R en O(P (n)) entonces. 1. Si Problema-Y(R) es O(P (n)) entonces el Problema-X puede resolverse en O(P (n) + P (n)) 2. Si sabemos que (P (n)) es una cota inferior para resolver el Problema-X, entonces (P (n) ; P (n)) tiene que ser una cota inferior del Problema-Y. La segunda propiedad es la que vamos a utilizar para la tarea nada grata de demostrar que un problema es demasiado complejo como para poder resolverlo.
0 0 0 0
junto de puntos en el plano encontrar el poligono convexo mas extenso que es frontera de dichos puntos. En la gura vemos una solucion a un ejemplo.
186
Supongamos que disponemos de una rutina Convex-Hull(V) que dado un vector V de n puntos calcula el poligono convexo frontera y lo devuelve en un vector. Sorprendentemente aplicando una reduccion podemos utilizar este problema para diseniar un curioso algoritmo de sort. Dado un vector W de n numeros que queremos ordenar lo convertimos en un vector de puntos en el plano donde W i]:x = W i] W i]:y = W i]2, al realizar esta transformacion los puntos forman una parabola.
187
Como la parabola es convexa cada punto de la parabola forma parte del poligono convexo frontera, ademas como los puntos vecinos en el poligono tienen valores de x vecinos el poligono covexo externo devuelve los puntos ordenados por sus coordenadas x (es decir que ordena los numeros originales).
Algorithm 63 Sort-CX(V,n). Ordena el vector V. for i = 1 to n do end for Convex ; Hull(W n) for i = 1 to n do V i] W i]:x end for Analisis Crear el vector W y luego copiarlo en V se hace en O(n). Recordemos que la cota inferior de un algoritmo de sort era (n log n). Si pudieramos hacer el procedimiento Convex ; Hull en menos de (n log n) entonces podriamos
ordenar en menos de (n log n) ya que un problema se puede reducir al otro!. Por lo tanto el algoritmo Convex-Hull aunque no sepamos hacerlo debe ser (n log n). Como podemos ver una reduccion es una herramienta muy poderosa que nos permite demostrar propiedades de algunos problemas aun cuando no sabemos como resolverlos. W i]:x W i]:y V i] V i]2
Que es un problema Un problema es una pregunta generica con parametros para los datos de entrada y condiciones que establecen como debe ser una solucion satisfactoria a dicho problema. Instancia de un problema Una instancia de un problema es un problema
aplicado a un cierto conjunto de parametros. las respuestas posibles son Si o No.
11.2 Fundamentos
188
Problema generico
i=1
Solucion
fv1 v2 v3 v4g
Costo=27.
Problema de decision
Dado un grafo G = (E V ) y un entero k, indicar si existe una solucion al problema del viajante tal que el costo sea k.
189
Usando busqueda binaria y el problema de decision del viajante podemos encontrar la solucion optima al problema generico del viajante. lemas de decision, el problema generico puede resolverse en general en tiempo polinomico si es posible resolver el problema de decision en tiempo polinomico.
11.3 Complejidad
Necesitamos alguna forma de diferenciar aquellos problemas que se pueden resolver de aquellos que no debido a que no existe un algoritmo e ciente para solucionarlos. Esta distincion la vamos a hacer considerando aquellos problemas que se pueden resolver en tiempo polinomico. Hemos medido el costo en tiempo de un algoritmo expresando el orden del mismo en funcion del tamanio de los datos de entrada n. Un algoritmo es de orden polinomico si su orden es O(nk )1 donde k es constante y no depende de n. Un problema es solucionable en tiempo polinomico si existe un algoritmo de orden polinomico que resuelve el problema. Algunas funciones no parecen polinomicas pero lo son, por ejemplo O(n log n) no parece un polinomio pero esta acotada superiormente por O(n2) y por lo tanto puede considerarse polinomica.
La clase P de problemas esta integrada por todos los problemas que son solucionables mediante un algoritmo de orden polinomico.
Algunos problemas no se pueden resolver en tiempo polinomico pero dada una solucion candidata es factible veri car si la solucion es optima en tiempo polinomico. Estos problemas forman la clase NP de problemas.
no dirigido. Dado un grafo G = (E V ) no dirigido encontrar un ciclo que visite cada vertice de G exactamente una vez.
190
Un aspecto interesante de este problema es que si el grafo tiene un ciclo hamiltoniano es facil veri carlo. Por ejemplo dado el ciclo hv3 v7 v1 : : : v13i. Podemos inspeccionar el grafo y veri car que sea un ciclo y que ademas visite todos los vertices exactamente una vez. Por lo tanto aunque no sepamos como resolver el problema del ciclo Hamiltoniano si disponemos de un metodo e ciente para veri car una posible solucion del problema. La solucion se denomina certi cado. Los problemas de tipo NP son veri cables en tiempo polinomico, por lo tanto el problema del ciclo Hamiltoniano pertenece a la clase NP .
lema es solucionable en tiempo polinomico entonces tambien es veri cable en tiempo polinomico. Lo que no se sabe es si P = NP , parece poco sensato pensar que las clases son iguales, es decir que poder veri car un problema en tiempo polinomico no necesariamente implica poder solucionarlo en tiempo polinomico, es aceptado que P 6= NP pero nadie ha podido demostrarlo hasta ahora, este es uno de los puntos abiertos mas importantes en el mundo de la computacion. P =?NP
Los problemas NP ; Completos son los problemas mas dificiles que vamos a enfrentar en este curso, existen sin embargo problemas aun mas dificiles que los problemas NP C que se denominan problemas NP ; Hard.
191
Es valido decir que NPC NP es decir que los problemas NP-Completos se pueden veri car en tiempo polinomico. Los problemas NP-Completos son problemas NP que cumplen la propiedad de que si existiera una solucion polinomica a uno de ellos entonces todos los demas serian solucionables en tiempo polinomico. Los problemas No ; NP son problemas que ni siquiera pueden veri carse en tiempo polinomico. Por ejemplo: Cuantos tours de longitud menor o igual que K existen en un grafo G en el problema del viajante. Dado que hay un numero exponencial de tours no podemos contarlos por lo que no podriamos veri car la solucion en tiempo polinomico. Este es un problema que no petenece a la clase NP. En general este tipo de problemas no son de mucha utilidad. Una historia En 1903 T.E.Bell y otros matemaaticos tenian como gran interrogante si el numero 267 ; 1 era o no primo. En la reunion de la sociedad matematica americana en Nueva York F.Cole presento un paper con el modesto titulo \Sobre la factorizacion de numeros muy grandes". Cuando le llego el turno de exponer su trabajo F.Cole -que siempre fue una persona muy callada- camino hacia el frente y con sumo cuidado procedio a calcular 267, luego le resto uno. Se movio hacia el otro extremo del pizarron y escribio 193707721x761838257287 y usando el metodo que todos aprendimos en la primaria hizo la cuenta, los dos numeros coincidieron sin decir una sola palabra volvio a su asiento. Por primera y unica vez la sociedad matematica americana aplaudio extensamente al autor de un paper sin siquiera leerlo. A Cole se le hizo una sola pregunta: cuanto tiempo le habia llevado la factorizacion del numero a lo cual respondio: \150 domingos". 192
Como puede verse hay problemas que son extremadamente simples de vericar pero muy di ciles de resolver. 2
11.4 Reducciones
Las reducciones son utiles para probar que un problema no puede resolverse en tiempo polinomico. Supongamos que queremos probar que el problema B no puede resolverse en tiempo polinomico y que disponemos de un problema A que sabemos que no se puede resover en tiempo polinomico. Si suponemos que hay una solucion polinomica para B y mediante una reduccion podemos llegar a que con esa solucion podemos resolver A en tiempo polinomico llegariamos a una contradiccion y entonces podriamos a rmar que B no puede resolverse en tiempo polinomico. La clave para demostrar que un problema B no puede resolverse en orden polinomico esta en poder reducir B a un problema A del cual sabemos no existe una solucion polinomica.
11.4.1 Ejemplo I
Es un hecho que el problema del ciclo Hamiltoniano en un grafo no-dirigido es Np-Completo. Es decir que no se conoce algoritmo que pueda resolverlo en tiempo polinomico y los expertos ampliamente estan convencidos de que tal algoritmo no puede eixstir. Tratemos de ver que ocurre con el problema de encontrar un ciclo Hamiltoniano en un grafo dirigido. Luego de pensar durante mucho tiempo podemos llegar a la sospecha de que no existe un algoritmo polinomico para este problema, entonces podemos tratar de demostralo aplicando una reduccion. Dado un grafo no-dirigido G = (E V ) creamos un grafo dirigido G reemplazando cada arista del grafo no-dirigido por un par de aristas en el grafo dirigido de forma tal que la arista no-dirigida (u v) se convierte en las aristas dirigidas (u v)y(v u). Ahora cada camino simple en G es tambien un camino simple en G y viceversa. Por lo tanto G tiene un ciclo Hamiltoniano solo si G tambien lo tiene. Si pudieramos resolver el problema del ciclo hamiltoniano en G podriamos resolver el problema del ciclo Hamiltoniano en G ya que un camino en G es tambien un camino valido en G. Por lo tanto el problema del ciclo Hamiltoniano dirigido tambien es NP-Completo.
0 0 0 0 0
2 Curiosamente no se ha podido demostrar que la factorizacion sea un problema NPCompleto, la clasi cacion de este problema es uno de los misterios que aun no se han resuelto y del cual curiosamente depende la seguridad de varios organismos ya que las funciones criptogra cas suelen estar basadas en la factorizacion de numeros.
193
Notemos que no hemos resuelto ninguno de los dos problemas, solo mostramos como podemos convertir la solucion de un problema P 1 en una solucion valida para otro problema P 2. A esto se lo denomina reducir P 1 a P 2.
uno de sus vertices puede ser pintado con uno de 3 (tres) colores distintos de forma tal que dos vertices adyacentes no esten pintados del mismo color.
Cubrimiento con un Clique (CC) Dado un grafo G y un entero k determinar si los vertices de G pueden ser particionados en subconjuntos V 1 V 2 : : : V k de forma tal que i V i = V y que cada Vi es un clique.
Recordemos que un clique es un subconjunto de vertices de forma tal que cada par de vertices del subconjunto son adyacentes entre si. (3COL) es un problema NP-Completo conocido, veamos si usando este problema podemos probar que (CC) tambien es NP-Completo.
Demostracion Tenemos que probar dos cosas, en primer lugar debemos probar
que (CC) pertenece a NP, esto es sencillo ya que dados k conjuntos veri car que la union de los mismos forme el grafo y que cada uno de ellos es un clique puede hacerse en tiempo polinomico.
En segundo lugar debemos probar que un problema NP-Completo conocido es reducible en forma polinomica a (CC). En nuestro caso el problema elegido va a ser (3COL). Asumimos que existe una subrutina CliqueCover(G,k) polinomica que determina si un grafo tiene un cubrimiento clique de tamanio k. Necesitamos poder usar esta rutina para resolver (3COL).
194
Veamos en que aspectos estos problemas son similares. Ambos problemas dividen los vertices en grupos, en (CC) para que dos vertices esten en un grupo deben ser adyacentes. En (3COL) para que dos vertices esten en el mismo grupo no deben ser adyacentes. En cierta forma los problemas son similares pero la nocion de adyacencia esta invertida. Recordemos que Gc es el grafo complemento de G es decir que tiene las aristas que le faltan a G y no tiene las aristas que tenia G. Nuestra observacion es que G es (3COL) si y solo si Gc tiene un cubrimiento clique con k = 3. La demostracion no es dificil y queda como ejercicio.
Usando esta observacion podemos reducir (3COL) a (CC) de la siguiente manera. Dado un grafo G obtenemos su complemento Gc y luego invocamos a Clique-Cover(Gc ,3). Luego cada uno de los subconjuntos que devuelve Clique ; Cover corresponde a un color en el grafo original y el problema (3COL) queda resuelto. Si podemos resolver (CC) en tiempo polinomico entonces podriamos resolver (3COL) en tiempo polinomico, pero como sabemos que (3COL) es NP-Completo entonces (CC) tambien es NP-Completo. Es importante notar que la reduccion funciona siempre mostrando como la solucion del problema que se sospecha NP-Completo puede usarse para encontar una solucion al problema que se sabe es NP-Completo. Esto es importante. 195
Importante: Siempre se debe probar que el problema NP-Completo se puede reducir al problema que sospechamos es NP-Completo. Notacion Cuando realizamos una reduccion utilizaremos la siguiente notacion: X/Y
Donde: X es un problema que se sabe es NP-Completo, Y es el problema que queremos probar que es NP-Completo, los pasos a seguir son: 1. Demostrar que Y pertenece a NP , es decir que dada una solucion se la puede veri car en tiempo polinomico. 2. Suponer que el problema Y se puede resolver en tiempo polinomico. 3. A partir de una instancia del problema X aplicar una transformacion que genere una entrada valida para el problema Y 4. Demostrar que una vez solucionado Y la solucion permite encontrar una solucion a la instancia del problema X planteada.
11.5.1 SAT
Ejemplo 1
Ejemplo 2
C = (v1 ^ v2) _ (v1 ^ ;v2) _ (;v1) Por mas que probemos y probemos no existen valores para v1 y v2 de forma tal que la expresion sea verdadera.
3
196
Claramente SAT pertenece a NP, ya que dada una expresion y un conjunto de valores para sus variables podemos chequear en tiempo polinomico si la expresion evalua como verdadera, en la decada del `70 Cook se encargo de demostrar que SAT era un problema NP-Completo, volveremos sobre esta demostracion mas adelante, por ahora supongamos que SAT es NP-Completo y como veremos a continuacion a partir de SAT y aplicando reducciones es sencillo demostrar que muchos otros problemas son NP-Completos. 3-SAT es una variacion de SAT en la cual cada \ororia" tiene exactamente tres terminos, de la forma: (v11 ^ v12 ^ v13) _ (v21 ^ v22 ^ v23) _ : : : _ (vn1 ^ vn2 ^ vn3) Claramente 3-SAT puede veri carse en tiempo polinomico y por lo tanto pertenece a NP, pero al ser un problema mas restringido que SAT podriamos pensar que 3-SAT no es NP-Completo (tal vez sea la cantidad ilimitada de terminos en cada ororia la fuente de la di cultad de SAT).
Si k = 3 Ci = (z 1 z 2 z 3) no realizamos ninguna modi cacion Si k > 3 Ci = (z 1 z 2 : : : zn) creamos una cadena de la forma: (v1 z 1 v1)(v1 z 2 ;v1)(v1 z 3 ;v1) : : : (v1 zn ; 1 ;v1)(;v1 zn ;v1) De esta forma si todas las variables son falsas no hay forma de que la expresion de verdadero, pero si una sola de ellas es verdadero entonces es posible asegurar que todas las demas sean verdadero. Esto puede resultar dificil de ver en forma inmediata pero vale la pena dedicar un tiempito a observarlo. Una vez aplicada esta transformacion si podemos resolver el problema 3-SAT entonces tambien resolvimos el problema SAT ya que ambos problemas son equivalentes, como SAT es un problema NP-Completo conocido resulta que 3SAT debe ser entonces NP-Completo. Un problema de programacion lineal entera esta dado por un conjunto de variables que deben ser enteras y no negativas (condicion de no-negatividad), un conjunto de inecuaciones lineales que involucran a dichas variables y una funcion tambien lineal que se debe maximizar o minimizar.
Ejemplo
x1 + x2 5 x2 ; x3 x2 f (x) = x1 + 2x2 ; 3x3(Max) La conversion a un problema de decision se hace jando una constante para la funcion a optimizar. Por ejemplo: x1 + 2x2 ; 3x3 100
198
Suponemos que existe una solucion polinomica a PLE, entonces dada una expresion de SAT de la forma: C = (v11 v12 : : :v1a)(v21 v22 : : : v2b) : : : (vn1 vn2 : : : vnz ) Podemos escribir a partir de la expresion un problema de programacion lineal de la forma: vij 1 v11 + v12 + : : : + v1a 1 v21 + v22 + : : : + v2b 1 etc La funcion a maximizar o minimizar no tiene importancia, vale por ejemplo maximizar o minimizar cualquiera de las variables. Observemos que si resolvemos el problema de PLE los valores que quedan asignados a las variables satisfacen la expresion SAT, ya que cada inecuacion se cumple si y solo si al menos una de las variables de PLE es mayor que cero lo cual implica que al menos una de las variables de la clausula de SAT es verdadera. Como deben cumplirse todas las inecuaciones tenemos que deben cumplirse todas las clausulas de SAT. Por lo tanto todo problema de PLE es NP-Completo.
tera podrian deducir en este momento que aquellos problemas que se pueden expresar como un problema de PLE son NP-Completos, esto no es cierto y es un mal-uso del mecanismo de reducciones, el uso de PLE para probar que un problema es NP-Completo implicaria demostrar que solucionando un determinado problema \X" en tiempo polinomico podemos resolver \Cualquier" problema de PLE, por lo tanto \X" debe ser NP-Completo. Hay problemas (muchisimos) que se pueden resolver en tiempo polinomico y tambien pueden escribirse como un problema de PLE (obviamente usar PLE para estos problemas es ine ciente pero no imposible).
Dado un grafo no dirigido G = (V E ) indicar si existe un conjunto de a lo sumo k vertices de forma tal que cada arista del grafo parte o va hacia uno de los vertices del conjunto. En la gura podemos ver un ejemplo.
199
una posible solucion podemos veri car que toda arista del grafo involucre a algun vertice del conjunto en forma sencilla en tiempo polinomico. Para demostrar que es NP-Completo vamos a aplicar una reduccion a partir de 3-SAT que ya demostramos es NP-Completo. Suponemos pues que existe una solucion polinomica para VC. Para convertir una instancia de 3-SAT en un problema de VC partimos de un problema 3-SAT con N variables y C clausulas y construimos un grafo con 2N +3C vertices.Para cada variable creamos dos vertices unidos por una arista.
Para cubrir las aristas por lo menos n vertices deben pertenecer al conjunto, uno por cada par. Para cada clausula creamos tres nuevos vertices, uno por cada literal en cada clausula y los conectamos en un triangulo. Por lo menos dos vertices por triangulo deben pertenecer al conjunto para cubrir las aristas que son lados del triangulo lo cual implica 2C vertices. 200
Finalmente conectamos cada literal en la estructura plana a los vertices de los triangulos que tienen el mismo literal.
Aseguramos que el grafo resultante tiene una solucion a VC de tamanio N+2C si y solo si la expresion 3-SAT puede evaluarse como verdadera. De nuestro analisis anterior al construir el grafo toda solucion a VC debe tener al menos N+2C vertices. Requeriria un analisis mas riguroso pero si nos tomamos algunos minutos analizando la transformacion que hicimos de 3-SAT se nos permite asegurar que si encontramos una solucion para VC en el grafo construido a partir de dicha solucion encontramos una solucion a 3-SAT. Por lo tanto VC es NP-Completo.
Esta reduccion fue bastante ingeniosa, como podemos ver hay que tener un criterio muy amplio y gran imaginacion para encontrar la reduccion apropiada para demostrar que un problema es NP-Completo.
201
El problema del conjunto independiente consiste en encontrar un conjunto de vertices (de tamanio k) de un grafo de forma tal que ninguna arista del grafo conecte dos vertices de dicho conjunto. La reduccion es muy sencilla ya que si calculamos el VC de un grafo los vertices restantes (los que no pertenecen a VC) forman un conjunto independiente ya que si existiera una arista entre dos de estos vertices los restantes no podrian cubrir todas las aristas y no serian un VC.
Por lo tanto VC e IS son problemas equivalentes y como VC es NP-Completo IS tambien debe serlo.
El problema del Clique consiste en encontra un subconjunto de K vertices de un grafo de forma tal que dicho subconjunto de vertices posee todas las aristas posibles (es fuertemente conexo).
202
La ilustracion muestra un grafo con un clique de tamanio 5. Probar que el problema del clique es Np-Completo es sencillo ya que si calculamos el conjunto independiente de tamanio x en un grafo de v vertices resulta que el grafo complemento del anterior tiene un clique de tamanio v ; x. Por lo tanto el problema del clique es equivalente al problema del conjunto independientey debe ser NP-Completo.
Dado un conjunto de enteros S y un entero T determinar si existe un subconjunto de S cuya suma es exactamente igual a T . Ejemplo S = f1 4 16 64 256 1040 1041 1093 1284 1344g y T = 3754. 203
Realizar cambios locales a la estructura del problema. Un ejemplo de esta tecnica es la que utilizamos en (SAT / 3-SAT).
11.6.3 Recomendaciones
Al realizar una reduccion X / Y buscar que X sea tan simple (restringido) como sea posible. Nunca usar el problema del viajante generico como problema fuente, en su lugar usar el problema del ciclo hamiltoniano donde todos los pesos son 1 o 1 o mejor aun usar el problema del camino hamiltoniano, o aun mejor todavia usar el problema del camino hamiltoniano en grafos planares dirigidos donde cada vertice tiene grado 3, etc. Todos estos problemas son NP-Completos y pueden usarse para hacer una reduccion, entre problemas equivalentes elegir siempre al mas restringido, de esta forma la reduccion sera menos trabajosa. Hacer que el problema destino Y en X / Y sea lo mas generico posible (cuanto mas dificil mas facil probar que es NP-Completo). Seleccionar el problema fuente correcto. Elegir el problema NP-Completo conocido para hacer la reduccion es sumamente importante y es la causa fundamental de problemas en demostrar que un problema es NP-Completo, en general lo mas recomendable es contar con una base de problemas NPCompletos quese conocen en gran profundidad (se conocen los problemas y sus variantes y el grado de di cultad de las mismas). Una buena base podria formarse con los siguientes problemas: 204
La teoria de los lenguajes formales se encarga de estudiar cuan poderosa debe ser una maquina para reconocer si un string pertenece a un lenguaje en particular. Por ejemplo podemos testear si un string es la representacion binaria de un numero par, esto se puede hacer con un automata simple.
Observemos que los problemas de decision pueden ser pensado como un problema de reconocimiento de un lenguaje formal. Las instancias de los problemas son codi cadas como strings y los strings son incluidos en el lenguaje solamente si la respuesta al problema es \Si".
205
Para reconocer estos lenguajes es necesaria una maquina de Turing. Cualquier algoritmo que resuelva el problema es quivalente a la maquina de turing que reconoce si un determinado string pertenece a un lenguaje. Esto nos va a servir para establecer un marco formal que nos permita precisar que queremos decir cuando a rmamos que un problema no puede o si puede ser resuelto en tiempo polinomico.
Supongamos que extendemos la maquina de Turing agregandole un \modulo adivinador" que en tiempo polinomico construye una posible solucion a un determinado problema. Para convencernos de que realmente es una solucion podemos correr otro programa que lo veri que. A esta maquina extendida la llamamos maquina de Turing No-Deterministica. Por ejemplo para el problema del viajante el modulo adivinador genera una permutacion de los vertices y luego podemos veri car que formen un camino y que la suma de los pesos sea menor que k. La clase de lenguajes que podemos reconocer en tiempo polinomico en funcion del tamanio del string en una maquina de turing deterministica (sin modulo adivinador) se denomina \P ". La clase de lenguajes que podemos reconocer en tiempo polinomico en funcion de la longitud del string en una maquina de Turing no-deterministica se denomina \NP ". Es claro que P 2 NP ya que cualquier programa que corra en una maquina de Turing deterministica puede correr en una maquina no-deterministica simplemente ignorando al modulo adivinador. Un problema que pertenece a NP para el cual un algoritmo polinomico implicaria que todos los lenguajes de NP pertenecen a P se denomina NP ; Completo.
Teorema: SAT es NP-Completo. Esta demostracion que afortunadamente no vamos a incluir fue realizada por Cook en la decada del `70 basandose en reducir a todas las maquinas de Turing a SAT, sabiendo la de nicion de un lenguaje NPC, Cook demostro que si existiera una solucion polinomica para SAT entonces seria posible veri car en una maaquina de Turing deterministica un lenguaje NPC, como sabemos que tal cosa no es cierta se demuestra que SAT es NP-Completo. La demostracion es de suma importancia ya que a partir de
206
11.9 Ejercicios
1. Dado un vector de 3 elementos escribir un problema de PLE que ordene el vector (que devuelva en variables enteras I 1 I 2 I 3 la posicion del elemento de cada vector. Por ejemplo si V = 13 1 6 deberia obtenerse I 1 = 3 I 2 = 1 I3 = 2 2. Realizar las siguientes reducciones. (a) HC / TSP (HC=Ciclo hamiltoniano, TSP=Problema del viajante). (b) TSP / PLE Aquellos que esten interesados en este tema deben leer este libro:
Computers and Intractibility: A guide to the theory of NP-completeness Michael R. Garey and David S. Johnson W. H. Freeman, 1979.
207
Chapter 12
La gura muestra un comparador, una red de ordenamiento para un secuencia de longitud n esta formada por n conectores o cables sobre los cuales se disponen los comparadores.
La gura muestra una red de ordenamiento que permite ordenar una secuencia de cuatro elementos utilizando 5 comparadores.Por ejemplo si la secuencia a ordenar fuera h3 1 4 2i la red realizaria lo siguiente.
1. 2. 3. 4. 5. 6. 7. 3-1-4-2 1-3-4-2 1-3-2-4 1-3-2-4 1-3-2-4 1-2-3-4 1-2-3-4 (inicial) (primer comparador) (segundo comparador) (tercer comparador) (cuarto comparador) (quinto comparador) (final)
209
cesador el procedimiento realizaba 5 comparaciones (una por comparador), sin embargo usando varios procesadores podemos realizar mas de una comparacion al mismo tiempo, a los efectos teoricos vamos a suponer que podemos realizar un numero ilimitado de comparaciones al mismo tiempo. Si observamos la red de ordenamiento vamos a ver que los comparadores 1 y 2 se pueden activar al mismo tiempo, luego los comparadores 3 y 4 se activan a la vez y por ultimo el comparador 5, por lo que la secuencia con paralelismo es:
1. 2. 3. 4. 5. 3-1-4-2 1-3-2-4 1-3-2-4 1-2-3-4 1-2-3-4 (inicial) (primer y segundo comparadores) (tercer y cuarto comparadores) (quinto comparador) (resultado final)
Debemos de nir cuando dos comparadores se pueden activar en paralelo: N comparadores pueden activarse en paralelo si y solo si no tienen conectores en comun y ademas los 2N conectores involucrados no tienen comparadores previos sin activarse. De acuerdo a esto los comparadores 1 y 3 de nuestro ejemplo no pueden activarse a la vez pues tienen al conector 1 en comun.
La e ciencia de una red de ordenamiento se mide de acuerdo a su profundidad. La profundidad de un conector esta dada por la cantidad de comparadores que inciden sobre el. La profundidad de la red es el maximo entre las profundidades de sus conectores. En nuestro ejemplo tenemos:
Conector Conector Conector Conector 1 2 3 4 = = = = Profundidad Profundidad Profundidad Profundidad 2 3 3 2
Profundidad de la red : 3
210
12.2.3 Ejemplo I
La siguiente red que ordena una secuencia de ocho elementos esta basada en el metodo insert-sort.
12.2.4 Ejemplo II
Esta es otra red para ordenar cuatro elementos en la cual todos los conectores tienen profundidad 3.
211
Teorema: Si una red de ordenamiento ordena una secuencia hs1 s2 : : :sni entonces dada una funcion monotona y creciente f la red tambien ordena
hf (s1) f (s2) : : : f (sn)i
. Esto puede demostrarse en forma bastante simple aplicando induccion aqui no lo hacemos para economizar tiempo y espacio.
Teorema del 0-1: Si una red de ordenamiento ordena todas las secuencias
posibles de n elementos donde cada elemento de la secuencia es 0 o 1, entonces la red ordena cualquier conjunto de elementos.
aria de n elementos pero que sin embargo no es capaz de ordenar la secuencia ha1 a2 : : : ani. Entonces podemos decir que para un cierto i y un cierto j con i < j la red ubica a aj antes que a ai en el resultado nal. De namos la siguiente funcion: si x ai f (x) = f 0 1 si x > ai Como la red ubica a aj antes que a ai al recibir ha1 a2 : : : ani entonces por el teorema anterior como f (x) es monotona y creciente ubica a f (aj ) antes que a f (ai). Como f (aj ) = 1 y f (ai) = 0 resulta que la red ubica un 1 antes que un 0 lo cual es una contradiccion porque supusimos que la red ordenaba cualquier secuencia binaria. Lo cual demuestra el teorema. Este teorema es util pues permite construir una red de ordenamiento basandose en secuencias binarias conociendo que si se ordenan todas las posibles secuencias binarias se puede ordenar cualquier secuencia generica.
1. Puede generarse concatenando dos secuencias s1 y s2 siendo s1 monotona y creciente y s2 monotona y decreciente. 2. Puede generarse concatenando dos secuencias s1 y s2 siendo s1 monotona y decreciente y s2 monotona y creciente. 3. La secuencia esta formada por n elementos iguales, en cuyo caso se la denomina secuencia bitonica pura.
Ejemplos
h1 5 15 23 7 4 2i h10 11 12 13 14 2i h1 1 1 1 1 1 1 1i
Secuencias bitonicas binarias Las secuencias bitonicas binarias pueden responder a las siguientes formas:
h1 1 1 1 1 1 1i h1 1 0 0 0 1 1 1i h0 0 0 1 1 1 0 0i
12.4.2 Bitonic-Sorter
El primer paso para construir la red de ordenamiento es construir una red que permita ordenar una secuencia bitonica, para ello utilizamos un dispositivo que se denomina Bitonic-Sorter. Un Bitonic-Sorter de 2 elementos es un comparador simple, para mas de dos elementos se recurre a la ayuda de un dispositivo denominado Half-Cleaner.
Half-Cleaner
213
Un Half-Cleaner recibe una secuencia bitonica de longitud N y genera dos secuencias bitonicas de longitud n=2, en donde una es una secuencia bitonica y la otra es una secuencia bitonica pura (de aqui el nombre de Half-Cleaner). Los Half-Cleaner tienen profundidad 1. Un Bitonic-Sorter se construye en forma recursiva utilizando un Half-Cleaner y luego aplicando Bitonic-Sort a las dos secuencias generadas por el Half-Cleaner.
La gura muestra un Bitonic-Sorter para ocho elementos, esta compuesto por un Half-Cleaner de 8 elementos y luego 2 Bitonic-Sorters de 4 elementos (uno arriba y otro abajo), cada uno de estos tiene a su vez un Half-Cleaner de 4 elementos y Bitonic-Sorters de 2 elementos los cuales son comparadores simples.
Analisis
La profundidad de un Bitonic-Sorter esta dado por la clasica recurrencia T (n) = 2T (N=2) + 1 (porque un Half-Cleaner tiene profundidad 1) y por lo tanto es O(log n).
12.4.3 Merger
Otro dispositivo necesario para construir la red de ordenamiento es la construccion de un Merger, observemos que dadas dos secuencias ordenadas si a la primera le concatenamos la reversa de la segunda obtenemos una secuencia bitonica, por ejemplo: s1 = h1 5 14 34 76i s2 = h2 3 8 11 20i s2 1 = h20 11 8 3 2i s1:s2 1 = h1 5 14 34 76 20 11 8 3 2i
; ;
214
Para proceder al merge utilizamos un dispositivo que dadas dos secuencias ya ordenadas construye dos secuencias bitonicas. El dispositivo tiene como propiedad que la primera secuencia tiene siempre elementos menores a la segunda y es:
Como puede verse este dispositivo tiene profundidad 1. Por lo anto si tomamos dos secuencias ordenadas y les aplicamos este dispositivo y a los resultados les aplicamos Bitonic-Sorter lo que obtenemos es una sola secuencia ordenada, es decir el merge de ambas.
Analisis
El analisis del Merger es identico al del Bitonic-Sorter por lo que su profundidad es O(log n) 215
Una vez construido un merger es sencillo construir un sort utilizando una suerte de merge sort invertido, empezamos por hacer un N=2 merges de 2 elementos cada uno, luego hacemos N=4 merges de 4 elementos cada uno y asi sucesivamente hasta obtener la secuencia ordenada, el dispositivo es de la forma:
Analisis
La profundidad de la red de ordenamiento esta dada por la recurrencia. T (n) = T (n=2) + log n Y su solucion es O(log2 n), siendo esta una funcion de orden sub-lineal. 216
12.6 Ejercicios
1. Indicar cuantos comparadores son necesarios para una Sorting-Network de n elementos 2. Probar que la profundiad de una red de ordenamiento es exactamente (log n)(log n + 1)=2
217
Chapter 13
Aproximacion y Heuristicas
13.1 Introduccion
Como se vio en el capitulo sobre complejidad existen numerosos problemas de optimizacion que no pueden resolverse en tiempo polinomico. Como varios de estos son problemas realmente importantes no es posible ignorarlos debido a que ciertas aplicaciones necesitan que se resuelvan estos problemas. Las opciones para enfrentar este tipo de problemas son: Usar un algoritmo de orden exponencial: Incluso en las computadoras paralelas mas avanzadas esta opcion solo es aplicable para instancias sumamente reducidas de estos problemas, hay pocas esperanzas de que el avance de la tecnologia permita resolver problemas para mas de 100 elementos mediante un algoritmo exponencial. Heuristicas: Frecuentemente no es necesario encontrar una respuesta optima a un problema sino que alcanza con descartar las soluciones que son realmente muy malas. En estos casos existen algoritmos heuristicos basados en estrategias muy faciles de computar pero que no necesariamente llevan a una solucion optima. Una regla heuristica es una regla que establece como tomar una decision a partir de un conocimiento intuitivo del problema a resolver. Inteligencia Arti cial: Hay varias tecnicas poderosas para resolver problemas de optimizacion combinatorios desarrollados en el campo de la inteligencia arti cial. Algunas de las tecnicas mas populares involucran algoritmos geneeticos, redes neuronales, busquedas de tipo A y variaciones dela programacion lineal. El rendimiento de estos metodos depende sensiblemente del problema al cual se apliquen. A menudo estos metodos obtienen soluciones muy buenas. 218
Algoritmos de aproximacion Son algoritmos de orden estrictamente polinomico que calculan una solucion con un cierto margen de tolerancia con respecto de la solucion optima, son muy necesarios cuando es necesario garantizar la calidad de la solucion. Pueden ser vistos como heuristicas que tienen una cierta performance calculable.
aproximation ratio
219
Los problemas de optimizacion presentan comportamientos diversos al intentarse una aproximacion, algunos problemas pueden ser aproximados al optimo tanto como se desee en tiempo polinomico (obviamente cuando la solucion tiende al optimo el tiempo tiende a in nito.) Estos algoritmos se denominan esquema de aproximacion polinomica y presentan un tiempo de ejecucion del estilo O(2(1=e)n2 ) de forma tal que cuando e, el error que se comete es mas chico el tiempo se vuelve mayor. Si el tiempo solo depende de una funcion polinomica de 1=e el algoritmo se clasi ca como esquema de aproximacion polinomica completo O((1=e)2 n3 ) seria un ejemplo de esta clase. Otros problemas no pueden aproximarse mas alla de un cierto limite, por ejemplo hay problemas para los cuales no hay algoritmo que supere (n) = 3=2 independientemente del tiempo que se disponga. En otros casos ni siquiera podemos garantizar un factor constante y en los casos mas extremos se llega a demostrar que encontrar un algoritmo de aproximacion con tasa de aproximacion constante es en si mismo un problema NP-Completo.
Existe una heuristica muy sencilla para Vertex-Cover que garantiza (n) = 2. Consideremos una arista cualquiera del grafo (u v), uno de sus dos vertices debe pertenecer al conjunto pero no sabemos cual de los dos. La idea de la heuristica es simplemente poner ambos vertices en el conjunto2 . Luego quitamos todas las aristas que sean incidentes a u y v (ya que ya estan cubiertas). Para cada vertice que debe estar en el conjunto ponemos dos por lo que es sencillo ver que esta estrategia genera a los sumo un conjunto del doble de tamanio del conjunto minimo (optimo).
2
220
vacio
Aparentemente existe una forma simple de mejorar la heuristica del 2x1. El algoritmo 2x1 selecciona cualquier arista y agrega ambos vertices al conjunto, en lugar de esto podemos elegir vertices de grado alto ya que un vertice de grado alto cubre mayor cantidad de aristas. Esta es la heuristica golosa.
221
Algorithm 65 Aprox-Vertex-Cover2(G=(V,E)) Calcula el VC usando la heuristica golosa C vacio while E no es vacio do u vertice de grado maximo add u to C remove from E incident edges to u
end while
return C
Para implementar este algoritmo en forma e ciente puede que sea necesario utilizar un Heap en donde se almacenen los vertices ordenados de acuerdo a su grado. Es interesante notar que en nuestro ejemplo (ver ilustracion) la heuristica golosa calcula la solucion optima real, esto no ocurre en todos los casos. La pregunta es si es posible demostrar que la heuristica golosa siempre es superior a la heuristica del 2x1, la respuesta es un no de nitivo, de hecho la heuristica golosa no tiene una tasa de aproximacion constante. Puede resultar arbitrariamente ine ciente o puede llegar a la solucion optima. Aunque no puede demostrarse es aceptable decir que la heuristica golosa es superior a la del 2x1 para grafos aceptados como normales.
222
tamanio n ; 2k, calculemos a partir de esto la tasa de aproximacion. n;k (n) = n ; 2k El problema de esta tasa de aproximacion es que podria resultar arbitrariamente grande. Por ejemplo si n = 1001 y k = 500 (n) = 500 lo cual es terrible. Como vemos la teoria de las reducciones no es aplicable a las aproximaciones.
(E V ) con pesos no-negativos en las aristas y dado un entero k calcular un subconjunto de k vertices C V denominados centros de forma tal que la distancia maxima entre cualquier vertice de V y su centro mas cercano es minima. Sea G = (V E ) el grafo y sea w(u v) el peso de la arista (u v) w(u v) = w(v u) ya que G es no-dirigido. Suponemos que todos los pesos son no-negativos. Para cada par de vertices u v 2 V sea d(u v) = d(v u) la distancia de u hasta v es decir el costo del camino minimo desde u hasta v. Consideremos el subconjunto C V de vertices: centros. Para cada vertice v 2 V podemos asociarlo con su vertice mas cercano denotado C (v). Podemos tambien, asociar cada centro con el conjunto de vertices denominado su vecindario para el cual el centro es su centro mas cercano. Para simpli car las de niciones supongamos que la distancia al centro mas cercano es unica. Para cada v 2 V y ci 2 Ci de nimos. C (v) = ci tal que d(v ci) d(v cj ) para i 6= j V (ci) = fv tal que C (v) = cig Si rompemos los empates para los vertices que son equidistantes desde dos o mas centros podemos pensar en V (c1) V (c2) : : : formando una particion de los vertices de G. La distancia asociada con cada centro es la distancia al vertice mas lejano de V(ci): D(ci) = maxv V (ci) d(v ci)
2
223
Esta es la distancia maxima oara cualquier vertice asociado con ci con respecto al centro. Finalmente de nimos. D(C ) = maxci C D(ci) Que es la distancia maxima desde cualquier vertice hasta su centro mas cercano, esta es la distancia critica para la solucion y por eso se la denomina cuello de botella, todo vertice que este a esta distancia de su centro mas cercano en la solucion se denominara vertice cuello de botella.
2
Dada la nomenclatura podemos de nir el problema del centro equidistante de ls siguiente manera: dado un grafo no dirigido G = (V E ) y un entero k jV j encontrar un subconjunto C 2 V de tamanio k de forma tal que D(C ) sea minimo. Una solucion por fuerza bruta implicaria enumerar todos los subconjuntos de k elementos de V y calcular D(C ) para cada uno, esto es O(nk ). Dado que el problema es NP-Completo es altamente improbable que un algoritmo signi cativamente mas e ciente exista.
Nuestro algoritmo de aproximacion se basa en un algoritmo goloso simple que produce una distancia cuello de botella D(C) que no es mas del doble de la distancia optima. Comenzamos permitiendo que el primer vertice c1 sea cualquier vertice del grafo. Calculamos las distancias entre este vertice y los demas vetices del grafo, tomamos el vertice mas lejano de este vrtice al cual consideramos el vertice cuello de botella de c1. A este vetice lo elegimos como proximo centro c2. A continuacion computamos las distancias de todos los vetices del grafo al centro mas cercano (c1 o c2). Nuevamente tomamos el vertice cuello de botella como el proximo centro y continuamos. El proceso se repite hasta seleccionar k centros.
224
Algorithm 66 KCenter-Aprox(G,k) usando la heuristica golosa for each u in v do d u] 1 end for for i = 1 to k do c i] u tal que d u] es maximo (cuello de botella) for each v in V do d v] =distancia minima desde v hasta cualquier centro c1 c2 : : : ect end for end for
return c 1..k] Mediante el algoritmo de Dijkstra podemos calcular los caminos minimos desde un vertice hasta todos los demas. Sin embargo cada paso de este algoritmo pide solucionar multiples problemas de este tipo, esto puede hacerse con una modi cacion del algoritmo original de Dijkstra. El tiempo del algoritmo es k veces el tiempo del algoritmo de Dijkstra : O(k(n + e) log n)
Queremos mostrar que este algoritmo siempre produce una distancia nal D(C ) que es a lo sumo el doble de la distancia de la solucion optima es decir que (n) = 2. Sea C = fc 1 c 2 : : : c kglos centros de la solucion optima y sea D = D(C ) la distancia cuello de botella optima. Sea CG = fc1 c2 : : : ckg los centros del algoritmo goloso. Ademas sea ck+1 el proximo centro que seria agregado, es decir el vertice cuello de botella para CG . Sea DG = D(CG ) la distancia cuello de botella de CG notemos que la distancia desde ck+1 a su centro mas cercano es DG
Cada ci 2 C esta asociado con su centro mas cercano en la solucion optima, es decir que corresponde a V (c k) para un cierto k. Debido a que hay k centros en C y k + 1 elementos en C se deduce que por lo menos dos centros de C estan en el mismo conjunto V (c k) para algun k. Sean estos centros ci y cj .
0 0
Dado que D es la distancia cuello de botella para C sabeos que hay un camino de distancia D desde ci hasta c k y un camino de distancia D desde c k hasta cj implicando que existe un camino de longitud 2d desde ci hasta cj . Por lo tanto d(ci cj ) 2D . Pero de los comentarios anteriores tenemos que d(ci cj ) DG . Por lo tanto: DG d(ci cj ) 2D Que es lo que se queria demostrar.
algoritmo que permite solucionar el problema del viajante con (n) = 2. El algoritmo es el siguiente: 1. Convertir el problema del viajante a un grafo completo y pesado G = (V E ) no dirigido, asignar distancia 1 a los caminos que no existen. 2. Calcular el arbol generador minimo (AGM) para G, usando por ejemplo el algoritmo de Kruskal o el de Prim. 3. Recorrer el AGM por ambos lados para obtener un ciclo (ver gura) 4. Ajustar el camino de forma tal de no visitar cada ciudad mas de una vez. 226
Analisis En primer lugar el paso 2 garantiza que el costo del arbol por se el
arbol generador minimo es menor o igual que el costo de la solucion optima del viajante. (En caso contrario quitando una arista cualquiera a la solucion del problema del viajante tendriamos un AGM menor). Luego al recorrerlo dos veces en el paso 3 garantizamos que la longitud de nuestro tour es menor o igual al doble de la longitud de la solucion optima ya que recorrermos el AGM dos veces. Por ultimo el paso 4 no puede empeorar la solucion ya que de acuerdo a la desigualdad triangular si reemplazamos (u w) (w v) por (u v) la distancia no puede aumentar. Por lo tanto (n) = 2 que es la tasa de aproximacion de este algoritmo. Si el grafo no es euclideano no existe algoritmo de aproximacion que permita resolver el problema del viajante con una cierta tasa de aproximacion menor a 1. Esto puede demostarse ya que si dicha aproximacion existiese el problema del ciclo Hamiltoniano no seria NP-Completo!. Para el problema Euclideano existen otros algoritmos de aproximacion, la Heuristica de Christo des obtiene (n) = 1:5 pero insume mucho mas tiempo. Si los pesos de las aristas son las distancias Euclideanas entre puntos en el plano existe una aproximacion (1 + ) (fue descubierta recientemente).
Calcular cual es el arbol de decision mas chico para un conjunto de datos dado. Este problema es analogo al de los colores, no se puede aproximar en forma acotada. Encontrar el clique de tamanio maximo dado un grafo G. Tampoco puede aproximarse.
13.7.4 Set-Cover
Dado un conjunto X = fx1 x2 : :: xmg y F = fS 1 S 2 : : : Sng una familia de subconjuntos de X de forma tal que cada elemento de X pertenece al menos a un 227
conjunto de F . Encontrar un conjunto C F de forma tal que cada elemento de X este en algun elemento de C es decir: X=
Si2C
Si
Ejemplo
X = f1 2 3 4 5 6 7 8 9 10 11 12g S 1 = f1 2 3 4 5 6g S 2 = f5 6 8 9 g S 3 = f1 4 7 10g S 4 = f2 5 8 11g S 5 = f3 6 9 12g C = fS 3 S 4 S 5g Este problema es sumamente amplio y cubre por ejemplo al problema de VertexCover (VC), entre otros. Las mejores aproximaciones a este problema son de tipo (n) = ln n. Una aproximacion golosa aplicable es elegir cada vez el conjunto Si que cubra mayor cantidad de elementos de X , esto permite (n) = n lo cual no es muy optimo.
228
229
Chapter 14
Indices
230
231
Algoritmo Pagina Kruskal 149 LCS (Subsecuencia comun maxima 136 Link 171 Make-Set 170 Matrix-Chain-Multiply 126 Matrix-Chain-Order 126 Matrix-Multipli 122 Merge-Sort 48 Merge 48 Multipop 155 Ordenamiento topologico 114 Particion 67 Primalidad aleatorizado 77 Primalidad por fuerza bruta 76 Quicksort 66 Radix-Sort 92 Relaxation 144 Seleccion 52 ShortestPath 131 Sort-CX 188 Union (Union-Find) 171
232
Tema Pagina 3-SAT 197 AGM 146 ALgoritmos golosos 140 Ackermann funcion 171 Agregacion metodo 155 Algoritmo de Dijkstra 143 Algoritmo de Floyd-Pratt-Rivest-Tarjan 54 Algoritmo de Floyd-Warshall 133 Algoritmo de Kruskal 148 52 Algoritmo de seleccion Algoritmo de nicion 8 Algoritmo deterministico 8 Algoritmo factorial 33 Algoritmo particion 67 Algoritmo validacion 8 Algoritmos aleatorizados 65 Algoritmos de aproximacion y heuristicas 218 Algoritmos de sort 81 Algoritmos disenio 9 Algoritmos iterativos 20 Algoritmos recursivos 33 Algoritmos tipo Montecarlo 65 Analisis amortizado agregacion 155 Analisis amortizado metodo contable 157 Analisis amortizado metodo de los potenciales 158 Analisis amortizado 154 Aprox-VC2 222 Aproximacion a Vertex Cover 220 Aproximacion al centro equidistante 224 Aproximacion al problema del viajante 226 Aproximaciones y reducciones 222
233
Tema Pagina Arbol generador minimo 146 Arboles AVL 154 Arboles binarios 154 Arboles binomiales implementacion 161 Arboles binomiales 160 Arboles de decision 227 Arboles 98 Aristas de avance 112 Aristas de cruce 112 Aristas de retroceso 112 BFS analisis 109 BFS caminos minimos 107 BFS 105 Bitonic-Sorter 213 Bosques 98 Buildheap 85 23 Burbujeo analisis Burbujeo 20 CFC algoritmo 117 CFCs identi cacion 115 Cambio de variables 36 Caminos minimos Dijkstra 143 Caminos minimos Floyd-Warshall 133 Caminos minimos por BFS 107 Caminos minimos uno con todos 143 Caminos y ciclos 97 Caras 102 Carmiqueleanos numeros 78 Centro equidistante aproximacion 224 Centro equidistante 223 Ciclo euleriano 97
234
Tema Pagina Ciclo hamiltoniano 97 Ciclos anidados independientes 22 Ciclos simples 21 Clase NP de problemas 190 Clase No-Np de problemas 192 Clase P de problemas 190 Clasi cacion de aristas por DFS 111 Clique problema 202 Cliques 101 Clique 227 Colas dobles 153 Colas 153 Complejidad introduccion 185 Complejidad 190 Componentes conexos 98 Componentes fuertemente conexos 99 101 Conjunto independiente Contable metodo 157 Convex-Hull 186 Costos en la maquina de Turing 12 Counting sort algoritmo 89 Counting sort 89 Create-Heap 163 Crecimiento de funciones 29 Cribas 51 DFS analisis 111 DFS aristas de avance 112 DFS aristas de cruce 112 DFS aristas de retroceso 112 DFS clasi cacion de aristas 111 DFS deteccion de ciclos 113
235
Tema Pagina DFS 109 Decrease-Key Heaps binomiales 165 Deteccion de ciclos 113 Dijkstra algoritmo 143 Direccionamiento abierto con doble hashing 180 Direccionamiento abierto cuadratico 179 Direccionamiento abierto lineal 179 Direccionamiento abierto 178 Disenio de algoritmos 9 Disenio paradigmas 10 Distancia minima algoritmo aleatorizado 72 Distancia minima solucion DC 58 Distancia minima v.iterativa 29 Dividir para conquistar 47 Doble hashing 180 Elevando un numero a una potencia 16 81 Estabilidad sort Estructuras de datos simples 152 Extract-Min Heaps binomiales 165 Fermat little theorem 77 Fibonacci Heaps 165 Fibonacci algoritmo iterativo 42 Fibonacci algoritmo recursivo 40 Fibonacci numeros 40 Fibonacci usando potencia de matrices 43 Floyd-Pratt-Rivest-Tarjan seleccion 54 Floyd-Warshall algoritmo 133 Formas planares 102 Formula de Euler 103 Funcion de Ackermann 171 Funciones crecimiento 29 Funciones de hashing 182
236
Tema Pagina Gauss metodo para multiplicar 58 Golsoso algoritmos 140 Grado de un vertice 96 Grafo aciclico 98 Grafo bipartito 101 Grafo complemento 101 Grafo completo 101 Grafo conexo 98 Grafo dirigido aciclico 99 Grafo inducido 100 Grafo transpuesto 101 Grafos BFS analisis 109 Grafos BFS 105 Grafos DFS analisis 111 Grafos DFS clasisi cacion de aristas 111 Grafos DFS 109 112 Grafos TimeStamps Grafos arboles 98 Grafos bosques 98 Grafos caminos minimos por BFS 107 Grafos caminos y ciclos 97 Grafos caras 102 Grafos ciclo euleriano 97 Grafos ciclo hamiltoniano 97 Grafos cliques 101 Grafos componentes conexos 98 Grafos componentes fuertemente conexos 99 Grafos conjunto independiente 101 Grafos deteccion de ciclos 113 Grafos dirigidos 95 Grafos dispersos 103
237
Tema Pagina Grafos formas planares 102 Grafos formula de Euler 103 Grafos fuertemente conexos 99 Grafos grado de un vertice 96 Grafos identi cacion de CFCs 115 Grafos isomorfos 99 Grafos lista de adyacencias 104 Grafos matriz de adyacencias 103 Grafos ordenamiento topologico 114 Grafos planares 102 Grafos representacion 103 Grafos subgrafos 100 Grafos tamanios 103 Grafos teorema de los intervalos 112 Grafos vertices adyacentes 96 Grafos 95 213 Half-Cleaner Hash-Tables encadenamiento 176 Hash-Tables resolucion de colisiones 176 Hash-Tables 175 Hashing por division 182 Hashing por multiplicacion 182 Hashing 182 Heapify 84 Heaps almacenamiento 83 Heaps binomiales Create-Heap 163 Heaps binomiales Decrease-Key 165 Heaps binomiales Extract-Min 165 Heaps binomiales Insert 165 Heaps binomiales Union 163 Heaps binomiales 159 Heaps de Fibonacci 165 Heaps resumen comparativo 165
238
Tema Pagina Heapsort Buildheap 85 Heapsort algoritmo 85 Heapsort heapify 84 Heapsort 82 Heaps 82 Heuristica 2x1 para VC 220 Heuristica golosa para VC 221 218 Heuristicas Insert heaps binomiales 165 Insert sort 24 Insertsort2 82 Instancia de un problema 188 Isomor smo de grafos 99 K-Coloring 227 KCenter-Aprox 224 Konigsberg problema de los puentes 97 148 Kruskal algoritmo La clase de problemas NP-Completos 191 Lista de adyacencias 104 Listas 153 Lomuto algoritmo de particion 80 Maquina RAM de costo jo 13 Maquina RAM de costo variable 15 Maquina de Turing Costos 12 Maquina de Turing e ciencia 13 Maquina de Turing no deterministica 206 Maquina de Turing 10 Matriz de adyacencias 103 143 Mecanismo de relajacion
239
Tema Mediana problema Merge-Sort analisis Merge-Sort optimizaciones Merge-Sort propiedades Merge-Sort Merger Mergesort2 Metodo contable Metodo de agregacion Metodo de division Metodo de la multiplicacion Metodo de los multiplicadores Metodo de los potenciales Metodo de sustitucion Metodo iterativo Modelos computacionales Multiplicacion de numeros Multiplicacion por Gauss Notacion O Numeros carmiqueleanos Numeros de Fibonacci Ordenamiento topologico algoritmo Ordenamiento topologico Paradigmas de disenio Particion algoritmo de Lomuto Particion analisis Particion entera Path-Compression Pilas Potencia algoritmo recursivo Potencia algoritmo simple
Pagina 51 48 50 48 47 214 82 157 155 182 182 37 158 34 35 10 56 58 27 78 40 114 114 10 80 68 203 170 152 17 16
240
Tema Pagina Potencia de un numero 16 Potenciales metodo 158 Primalidad algoritmo aleatorizado 77 Primalidad algoritmo fuerza bruta 76 Primalidad testeo 76 Principio de uniformidad simple 176 Principio del 0-1 212 Problema 3-SAT 197 Problema K-Coloring 227 Problema SAT 196 Problema Vertex-Cover 199 Problema de la esta carismatica 139 Problema de la maquina expendedora 151 Problema de la particion entera 203 Problema de la seleccion de actividades 141 Problema de la seleccion 51 134 Problema de la subsecuencia comun maxima Problema de los arboles de decision 227 Problema de los pozos petroleros 63 Problema de los puentes de Konigsberg 97 Problema de todas las distancias minimas 128 Problema de nicion 188 Problema del arbol generador minimo 146 Problema del centro equidistante 223 Problema del clique 202 Problema del conjunto independiente 202 Problema del viajante aproximacion 226 Problema del viajante 189 Problema euclideano del viajante 226 Problema mediana 51
241
Tema Pagina Problemas NP-Completos teoria 205 Problemas NP-Completos 191 Problemas NP 190 Problemas P 190 Problemas de decision 188 Problemas no-NP 192 Profundidad de sorting networks 210 Programacion dinamica 120 Programacion lineal entera 198 Quicksort analisis 66 Quicksort2 82 Quicksort 65 Radix sort 91 Radix-Sort algoritmo 92 Recurrencias cambio de variables 36 Recurrencias metodo de los multiplicadores 37 34 Recurrencias metodo de sustitucion Recurrencias metodo iterativo 35 Recurrencias teorema maestro 36 Reduccion 3COL-Clique 194 Reduccion 3SAT-VC 199 Reduccion HC-HP 193 Reduccion IS-Clique 202 Reduccion SAT-3SAT 197 Reduccion SAT-PLE 198 Reduccion VC-IP 203 Reduccion VC-IS 202 Reducciones complejidad 186 Reducciones notacion 196 Reducciones pasos a seguir 196 Reducciones tecnicas 204 Reducciones 193
242
Tema Pagina Relajacion 143 Representacion de grafos 103 Resolucion de colisiones por encadenamiento 176 Resolucion de colisiones 176 Resolucion de colsiones por dir abierto 178 SAT 196 Secuencia de prueba 178 Secuencias bitonicas binarias 213 Secuencias bitonicas 212 Seleccion Floyd-Pratt-Rivest-Tarjan 54 Seleccion de actividades: goloso 142 Seleccion de actividades 141 Seleccion problema 51 Seleccion sort 81 Sort Mergesort 47 Sort Quicksort 65 93 Sort Stooge-sort Sort algoritmos lineales 89 Sort algoritmos no recursivos 81 Sort algoritmos recursivos 82 Sort algoritmos 81 Sort burbujeo analisis 23 Sort burbujeo2 81 Sort counting sort 89 Sort en paralelo 208 Sort estabilidad 81 Sort heapsort 82 Sort in-situ 81 Sort insercion2 82 Sort insercion 24 Sort limite inferior 87
243
Tema Sort mergesort2 Sort por burbujeo Sort quicksort2 Sort radix-sort Sort seleccion Sort tabla comparativa Sorting Networks Sorting Network Sorting networks analisis Sorting networks profundidad Stooge-sort Subgrafos Subsecuencia comun maxima T(n)=1/n Sum q=1] n] T(q-1)+T(n-q)+n por sustitucion T(n)=2T(n/2)+n log n por iteracion T(n)=2T(n/2)+n por iteracion T(n)=2T(n/2)+n por sustitucion T(n)=2T(n/2)+n por teorema maestro T(n)=3T(n/4)+n por metodo iterativo T(n)=3T(n/4)+n por teorema maestro T(n)=8T(n-1)-15T(n-2) por multiplicadores T(n)=T(n-1)+T(n-2) por arbol T(n)=T(q-1)+T(n-q)+n por iteracion T(n)=T(sqrt(n))+1 por cambio de variables Tasa de aproximacion Tasa de error Teorema de Cook Teorema de los intervalos Teorema del 0-1 Teorema maestro
Pagina 82 20 82 91 81 93 208 216 210 210 93 100 134 69 39 35 34 36 38 38 37 41 69 37 219 219 206 112 212 36
244
Tema Teorema pequenio de Fermat Teoria formal de los problemas NP-Completos Testeo de primalidad TimeStamps Turing maquina de Union (Heaps binomiales) Union (Heaps-Binomiales) Union-Find analisis Union-Find path compression Union-Find usando forests Union-Find usando listas Union-Find Validacion de un algoritmo Vertex Cover aproximacion Vertex-Cover Vertices adyacentes burbujeo2
Pagina 77 205 76 112 10 163 163 171 170 169 167 165 8 220 199 96 81
245