Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 1. 1.1. 1.2. 1.3. 1.4. 1.5. INTRODUCCIÓN 4 PRESENTACIÓN ANÁLISIS DE ANTECEDENTES OBJETIVOS APLICACIÓN PLANIFICACIÓN DEL TRABAJO 4 5 6 6 7 2. ANÁLISIS DE REQUERIMIENTOS 9 2.1. REQUERIMIENTOS 2.1.1. QUÉ OFRECE LA LIBRERÍA DE CAMINOS MÍNIMOS. 2.1.2. NECESIDAD DE UNA LIBRERÍA DE ESTE TIPO. 2.1.3. USO DE LA LIBRERÍA. 2.1.4. USUARIOS 9 9 10 12 12 3. EL PROBLEMA DEL CAMINO MÍNIMO 13 3.1. DEFINICIONES 3.2. MÉTODO GENERAL DEL PROBLEMA DEL CAMINO MÍNIMO (UN ORIGEN/MUCHOS DESTINOS) 3.2.1. ALGORITMO GENÉRICO DEL CAMINO MÍNIMO 3.3. IMPLEMENTACIONES DEL ALGORITMO GENÉRICO 3.3.1. MÉTODOS ETIQUETADORES (LABEL SETTING METHODS). 3.3.2. MÉTODOS CORRECTORES DE ETIQUETA (LABEL CORRECTING METHODS). 3.4. MÉTODOS ETIQUETADORES (DIJKSTRA) 3.4.1. TIEMPO DE EJECUCIÓN DEL ALGORITMO DE DIJKSTRA 3.4.2. EJEMPLO GRÁFICO DE EJECUCIÓN DEL ALGORITMO 3.5. OTRAS VERSIONES DEL ALGORITMO 3.5.1. ALGORITMO DE DIJKSTRA HACIA ATRÁS 3.5.2. ALGORITMO DE DIJKSTRA BIDIRECCIONAL 3.6. IMPLEMENTACIONES DEL ALGORITMO DE DIJKSTRA 3.6.1. IMPLEMENTACIÓN DE DIAL 3.6.2. IMPLEMENTACIONES CON COLAS DE PRIORIDAD 3.6.3. IMPLEMENTACIÓN CON HEAP BINARIO 3.6.4. IMPLEMENTACIÓN CON D-HEAP 3.6.5. IMPLEMENTACIÓN CON HEAP DE FIBONACCI 3.6.6. IMPLEMENTACIÓN CON RADIX HEAP 3.7. MÉTODOS CORRECTORES DE ETIQUETA 3.7.1. IMPLEMENTACIÓN CON “DEQUEUES” 3.8. RESUMEN Y RECAPITULACIÓN DE LOS ALGORITMOS 3.8.1. GRAFOS COMPLETOS. AUMENTANDO EL NÚMERO DE NODOS. 3.8.2. GRAFO COMPLETO PEQUEÑO. AUMENTANDO COSTE MÁXIMO DE ARISTA. 3.8.3. GRAFO COMPLETO GRANDE. AUMENTANDO COSTE MÁXIMO DE ARISTA. 3.8.4. GRAFO PEQUEÑO. DISMINUYENDO DENSIDAD DE ARISTAS. 3.8.5. GRAFO GRANDE. DISMINUYENDO DENSIDAD DE ARISTAS. 3.8.6. GRAFO DISPERSO PEQUEÑO. AUMENTANDO COSTE MÁXIMO DE ARISTA. 3.8.7. GRAFO DISPERSO GRANDE. AUMENTANDO COSTE MÁXIMO DE ARISTA. 3.8.8. RESUMEN 13 15 16 17 17 17 18 19 20 21 21 21 22 22 24 25 25 25 25 27 27 28 29 29 30 30 30 31 31 32 4. IMPLEMENTACIÓN 33 1 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 4.1. REPRESENTACIONES DE UN GRAFO 4.1.1. MATRIZ DE INCIDENCIAS NODO-ARISTA 4.1.2. MATRIZ DE ADYACENCIAS NODO-NODO 4.1.3. LISTAS DE ADYACENCIA 4.1.4. REPRESENTACIONES EN ESTRELLA 4.2. DISEÑO DE LA REPRESENTACIÓN DEL GRAFO 4.2.1. ELEMENTOS DEL GRAFO EJEMPLO: 4.3. IMPLEMENTACIÓN DE LAS ESTRUCTURAS DE DATOS. 35 35 36 37 38 39 39 40 42 5. ESPECIFICACIÓN DEL SOFTWARE. 43 5.1. MODELO CONCEPTUAL. 5.1.1. INTRODUCCIÓN. 5.1.2 APARICIÓN DE NUEVOS ALGORITMOS 5.1.3. HERRAMIENTA DE ESPECIFICACIÓN Y DISEÑO 5.1.4. DIAGRAMA DE CLASES 5.1.5. CLASES DEL DOMINIO 5.1.6. CASOS DE USO 5.1.7. DIAGRAMA DE SECUENCIA 5.1.4. PSEUDO CÓDIGO DE LAS OPERACIONES GRAFO CAMMIN DIJKSTRA 5.2. HERRAMIENTAS Y BASE TEÓRICA 5.2.1 PROGRAMACIÓN ORIENTADA A OBJETOS 5.2.2. C++ 43 43 45 45 46 48 49 49 51 51 53 54 57 57 58 6. MANUAL DE USO 59 6.1 INTRODUCCIÓN 6.2. PROCEDIMIENTO PARA EJECUTAR UN ALGORITMO DE LA LIBRERÍA 6. 2. 1. UNIDIRECCIONAL: 6. 2. 2. BIDIRECCIONAL: 6. 3. CREACIÓN DEL GRAFO 6.4. CLASES Y MÉTODOS PÚBLICOS 6.4.1. GRAFO 6.4.2. CAMMIN 59 60 62 62 63 64 64 66 7. RESULTADOS 67 7.1. PREPARATIVOS 7.2. RESULTADOS: 7.3 ANÁLISIS DE LOS RESULTADOS 7.3.1. GRAFOS COMPLETOS. CONCLUSIONES 7.3.2. GRAFO COMPLETO PEQUEÑO. AUMENTANDO COSTE MÁXIMO DE ARISTA. 7.3.3. GRAFO COMPLETO GRANDE. AUMENTANDO COSTE MÁXIMO DE ARISTA. 7.3.4. GRAFO PEQUEÑO. DISMINUYENDO DENSIDAD DE ARISTAS. 7.3.5. GRAFO GRANDE. DISMINUYENDO DENSIDAD DE ARISTAS. 7.3.6. GRAFO DISPERSO PEQUEÑO. AUMENTANDO COSTE MÁXIMO DE ARISTA. 7.3.7. GRAFO DISPERSO GRANDE. AUMENTANDO COSTE MÁXIMO DE ARISTA. 67 69 72 72 73 73 74 74 75 76 77 2 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.4. PRIMERAS CONCLUSIONES 78 7.5. TABLA CONSEJO SOBRE UTILIZACIÓN DE LOS ALGORITMOS SEGÚN PARÁMETROS DE GRAFO 79 DE ENTRADA. 7. APLICACIÓN. 82 7.1. INTRODUCCIÓN 7.2 DESCRIPCIÓN DEL PROBLEMA. 7.3 APLICACIÓN REAL 7.4 ANÁLISIS DE LA APLICACIÓN 7.5. CÓMO SUSTITUIR EL CÓDIGO 7.6 SUSTITUCIÓN DEL CÓDIGO 7.6.1. CREACIÓN DEL GRAFO. 7.6.2. NODOS ORIGEN Y DESTINO DEL PROBLEMA 7.6.3. EJECUCIÓN DEL ALGORITMO 7.7. RESULTADOS 82 82 82 83 86 87 87 88 89 90 8. APÉNDICE 92 8.1. ESTRUCTURAS DE DATOS 8.2. D-HEAPS 8.2.1. DEFINICIÓN Y PROPIEDADES DE UN D-HEAP 8.2.2. ALMACENAMIENTO DE UN D-HEAP. 8.2.3. PROPIEDAD ORDEN DEL HEAP 8.2.4. INTERCAMBIO 8.2.5. RECUPERAR EL ORDEN DEL HEAP 8.2.6. OPERACIONES DEL HEAP 8.3. HEAPS DE FIBONACCI 8.3.1. PROPIEDADES 8.3.2. DEFINICIÓN Y ALMACENAMIENTO DE UN HEAP DE FIBONACCI 8.3.3. UNIENDO Y CORTANDO 8.3.4. INVARIANTES 8.3.5. RESTAURANDO EL INVARIANTE 2 8.3.6. RESTAURANDO EL INVARIANTE 3 8.3.7. OPERACIONES 92 92 92 93 94 94 94 95 96 96 96 97 98 98 99 100 9. BALANCES Y CONCLUSIONES 101 9.1. COMPARACIÓN TIEMPO ESTIMADO CON TIEMPO REAL 9.2. BALANCE ECONÓMICO 9.3. CONCLUSIONES 9.4. LÍNEAS ABIERTAS 101 102 103 104 10. BIBLIOGRAFÍA 105 10.1. LIBROS 10.2. ARTÍCULOS EN REVISTAS 10.3. DE INTERNET: 10.4. DE LA APLICACIÓN DE PROTECCIÓN DE DATOS 105 105 106 107 3 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 1. Introducción 1.1. Presentación Este proyecto tiene como objetivo desarrollar una librería cuyo contenido serán diferentes versiones de algoritmos que solucionan el problema de encontrar el camino mínimo entre nodos de una red o grafo de forma eficiente. Una vez implementada, se empleará en una aplicación real de protección de datos estadísticos en la que se ejecutan numerosos subproblemas que no son más que búsquedas de caminos mínimos en redes. Dicha aplicación dispone ya de un módulo básico que implementa una única versión del algoritmo básico de Dijkstra. Una vez creada la librería, se sustituirá ésta por el módulo existente, lo que permitirá evaluar diferentes variantes del algoritmo, minimizando así los tiempos de espera de cada subproblema y, como resultado, el de toda la aplicación. El proyecto se compone de varias partes: Básicamente, la librería consistirá en diferentes implementaciones del conocido algoritmo de Dijkstra más otros, llamados correctores de etiqueta, que pueden ser más idóneos y mejorar el tiempo de cálculo para ciertos tipos de redes o topologías de éstas. Además, cada implementación tendrá dos variantes: la original, que encuentra dado un nodo origen todos los caminos mínimos al resto de nodos de la red y una versión que encuentra el camino mínimo entre dos nodos dados origen y destino. Ésta última versión de buscar el camino mínimo entre dos nodos es la que verdaderamente necesita la aplicación real y es por ello de su existencia en el proyecto. Para el algoritmo de Dijkstra, las versiones difieren entre sí en el uso de distintas estructuras de datos internas durante su ejecución, que ayudan a reducir el tiempo de cálculo, aunque a priori, y para un cierto tipo de red, no es fácil intuir qué versión será la más beneficiosa. Es decir, cada tipo de implementación no es una mejora de otra, sino una forma diferente de intentar aumentar la eficiencia. 4 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 1.2. Análisis de antecedentes Por extraño que parezca en un principio, no existe ninguna biblioteca o aplicación que reúna las características deseadas en este proyecto. Normalmente, en las librerías existentes especializadas en problemas con grafos o redes, se puede encontrar como mucho uno o dos algoritmos para la resolución de los caminos mínimos. En concreto, el algoritmo básico de Dijkstra y una mejora eficiente usando d-heaps. Estos algoritmos son más que suficientes para aplicaciones que necesiten aplicarlos unas pocas veces o que la ejecución de los mismos no sea una parte esencial del total del programa. El problema surge cuando en la aplicación la mayor parte del tiempo se dedica a buscar caminos mínimos y, por tanto, es crucial la eficiencia de este tipo de algoritmos. Posiblemente, y con muchos esfuerzo (temporal o económico) se podrán encontrar otras versiones, pero con interficies distintas o incluso lenguajes de programación diferentes. De todas formas, después de una búsqueda intensiva no se han encontrado más de tres algoritmos implementados. El resto, únicamente aparece en literatura. Afortunadamente, sí que existe bastante documentación con propuestas de algoritmos, ya sea en forma de libros especializados en grafos y redes o en revistas científicas e informáticas. 5 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 1.3. - Objetivos Diseñar una biblioteca que contenga distintas versiones de algoritmos que resuelven el problema del camino mínimo en redes. - Sencillez de uso de los algoritmos. El usuario debería nada más que proporcionar los mínimos datos en un tipo de formato predeterminado y elegir la versión del algoritmo a ejecutar. - Capacidad de detectar anomalías y comunicarlas al agente externo que ejecute las funciones de la biblioteca. - Máxima eficiencia temporal y espacial, aunque si es necesario se sacrificará el segundo a favor del primero. - Implementar las estructuras de datos que emplearán las distintas versiones de los algoritmos, de forma lo más eficiente posible (d-heaps, fibonnaci-heaps, colas circulares...). - Una vez se tengan dichas estructuras, implementar las versiones de los algoritmos de caminos mínimos, también de manera lo más eficientemente posible. - Proporcionar una interficie sencilla y clara al usuario de los algoritmos que permita escoger la versión de implementación pero que sea única independientemente de la elección. 1.4. - Aplicación Estudiar la topología de las redes que aparezcan en la aplicación y observar empíricamente cuál de las versiones de los algoritmos de caminos mínimos ya implementadas es la ideal. - Sustituir las llamadas al módulo inicial de Dijkstra de la aplicación por la librería implementada. 6 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 1.5. Planificación del trabajo Previamente a la planificación seria del trabajo se trabajó en un prototipo del software sin dedicación temporal específica ni precisable, a la vez que se recogía información sobre la existencia de distintos algoritmos de caminos mínimos, se estudiaban las herramientas de trabajo, etc. Por eso, ya se disponía de un prototipo de software que, sin mucho refinamiento, cubría la mitad de los objetivos referentes a la librería. Se tenían implementadas la mayoría de estructuras de datos y alguno de los algoritmos. Quedaba, entonces, acabar de implementar las estructuras de datos restantes y las versiones de los algoritmos. Además, algunas de estas versiones no habían sido todavía estudiadas a fondo, aunque se disponía de la documentación. Por supuesto, todo lo referente a insertar la librería en el problema de protección de datos quedaba por ser desarrollada, junto con un estudio previo del código de la aplicación, para detectar los puntos donde se efectuarían las modificaciones necesarias. 1ª Quincena Julio 2003 Terminar las estructuras de datos y algoritmos (versiones de Dijkstra). 2ª Quincena Julio 2003 Estudio del resto de algoritmos (no de Dijkstra). Implementación de los mismos. 1ª Quincena Agosto 2003 Revisión de las implementaciones de los algoritmos. Juegos de prueba. 2ª Quincena Agosto 2003 Estudio del código de la aplicación. Elección de la versión idónea de algoritmo para los tipos de redes que aparecen en el problema (dependiendo del tiempo disponible, se podrá hacer un estudio empírico más global, para todos las versiones implementadas). Aplicación de la librería en el problema. Septiembre 2003 Revisiones de código. Redacción últimos capítulos de la memoria. Septiembre u Octubre 2003 Revisión final de la memoria. (fecha no escogida todavía) Preparación de la defensa del proyecto (diapositivas). Defensa del proyecto. 7 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Para el periodo previo a la matriculación del proyecto (Julio 2003) se estiman 5 horas semanales de dedicación media (desde Marzo 2003). Esto hace un total aproximado de 80 horas. Para el periodo posterior se estiman 20 horas de dedicación por semana. Esto hace un total aproximado de 320 horas. Total: 400 horas. 8 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 2. Análisis de requerimientos 2.1. Requerimientos 2.1.1. Qué ofrece la librería de caminos mínimos. La biblioteca ofrece distintas implementaciones muy eficientes de algoritmos que resuelven el problema del camino mínimo, para un único origen y todos los destinos y para parejas origen-destino. Estas implementaciones están totalmente integradas en una interfaz que proporciona transparencia y sencillez de cara al usuario, de modo que éste no nota diferencia a la hora de trabajar aunque use implementaciones distintas del algoritmo. No debe existir algoritmo mejor que otro (en términos de eficiencia). Para cierta combinación de condiciones (tamaño de grafo, tipo de solución requerida, topología del grafo, etc.) existirá uno que será más eficiente que el resto y cada uno de ellos será el más eficiente en como mínimo alguna de las combinaciones. Es decir, no tiene sentido ofrecer un algoritmo que en todos los casos proporcione una eficiencia peor que otro. Sencillez a la hora de integrar nuevas implementaciones en la biblioteca. Debe resumirse en añadir el código que difiera del resto de algoritmos aprovechando las partes comunes a todos. Por norma general las diferencias entre implementaciones suelen ser el uso de distintas estructuras de datos para almacenar datos temporales durante la ejecución del algoritmo, pero todos ellos comparten el algoritmo básico. 9 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 2.1.2. Necesidad de una librería de este tipo. Existen multitud de implementaciones de algoritmos que resuelven el problema del camino mínimo en grafos o redes. La mayoría de ellas, pero, están integradas en el propio código del software que las emplean con lo que su función no va más allá de ser un simple módulo o subrutina de todo un sistema más global. En estos casos seguramente cumplen a la perfección su función y cumplen los requerimientos propuestos. Normalmente a un algoritmo de este tipo se le pide que tenga una eficiencia temporal aceptable y el tiempo de procesado de su código supone un pequeño porcentaje del código total de la aplicación. Se pueden encontrar, ya sea pagando o gratuitamente, algunas bibliotecas que ofrecen unas pocas implementaciones del algoritmo de Dijkstra, pero están basadas más en la sencillez de su uso que en la rapidez de su ejecución. El problema surge cuando en una cierta aplicación, se necesita ejecutar estos algoritmos multitud de veces, ocupando gran parte del tiempo total de ejecución. En este caso una mínima diferencia de eficiencia temporal entre dos implementaciones puede suponer una demora considerable cuando es ejecutada miles o millones de veces. 10 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Se necesita, por tanto, una biblioteca que proporcione implementaciones lo más eficientes posible, pensadas para ser ejecutadas multitud de veces en una misma aplicación. Deben tener, entonces, las siguientes características: • Eficiencia temporal máxima. Empleo de complejas estructuras de datos temporales para minimizar el número total de operaciones en el proceso de búsqueda de la solución al problema. • Austeridad en funcionalidad extra. Hay que limitarse a proporcionar la solución, evitando proporcionar funciones que simplifiquen o hagan más cómodo su uso si eso conlleva demoras temporales. • Consideración de la topología del grafo de entrada. Una implementación puede ser la más eficiente para un determinado tipo de grafo o red pero no serlo para otro. Por ello hay que incluir una gran variedad de alternativas que abarquen el mayor de número de topologías. 11 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 2.1.3. Uso de la librería. Una vez desarrollada e implementada la librería, su uso se limitará en la construcción de aplicaciones que necesiten resolver problemas de caminos mínimos en redes o grafos. Se entiende que estas aplicaciones trabajarán o con grafos enormemente grandes o que gran parte de su tiempo de ejecución lo dedicarán a resolver problemas de caminos mínimos. Es decir, la librería es una buena ayuda a la hora de querer ganar eficiencia cuando la mayor parte de los cálculos se destina a este tipo de problemas. No obstante, no se limita el uso a este tipo de aplicaciones pues su sencilla interficie puede ser lo suficientemente atractiva para que muchos usuarios decidan emplear la librería aún cuando no ganen eficiencia temporal considerable o no sea su prioridad. 2.1.4. Usuarios Vale la pena recordar que estamos hablando de una librería que contiene distintas implementaciones de algoritmos que resuelven problemas de caminos mínimos, pero en absoluto se ofrece una herramienta que los resuelva por ella misma. Los usuarios, por tanto, no serán los comúnmente llamados “finales”, sino que serán la mayoría de veces desarrolladores de software que construyen aplicaciones donde aparecen subproblemas de caminos mínimos en grafos. 12 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3. El problema del camino mínimo 3.1. Definiciones Sea G = (Ν, Α) un grafo dirigido con nodos numerados 1, ..., N. Cada arco (i,j) ∈ Α tiene un coste o “longitud” aij asociado con él. La longitud de un camino (i1, i2, ..., ik) que consista exclusivamente en arcos hacia delante, es igual a la suma de los arcos k −1 ∑a n =1 inin +1 . Este camino es mínimo si tiene la longitud menor entre todos los caminos posibles con los mismos nodos origen y destino. El camino mínimo también puede llamarse distancia mínima. La distancia mínima de un nodo a sí mismo es 0 por convención. El problema del camino mínimo intenta buscar las distancias mínimas dentro de un grafo entre un cierto número de nodos. Según los nodos escogidos inicialmente, aparecen distintas versiones del problema aunque dos son las principales: • Un origen / muchos destinos: Se indica un nodo inicial y se requiere todos los caminos mínimos entre este nodo y el resto. La solución entonces aparece como un árbol de recubrimiento mínimo con el nodo inicial como raíz. Ejemplo: 2 1 4 4 1 1 1 0 1 5 3 1 2 3 4 Figura 1. Grafo donde se busca el camino mínimo. Nodo origen: 0. 13 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 1 4 1 1 1 0 5 1 1 2 3 Figura 2. Solución del problema del camino mínimo: un origen / muchos destinos. • Un origen / un destino: Se indica una pareja de nodos y se requiere el camino mínimo entre ellos. La solución será la sucesión de aristas que construyen el recorrido. Nótese que a partir de la versión un origen / muchos destinos se puede encontrar también la solución al de esta versión. 1 4 1 1 0 5 1 1 2 3 Figura 3. Solución del problema del camino mínimo: un origen / un destino. Nodo origen: 0. Nodo destino: 5. 14 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.2. Método general del problema del camino mínimo (un origen/muchos destinos) En este método se empieza con un vector de distancias al origen para cada nodo (d1, d2, ..., dN), inicializadas a un valor máximo o infinito. Se van seleccionando sucesivamente arcos (i, j) que violen la condición dj > di + aij, y se obliga a dj := di + aij. Se continúa hasta que la condición dj ≤ di + aij se satisfaga para todos los arcos (i, j). La idea es que, en el transcurso del algoritmo, di puede ser interpretado para todo i como la longitud de algún camino Pi desde el origen hasta i. Si dj > di + aij para algún arco (i, j), el camino que se obtiene extendiendo el camino Pi con el arco (i, j), de longitud di + aij, es más corto que el actual camino Pj, de longitud dj. Por eso, el algoritmo encuentra sucesivamente caminos más cortos desde el origen hasta todos los destinos. Normalmente se implementa este método examinando los arcos que salen de un nodo dado i consecutivamente. Se mantiene una lista de nodos V, llamada lista de candidatos, y un vector d = (d1, d2, ..., dN), donde cada dj, llamada etiqueta del nodo j, es un número real o ∞. Inicialmente, V = {1}, d1 = 0, di = ∞, ∀ i ≠ 1. El algoritmo itera hasta que V esté vacía. La típica iteración (asumiendo V no vacía) es: borrar un nodo i de la lista de candidatos V. para cada arco (i, j) ∈ A con j ≠ 1 hacer si dj > di + aij entonces dj := di + aij; añadir j a V si todavía no lo estaba fsi fpara Nótese que, en el transcurso del algoritmo, las etiquetas nunca se incrementan. Por lo tanto, di < ∞ ⇔ i ha entrado en la lista de candidatos V por lo menos una vez. 15 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.2.1. Algoritmo genérico del camino mínimo • Al final de cada iteración, se dan las siguientes condiciones: o d1 = 0. o Si dj < ∞ y j ≠ 1, entonces dj es la longitud de algún camino que empieza en 1, nunca regresa a 1, y termina en j. o Si i ∉ V, entonces di = ∞ o dj ≤ di + aij, • ∀j tal que (i, j) ∈ A. Si el algoritmo ha terminado, para todo j ≠ 1 tal que dj < ∞, dj es la distancia mínima entre 1 y jy dj = mín{d ( i , j )∈A i + aij }; dj = ∞ si y sólo si no existe ningún camino desde 1 hasta j. • Si el algoritmo no termina, entonces es que existe algún camino de longitud negativa que empieza en 1 y nunca regresa a 1. 16 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.3. Implementaciones del algoritmo genérico Existen muchas implementaciones del algoritmo genérico; difieren entre ellas en cómo se selecciona el nodo a eliminar de la lista de candidatos V. Así, se pueden clasificar en dos categorías: 3.3.1. Métodos etiquetadores (Label setting methods). En estos métodos, el nodo i eliminado de V es el nodo con la etiqueta más pequeña entre todos los candidatos en V. Asumiendo que todos los arcos son de longitud no negativa, estos métodos tienen una propiedad remarcable: cada nodo entrará en V como mucho una sola vez; su etiqueta obtendrá su valor final en el momento que salga de V. La parte más costosa en tiempo de estos métodos es calcular el nodo con etiqueta mínima entre todos los de V en cada iteración; existen muchas implementaciones que emplean una variedad de métodos más o menos creativos y estructuras de datos eficientes para calcular estos valores mínimos. 3.3.2. Métodos correctores de etiqueta (Label correcting methods). En estos métodos la elección del nodo i que se borra de V es menos sofisticada que en los métodos etiquetadores y requieren menos cálculo. Sin embargo, un nodo puede que entre en la lista de candidatos V muchas veces. En la práctica, cuando los arcos tienen longitud no negativa (cosa que supondremos siempre), los mejores métodos de los etiquetadores y los mejores de los correctores de etiqueta son competitivos entre sí. Hay también muchas cotas de complejidad en los peores casos para los dos métodos. Las mejores cotas corresponden a los métodos etiquetadores pero, sin embargo, en la práctica no se puede afirmar que los que tengan mejores cotas de complejidad temporal sean los mejores. 17 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.4. Métodos etiquetadores (Dijkstra) El algoritmo de Dijkstra es el más famoso de los algoritmos de caminos mínimos. Es idéntico al método general excepto que siempre borra de la lista de candidatos V el nodo j que tiene la etiqueta más pequeña. Es decir, dj = mín d i . i∈V El método queda así: Inicialmente, V = {1}, di = ∞, d1 = 0, ∀ i ≠ 1. El método itera hasta que V se queda vacía. Una típica iteración (asumiendo V no vacía) es: borrar de V un nodo i tal que di = mín d j . j∈V para cada arco (i, j) ∈ A, con j ≠ 1 hacer si dj > di + aij entonces dj := di + aij añadir j a V si no lo estaba ya fsi fpara Mirando con detalle el algoritmo, se puede considerar W como el conjunto de nodos que han estado en V pero que ya no lo están, W = {i | di < ∞, i ∉ V}. Si de V sólo se borra el nodo con etiqueta mínima, entonces W contiene los nodos con las etiquetas de valor más pequeño durante todo el algoritmo, dj ≤ di, si j ∈ W e i ∉ W. Como se asume aij ≥ 0, cuando un nodo i se borra de V y pasa a ser de W, se tiene que para cada j ∈ W tal que (i, j) es un arco de A, dj ≤ di + aij. Un nodo, al entrar en W, permanecerá siempre ahí ya y su etiqueta nunca variará. Por lo tanto, W puede ser visto como el conjunto de nodos permanentemente etiquetados, es decir, los nodos que han adquirido su etiqueta final, que debe ser igual a la distancia mínima desde el origen. 18 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Más formalmente: Se asume que todos los arcos son no negativos y que existe por lo menos un camino mínimo desde el nodo origen (nodo 1) hasta cada uno de los demás nodos. • Para cualquier iteración del método etiquetador, se cumple lo siguiente: W = {i | di < ∞, i ∉ V}. • Un nodo que pertenezca a W al principio de la iteración no entrará en la lista de candidatos V durante ésta. • Al final de la iteración, di ≤ dj para todo i ∈ W y j ∉ W. • Para cada nodo i, considerar caminos que empiezan en 1, acaban en i, y tienen todos sus nodos en W al final de la iteración. Entonces la etiqueta di al final de la iteración es igual a la longitud del mínimo de estos caminos (di = ∞ si no existe tal camino). • Al ser un método etiquetador, todos los nodos se borrarán de la lista de candidatos exactamente una vez en orden creciente de distancia al nodo 1; i será borrado antes que j si la etiqueta final satisface di < dj. 3.4.1. Tiempo de ejecución del algoritmo de Dijkstra • Selección de nodos. El algoritmo acaba seleccionando los N nodos del grafo quitándolos de V. Como tiene que encontrar cada vez el mínimo entre todos los de la lista de candidatos, el tiempo total de selección es N + (N – 1) + (N – 2) + ... + 1 = O(N2). • Actualización de distancias. El algoritmo examina cada arco (i, j) ∈ A exactamente una vez, para comprobar si j ≠ 1 o si se cumple la condición dj > di + aij y actualizar dj := di + aij si se requiere. En total, se necesitarán O(A) operaciones, menor en comparación con O(N2). El recorrido por todos los arcos en tiempo O(A) es inevitable y no es posible reducirlo. Sin embargo, la búsqueda de las etiquetas mínimas en tiempo O(N2) puede mejorarse considerablemente empleando diversas estructuras de datos que ayuden a hacer esta búsqueda más eficientemente. Las mejores estimaciones en el peor caso que se han obtenido son de O(A + NlogN) y O(A + N log C ), siendo C el rango de las longitudes de los arcos: C = max(i,j)∈Aaij. De todas formas la experiencia dice que los métodos que se comportan mejor en la práctica tienen cotas temporales en el caso peor bastante malas. 19 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.4.2. Ejemplo gráfico de ejecución del algoritmo ∞ 1 0 ∞ 2 4 4 1 1 1 0 1 3 1 0 5 1 3 1 0 4 4 1 1 0 1 1 2 5 5 3 1 1 3 1 3 2 4 Aristas visitadas Aristas del grafo solución 20 5 5 3 4 3 2 1 1 2 4 3 1 1 1 ∞ 4 4 0 3 ∞ 2 ∞ 3 2 2 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.5. Otras versiones del algoritmo 3.5.1. Algoritmo de Dijkstra hacia atrás En el algoritmo original se determina un camino mínimo desde s al resto de nodos en N – {s}. Esta versión lo que pretende resolver es la búsqueda de un camino mínimo desde cada uno de los nodos en N – {t} a un nodo destino {t}. Para ello, se modifica ligeramente el algoritmo original de Dijkstra. Se mantiene una distancia d’j para cada nodo j, que es una cota superior de la longitud del camino mínimo desde el nodo j hasta el nodo t. Como antes, el algoritmo designa a un conjunto de nodos, W, como a los permanentemente etiquetados y al resto, V, como a los temporalmente etiquetados. En cada iteración, el algoritmo escogerá el nodo con etiqueta temporal mínima, d’j, y lo hará permanente. Después examinará cada arco que llegue (i, j) y modificará la distancia del nodo i a mín{d’i, cij + d’j}. El algoritmo termina cuando todos los nodos hayan sido etiquetados permanentemente. 3.5.2. Algoritmo de Dijkstra bidireccional En algunas aplicaciones de caminos mínimos (el caso, por ejemplo, de la aplicación donde usaremos la librería desarrollada para este proyecto) no se necesita encontrar el camino mínimo entre un nodo s hasta el resto de nodos de la red o grafo. En vez de eso, se quiere determinar un camino mínimo entre un nodo s y otro t especificado también. Para resolver esto y evitar otros cómputos, se podría ejecutar el algoritmo original de Dijkstra y terminar en el instante en que se seleccione t de V (aunque otros nodos sigan estando etiquetados temporalmente). De todas formas, el siguiente algoritmo resuelve este problema de forma más eficiente en la práctica (aunque no en el peor caso). En el algoritmo bidireccional, se aplica simultáneamente la versión original (hacia delante) desde el nodo s y la versión hacia atrás desde el nodo t. El algoritmo alternativamente designa como permanentes un nodo en V y un nodo en V’ hasta que las dos versiones hayan etiquetado permanentemente el mismo nodo, por ejemplo el nodo k. En este punto, sea Pi el camino mínimo desde el nodo i ∈ W encontrado por el algoritmo de Dijkstra original, y sea P’j el camino mínimo desde el nodo j ∈ W’ al nodo t encontrado por la versión hacia atrás. Se puede demostrar que el camino mínimo desde el nodo s hasta el nodo t es o el camino Pk ∪ P’k o el camino Pi ∪ {(i, j)} ∪ P’j para algún arco (i, j), i ∈ W y j ∈ W’. Este algoritmo es muy eficiente porque tiende a etiquetar de forma permanente pocos nodos y casi nunca examina arcos incidentes a un número elevado de nodos. 21 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.6. Implementaciones del algoritmo de Dijkstra La selección de los nodos para sacarlos de la lista de candidatos es el principal cuello de botella del algoritmo de Dijkstra. La idea que tienen todas las distintas implementaciones del algoritmo es intentar guardar de alguna forma estos nodos para que encontrar el de etiqueta mínima sea lo más eficiente posible. 3.6.1. Implementación de Dial El algoritmo de Dial se basa principalmente en la siguiente propiedad: Las etiquetas de distancia que el algoritmo de Dijkstra designa como permanentes no van a disminuir su valor. Esta propiedad surge del hecho de que el algoritmo etiqueta permanentemente un nodo i con la menor distancia temporal di y, mientras recorre los arcos (i, j), nunca decrementará el valor de ninguna etiqueta más allá de di porque los arcos son no negativos. El algoritmo de Dial almacena los nodos con etiqueta temporal de un forma peculiar. Mantiene NC + 1 conjuntos, llamados baldes (buckets), numerados 0, 1, 2, ..., NC: El balde k guarda todos los nodos con distancia temporal igual a k. C es la longitud máxima de todos los arcos del grafo y, por tanto, NC es una cota superior de la distancia del origen a cualquier nodo. Los nodos con etiqueta infinita no se guardarán en ningún balde. El contenido de un balde será el conjunto contenido(k). En la operación de selección de nodo, se recorren los baldes 0, 1, 2, ... hasta encontrar el primero que no esté vacío. Supongamos que k es el primero no vacío. Entonces, cada nodo en contenido(k) tendrá la mínima distancia. Uno por uno, se borrarán estos nodos del balde, designándolos como permanentes (es decir, saldrán de V para pasar a W) y se recorrerán sus arcos para actualizar las distancias de los nodos adyacentes. Siempre que se modifique la etiqueta de algún nodo i de d1 a d2, deberá moverse i desde el contenido(d1) hasta el contenido(d2). En la siguiente operación de selección, deberá reanudarse la búsqueda desde los baldes k + 1, k + 2, ... para encontrar el siguiente balde no vacío. La propiedad en la que se basa el algoritmo de Dial asegura que los baldes 0, 1, 2, ..., k siempre estarán vacíos en las siguientes iteraciones hasta el final y, por tanto, no deberán ser examinados. La estructura de datos que guardará el contenido de un balde será una lista doblemente encadenada. Esta estructura permite realizar cada operación en tiempo O(1): comprobar si un balde está o no vacío, borrar un elemento del balde y añadir un elemento al balde. Con ésta, el algoritmo necesita un tiempo 22 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. O(1) para actualizar cada distancia, y un total de O(A) para todas las actualizaciones. El cuello de botella en esta implementación aparece al recorrer los NC + 1 baldes durante la selección de nodos. Consecuentemente, el tiempo de ejecución del algoritmo de Dial es de O(A + NC). Puesto que el algoritmo de Dial usa NC + 1 baldes, los requerimientos de memoria pueden llegar a ser prohibitivos. La siguiente idea permite reducir el número de baldes a C + 1: Si di es la etiqueta que el algoritmo designa como permanente al principio de una iteración, entonces al final dj ≤ di + C para cada etiqueta j en V. Esto proviene de que dl ≤ di para cada nodo l ∈ W y de que para cada nodo j ∈ V con dj ≠ ∞, dj = dl + cij para algún nodo l ∈ W. Por tanto, dj = dl + cij ≤ di + C. En otras palabras, todas las etiquetas no infinitas están comprendidas entre di y di + C. En consecuencia, son suficientes C + 1 baldes para guardar los nodos con etiquetas no infinitas. El algoritmo de Dial usa C + 1 baldes numerados 0, 1, 2, ... , C, que puede verse como una lista circular. Se guarda temporalmente un nodo j con etiqueta dj en el balde dj mod (C + 1). Durante la ejecución del algoritmo, el balde k guarda los nodos con etiqueta k, k + (C + 1), k + 2(C + 1), etc. Sin embargo, por la propiedad antes expuesta, un nodo siempre guardará nodos de la misma distancia. Esta forma de almacenamiento también implica que, si un balde k contiene un nodo con la mínima etiqueta, entonces los baldes k + 1, k + 2, ..., C, 0, 1, 2, ..., k – 1, guardan nodos con etiquetas mayores. Este algoritmo examina los baldes secuencialmente para identificar el primero no vacío. En la siguiente iteración sigue recorriéndolos empezando desde donde lo dejó. Una potencial desventaja de esta implementación comparada con la implementación original de Dijkstra O(N2) es que requiere mucho espacio de memoria cuando C es muy grande. Y, en ese caso, el tiempo de cálculo también se incrementará al tener que recorrer una estructura tan grande. El algoritmo se ejecuta en O(A + NC), que es pseudopolinómico. Por ejemplo, si C = N4, el algoritmo se ejecuta en O(N5) y, si C = 2n, se necesita un tiempo exponencial en el peor de los casos. Sin embargo, el algoritmo normalmente no llega a la cota de O(A + NC). Para la mayoría de aplicaciones, C es un número modesto, y los baldes que se recorren suelen ser inferiores a N - 1, con lo que el tiempo de ejecución en la práctica es mucho mejor que el indicado por su complejidad en el caso peor. 23 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.6.2. Implementaciones con colas de prioridad Una cola de prioridad o heap, es una estructura de datos que permite realizar las siguientes operaciones sobre una colección de objetos H, cada uno asociado a un número real llamado su clave. crear-heap(H). Crea un heap vacío. busca-mínimo(i, H). Encuentra y devuelve el elemento i de clave mínima. inserta(i, H). Inserta un nuevo elemento i con una clave predefinida. decrementa-clave(valor, i, H). Reduce la clave del elemento i desde su valor actual a valor, que debe ser inferior a la que reemplaza. borra-mínimo(i, H). Borra el elemento i de clave mínima. Si se implementa el algoritmo de Dijkstra usando una cola de prioridad, H sería la colección de nodos con etiqueta de distancia finita y temporal (los que están en V). El algoritmo queda así: algoritmo Dijkstra-heap; crear-heap(H); dj := ∞ para todo j ∈ N; ds := 0 y pred(s) := 0; inserta(s, H); mientras H ≠ ∅ hacer busca-mínimo(i, H); borra-mínimo(i, H); para cada (i, j) ∈ A(i) hacer valor := d(i) + cij; si d(j) > valor entonces si d(j) = ∞ entonces d(j) := valor; pred(j) := i; inserta(j, H) si no d(j) := valor; pred(j) := i; decrementa-clave(valor, i, H) fsi fsi fpara fmientras Del algoritmo se puede observar que las operaciones busca-mínimo, borra-mínimo e inserta se realizan como mucho N veces y que la operación decrementa-clave como mucho A veces. Se analizarán a continuación los tiempos de ejecución del algoritmo de Dijkstra implementado usando diferentes tipos de heaps: heaps binarios, d-heaps, heaps de Fibonacci ... 24 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.6.3. Implementación con heap binario Un heap binario necesita un tiempo O(logN) para efectuar las operaciones inserta, decrementaclave y borra-mínimo, y un tiempo O(1) para las demás. En consecuencia, esta versión ejecuta el algoritmo de Dijkstra en tiempo O(AlogN). Es más lenta que la original cuando se tratan grafos muy densos (por ejemplo de orden cuadrático respecto el número de nodos), pero es más rápida cuando A = O(N2 / logN). 3.6.4. Implementación con d-heap Dado un parámetro d ≥ 2, el d-heap requiere un tiempo O(dlogdN) para las operaciones borramínimo y un tiempo constante O(1) para el resto. Por eso, el tiempo de ejecución de esta versión es de O(AlogdN + NdlogdN). Para obtener el valor óptimo de d, se igualan los dos términos, dando d = máx{2, ⎡A/N⎤}. El tiempo resultante es O(AlogdN). Hay que observar que para redes muy dispersas (por ejemplo con A = O(N)) el tiempo de ejecución es O(NlogN). Para grafos no dispersos (A = f(N1 + x ) para algún x > 0), el tiempo de cálculo de la implementación con d-heap es O(AlogdN) = O((AlogN)/(logd)) = O((AlogN)/(logNx)) = O((AlogN)/(xlogN)) = O(A/x) = O(N). El último paso es cierto porque x es una constante. Por tanto, el tiempo de cálculo total es O(A), que es óptimo. 3.6.5. Implementación con heap de Fibonacci El heap de Fibonacci permite hacer cada operación en tiempo O(1) excepto borra-mínimo, que requiere un tiempo O(logN). Por lo tanto, el tiempo de cálculo total es O(A + NlogN). Esta cota temporal es bastante mejor que la de la implementación con heap binario y con d-heap para todos las densidades de grafo. La implementación, además, es actualmente la mejor polinómicamente hablando para resolver el problema del camino mínimo. 3.6.6. Implementación con Radix Heap Esta implementación es una mezcla de la versión original y la de Dial, que usa nC + 1 baldes. La implementación original de Dijkstra designa una misma prioridad a todas las etiquetas temporales y busca la mínima entre ellas, como si se encontrasen en un mismo y gran balde. Dial emplea un número considerable de baldes para separar las etiquetas y, así, las búsquedas se efectúan entre menos elementos. La implementación con radix heap mejora estos dos métodos adoptando una aproximación intermedia: guarda un cierto número de etiquetas en un balde, pero no todas. En vez de guardar en cada balde las etiquetas con misma distancia, guarda las etiquetas con valor comprendido en un mismo rango. La cardinalidad de este rango se denomina anchura. Si se emplean baldes de 25 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. anchura k, se reduce el número necesario en un factor de k. Pero para encontrar la etiqueta mínima, habrá que buscar entre todas las etiquetas del balde no vacío con rango más pequeño. Nótese que si k es suficientemente grande, sólo existiría un único balde y se tendría el algoritmo original de Dijkstra. Para evitar tener que buscar la etiqueta mínima, se modifica esta implementación de forma que siempre el balde con rango inferior tenga anchura 1. El algoritmo de radix heap usa anchuras variables para los baldes y los cambia dinámicamente. 1. Las anchuras de los baldes son 1, 1, 2, 4, 8, 16, ..., con lo que el número necesario de ellos es de sólo O(log(NC)). 2. Los rangos se modifican dinámicamente y se recolocan las etiquetas temporales de forma que siempre se tenga la mínima en el balde de anchura 1. La ventaja sobre la versión de Dial es que ahora sólo se tienen O(logNC)) baldes, pero se mantiene la propiedad de que no hay que hacer búsquedas de etiquetas mínimas. Esta implementación tiene un coste temporal de O(A + Nlog(NC)). 26 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.7. Métodos Correctores de Etiqueta El método corrector de etiqueta es prácticamente idéntico a su hermano etiquetador. La diferencia radica en que no realiza ningún cómputo complejo para seleccionar un elemento de V, empleando siempre un tiempo O(1) para ello. En el método etiquetador un elemento extraído de V, pasaba a W y se quedaba ahí hasta el final del algoritmo. Ahora, normalmente esto no ocurrirá así y un elemento podrá entrar en V más de una vez. El algoritmo queda así: algoritmo corrector_de_etiqueta d(j) := ∞ para cada nodo j ∈ N; d(s) := 0 y pred(s) := 0; V := {s}; mientras L ≠ ∅ hacer borrar un elemento i de V; para cada arco (i, j) ∈ A(i) hacer si d(j) > d(i) + cij entonces d(j) := d(i) + cij; pred(j) := i; si j ∉ V entonces añade j a V; fsi fpara fmientras El coste de ejecución de este algoritmo es de O(NA). 3.7.1. Implementación con “dequeues” Esta modificación proporciona muy buenos tiempos en la práctica. De hecho, es de los mejores métodos para redes dispersas. La única contrapartida es que su caso peor tiene un coste pseudopolinómico. Esta implementación guarda V en una “dequeue”. Una “dequeue” es una estructura de datos que permite guardar una lista a la que se pueden añadir o borrar elementos tanto por delante como por detrás. Este algoritmo siempre selecciona los nodos que están delante de V, pero los añade según sea el caso. Si el nodo ya había estado en la lista, lo añade al frente. Si no, detrás. Este heurístico ha sido comprobado empíricamente y ha dado como resultado que el algoritmo examine menos nodos que la mayoría de otros algoritmos de caminos mínimos. 27 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.8. Resumen y recapitulación de los algoritmos Algoritmo Dijkstra original Coste temporal O(N2) Dial O(A + NC) d-Heap O(AlogdN), con d = A/N Fibonacci O(A + NlogN) Radix Heap O(A + Nlog(NC)) Corrector de etiqueta O(AN) Corrector de etiqueta con “dequeue” O(mín({NAC, A2N}) 28 Características Muy fácil de implementar. Ofrece el mejor tiempo de ejecución para grafos densos. Fácil de implementar y excelente comportamiento empírico. Coste temporal pseudopolinómico y no atractivo en teoría. Tiempos de ejecución lineales si el número de aristas es función del número de nodos elevado a alguna potencia. Ofrece el mejor tiempo polinómico teórico. Difícil y complicado de implementar. En principio es una mejora del algoritmo de Dial. Excelentes tiempos de ejecución. Ofrece el mejor tiempo polinómico con pesos de aristas arbritarios. Bastante eficiente en la práctica. Muy eficiente en la práctica (con tiempo posiblemente lineal). El peor caso es intratable. Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. A continuación se expone una tabla que trata de estudiar la eficiencia de estos algoritmos en la teoría. Como se ha expuesto anteriormente parece que en la práctica la mayoría de los algoritmos se comportan de forma diferente a lo que se podría esperar. Es por ello que no se podrá desechar ninguna de las versiones como tampoco adivinar cuál de ellos es el más rápido pero seguramente se podrá tener una primera aproximación de sus comportamientos según las tres variables principales: número de nodos del grafo, número de aristas (densidad) y peso máximo de las aristas (aunque no todos dependen de ésta). 3.8.1. Grafos completos. Aumentando el número de nodos. Nodos Aristas Peso máx Dijkstra Dial 10 20 100 400 1 1 40 80 1600 6400 1 1 d-Heap Fibonacci RadixHeap Corrector Dequeue 100 110 100 110 110 1000 1000 400 1600 6400 420 1640 6480 400 1600 6400 426 1664 6552 426 1664 6552 8000 64000 512000 8000 64000 512000 Para grafos densos los métodos etiquetadores tienen claramente los mejores costes temporales, aunque entre ellos no hay grandes diferencias. El método de dijkstra, tal como se expuso, es el que consigue tiempos peores de ejecución más pequeños. 3.8.2. Grafo completo pequeño. Aumentando coste máximo de arista. Nodos Aristas Peso máx Dijkstra 10 10 10 10 10 100 100 100 100 100 1 10 100 1000 1000000 100 100 100 100 100 Dial 110 200 1100 10100 10000100 d-Heap Fibonacci RadixHeap Corrector Dequeue 100 100 100 100 100 110 110 110 110 110 110 120 130 140 170 1000 1000 1000 1000 1000 1000 10000 100000 102400 102400 Como en el caso anterior, Dijkstra sigue siendo el más eficiente en grafos completos. Nótese que Dial no será nada recomendable para grafos completos con pesos máximos de arista elevados. 29 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.8.3. Grafo completo grande. Aumentando coste máximo de arista. Nodos Aristas Peso máx Dijkstra Dial d-Heap Fibonacci RadixHeap Corrector Dequeue 106 106 106 106 106 109 106 1 109 103 3 6 6 6 6 6 6 9 10 10 10 10 10 10 10 10 10 1010 3 6 6 6 6 6 6 9 10 10 10 10 10 10 10 10 100 1011 3 6 6 6 6 6 9 10 10 10 10 10 10 10 103 2*106 1012 6 6 6 6 9 6 9 103 106 10 10 10 10 10 10 10 1015 En grafos completos grandes ya no existe tanta diferencia entre los métodos etiquetadores si exceptuamos el de Dial que, como anteriormente, se comporta bastante mal cuando los pesos máximos de arista crecen considerablemente. 3.8.4. Grafo pequeño. Disminuyendo densidad de aristas. Nodos Aristas Peso máx Dijkstra Dial 10 10 10 10 10 100 75 50 25 11 1 1 1 1 1 100 100 100 100 100 110 85 60 35 21 d-Heap Fibonacci RadixHeap Corrector Dequeue 100 83 72 52 37 110 85 60 35 21 110 85 60 35 21 1000 750 500 250 110 1000 750 500 250 110 Aquí se encuentra la primera dificultad a la hora de asignar un algoritmo ideal para un cierto tipo de grafo. Es cierto que a partir de una densidad media Dial, Fibonacci y RadixHeap sobresalen del resto pero con densidades medio-altas será difícil precisar cuál de ellos se comporta mejor temporalmente. 3.8.5. Grafo grande. Disminuyendo densidad de aristas. Nodos Aristas Peso máx Dijkstra Dial 106 106 1 106 103 3 6 5 10 10 5*10 1 5*105 3 6 10 10 3*105 1 3*105 3 6 5 10 10 10 1 105 3 6 10 10 2*103 1 3*103 d-Heap Fibonacci RadixHeap Corrector Dequeue 106 6*105 3*105 3*105 2*104 106 5*105 3*105 105 5*103 106 5*105 3*105 105 5*103 109 5*108 3*108 108 2*106 109 5*108 3*108 108 2*106 Dial se sitúa en el algoritmo más eficiente cuando la densidad baja considerablemente (aunque en este ejemplo los costes máximos de arista son mínimos). Cabe notar que d-Heap tiene un extraño comportamiento en el rango de densidades medio-altas, donde en algunos casos es comparable al resto de métodos etiquetadores, aunque para densidades pequeñas es de los menos eficientes. 30 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.8.6. Grafo disperso pequeño. Aumentando coste máximo de arista. Nodos Aristas Peso máx Dijkstra Dial d-Heap Fibonacci RadixHeap Corrector Dequeue 10 10 10 10 10 11 11 11 11 11 1 2 4 10 100 100 100 100 100 100 21 31 51 111 1011 37 37 37 37 37 21 21 21 21 21 21 24 27 31 41 110 110 110 110 110 110 220 440 1100 11000 Al estudiar anteriormente el comportamiento en grafos pequeños disminuyendo la densidad, se vio que habían tres versiones “ganadoras”. Aquí se puede afirmar que para un coste máximo de arista arbitrario, Fibonacci es el método más adecuado y, en cualquier caso, Dial no es nada aconsejable. 3.8.7. Grafo disperso grande. Aumentando coste máximo de arista. Nodos Aristas Peso máx Dijkstra Dial d-Heap Fibonacci RadixHeap Corrector Dequeue 4*103 103 1 106 2*103 104 4*103 106 106 103 3 3 6 4 3 3 6 10 10 10 10 4*10 4*10 10 2 3*103 2*106 3 3 6 4 3 6 3 3 10 10 10 10 4*10 10 4 5*10 5*10 4*106 3 3 6 4 3 6 10 10 10 10 4*10 10 10 104 5*103 107 6 4 3 6 5 3 103 103 10 10 4*10 10 100 10 6*10 108 Como se había observado anteriormente, Dial ofrece los mejores tiempos en grafos dispersos con costes máximos de arista pequeños pero, a mayores pesos de arista Fibonacci destaca algo sobre el resto. 31 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 3.8.8. Resumen Este primer estudio teórico de los algoritmos ha proporcionado una primera idea del comportamiento de los mismos aunque habrá que esperar a un posterior estudio empírico, una vez implementada la librería y mediante juegos de pruebas, para conocer sus comportamientos reales. Es de destacar el horroroso papel de los métodos correctores, con tiempos de ejecución teóricos nada atractivos que hacen pensar en descartarlos totalmente. El motivo de que se sigan manteniendo en la librería de caminos mínimos es el hecho de que parece que en la práctica se pueden comportar incluso mejor que los métodos etiquetadores, según la bibliografía. Densidad Tamaño Grande Alta Pequeño Grande Baja Peso máximo Alto Bajo Alto Bajo Alto Bajo Alto Algoritmo más eficiente Etiquetadores excepto Dial Etiquetadores Dijkstra, d-Heap Dijkstra, d-Heap Fibonacci Dial Fibonacci Dial, Fibonaccci, RadixHeap Pequeño Bajo 32 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 4. Implementación Uno de los objetivos del presente proyecto es la implementación de algoritmos lo más eficientemente posible. Si se resume al máximo el comportamiento de los algoritmos se puede observar que los mismos no son más que recorridos a través de un grafo. El orden en que se recorre es lo que los diferencia entre ellos, además de la forma en que guardan valores temporales. Por tanto, donde realmente se tiene que tener cuidado a la hora de implementar para lograr la mejor eficiencia es en ese recorrido sobre el grafo. El grafo debería ser implementado mediante una estructura que permitiera acceder a los elementos necesarios para “viajar” por él en tiempo de ejecución mínimos. Analizando con más detalle el algoritmo de Dijkstra se podrán detectar los elementos del grafo que son más cruciales respecto a la eficiencia temporal. borrar de V un nodo i tal que di = mín d j . j∈V para cada arco (i, j) ∈ A, con j ≠ 1 hacer si dj > di + aij entonces dj := di + aij añadir j a V si no lo estaba ya fsi fpara De V, se borrarán como máximo el número de nodos del grafo. Además, primero deben añadirse. Para cada uno de estos nodos borrados se examinarán todas sus aristas salientes. De las aristas se necesita saber a qué nodo van a parar y el peso de las mismas. El resto de cálculos deberían ser simples operaciones aritméticas o comparaciones, que no dependerán de las implementaciones. Resumiendo, se realizarán |N| inserciones en una estructura de datos que representará a V, |N| extracciones de V y |A| observaciones de aristas. 33 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Como se ha comentado anteriormente, la investigación de las aristas del grafo es inevitable a la hora de encontrar los caminos mínimos. Lo que diferenciará a cada uno de los algoritmos es la representación del conjunto V y las operaciones de inserción y extracción sobre él. Esto no evita, sin embargo, que el recorrido por las aristas del grafo no tenga que ser lo más rápido posible, pues normalmente siempre será mucho mayor |A| que 2*|N|. Sobre la estructura V, en los métodos etiquetadores el problema radica en que cada vez que se extrae un elemento, éste tiene que ser el mínimo. En los métodos correctores este problema no existe, pero, se realizan muchas más inserciones y extracciones. Este proyecto no pretende buscar métodos eficientes de búsqueda de valores mínimos, pues éstos ya los proporciona la distinta bibliografía y fuentes de documentación, pero sí que tiene que proporcionar la implementación y, por tanto, encontrar la forma más idónea para traducir éstos métodos a código ejecutable, intentando que los tiempos de ejecución se minimicen. Concluyendo, habrá que decidir sobre la implementación de: • Grafos: encontrar la estructura idónea para que las operaciones de creación y recorrido tengan los mejores tiempos teóricos y prácticos. • TADs: encontrar las estructuras idóneas para minimizar los tiempos de ejecución de sus métodos u operaciones. 34 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 4.1. Representaciones de un grafo A continuación se exponen distintas posibles representaciones de un grafo. Al final se decidirá cuál de ellas es la más idónea para este proyecto. 4.1.1. Matriz de incidencias nodo-arista Esta representación consiste en una matriz M de tamaño N x A (número de vértices x aristas del grafo). La columna correspondiente a la arista (i, j) únicamente tiene dos elementos distintos de 0. Tiene +1 en la fila correspondiente al nodo i y -1 a la fila correspondiente al nodo j. Las características de esta implementación son: • La matriz M sólo tiene 2A de sus NA entradas con valores diferentes a 0. • Los valores distintos a 0 son +1 o -1. • Cada columna tiene exactamente un +1 y un -1. • El número de +1s en una fila equivale al grado saliente del correspondiente nodo y el número de -1s en la fila equivale al grado entrante del nodo. Ventajas: • Sencillo de implementar. Desventajas: • Uso ineficiente del espacio. • No guarda los pesos de las aristas. 35 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 4.1.2. Matriz de adyacencias nodo-nodo Esta representación consiste en una matriz M de tamaño N x N. La matriz contiene una fila y una columna para cada nodo y una entrada ij es igual a 1 si (i, j) ∈ A y 0 en caso contrario. Se puede emplear otra matriz C del mismo tamaño para almacenar los costes de las aristas. Las características de esta implementación son: • La matriz tiene N2 elementos. • Sólo A elementos de la matriz son distintos de 0. • El coste de una arista (i, j) se obtiene mirando el elemento ij en la matriz C. O(1). • Los arcos que salen del nodo i se obtienen escaneando la fila i. O(N). • Los arcos que entran al nodo j se obtienen escaneando la columna j. O(N). Ventajas: • Sencillo de implementar. • Eficiente en espacio si el grafo es lo suficientemente denso. • Eficiencia temporal a la hora de escanear arcos en grafos lo suficientemente densos. Desventajas: • Uso ineficiente de espacio en grafos poco densos. • Ineficiencia a la hora de escanear arcos en grafos poco densos. • Uso de una matriz adicional para guardar los costes. 36 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 4.1.3. Listas de adyacencia Esta representación guarda la lista de adyacencia de cada nodo i (el conjunto de nodos j tal que (i, j) ∈ A) en una lista encadenada. La lista de adyacencia para el nodo i será una lista encadenada que tendrá |A(i)| celdas y cada celda corresponderá a la arista (i, j) ∈ A. La celda correspondiente a la arista (i, j) tendrá tantos campos como información se quiera guardar. Uno de ellos guardará al nodo j. También se pueden añadir campos para el peso o la capacidad de la arista. El último campo guardará un enlace a la siguiente celda en la lista de adyacencias. Si la celda es la última de la lista se guardará un 0 en este campo. Además, se necesita de un vector de punteros que apunten a la primera celda para cada lista de adyacencias (hay una por nodo). Se define por lo tanto un vector de tamaño N llamado primero cuyo elemento primero(i) guarda un puntero dirigido la primera celda de la lista de adyacencias del nodo i. Si la lista de adyacencias está vacía el puntero tomará el valor 0. Ventajas: • Uso eficiente del espacio, independientemente de la densidad del grafo. • Eficiencia temporal a la hora de escanear las aristas salientes de un nodo. Inconvenientes: • No tan sencillo de implementar como las matrices. 37 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 4.1.4. Representaciones en estrella La representación en estrella hacia delante es similar a las listas de adyacencias pero, en vez de usar listas encadenadas, se usan vectores. Primeramente se asocia secuencialmente un número a cada arista del grafo, en orden ascendente según del nodo de donde salgan y, si salen del mismo nodo, de forma arbitraria. Para cada arista se guarda el nodo origen, nodo destino, coste y capacidad. A cada nodo se le añade un puntero que se dirige al primero de estos arcos que emanan de él. Con esta representación se tiene de forma eficiente el conjunto de aristas que salen de un nodo determinado. Si se desea también conocer los conjuntos de aristas que llegan a los nodos se necesita una estructura adicional llamada de estrella hacia atrás. Partiendo de una representación en estrella hacia delante es muy fácil conseguir la versión hacia atrás. Se van examinando los nodos en orden y, para cada uno, se guarda el destino, origen, coste y capacidad de los arcos que llegan al nodo i. Como antes, se guarda un puntero que indica para un nodo dado la primera de las aristas que llegan a él. Si se juntan las dos representaciones es fácil ver que hay información duplicada. Para evitar esto, en la representación estrella hacia atrás únicamente se guarda el número de arista pues la información ya se tiene en la representación estrella hacia delante. Ventajas: • Uso eficiente del espacio. • Eficiencia temporal a la hora de escanear aristas. • Se tiene información sobre aristas entrantes y salientes para cada nodo. Inconvenientes: • Es la versión más difícil de implementar. • Más difícil de actualizar. 38 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 4.2. Diseño de la representación del grafo Para la representación del grafo en la librería se empleará la representación estrella hacia delante y atrás, con algunas modificaciones. La principal razón es la eficiencia temporal que ofrece, pues trabaja únicamente con vectores. Posiblemente una versión con punteros ofrezca un uso más racional del espacio pero en este proyecto el factor crítico es la velocidad y, en todo caso, no se desperdicia demasiado el espacio de memoria. 4.2.1. Elementos del grafo N (entero): Número máximo de nodos. M (entero): Número máximo de aristas. C (entero): Peso máximo de arista. ini_salen (vector de enteros [1..M]): ini_salen[i] guarda el número de la primera arista que sale del nodo i. ini_llegan (vector de enteros [1..M]): ini_llegan[j] guarda el número de la primera arista que llega al nodo j. Estructura de datos AristaTAD: d (entero): nodo destino de la arista. o (entero): nodo origen de la arista. p (entero): peso de la arista. sig_d (entero): número de la siguiente arista incidente al origen. sig_o (entero): número de la siguiente arista incidente al destino. aristas (vector de AristaTAD): aristas[a] guarda la información de la arista con número a. 39 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Ejemplo: Nótese que es un grafo con aristas bidireccionales. Por tanto, en la estructura se guardará la información relativa al grafo como si las aristas se desglosaran en dos, una por cada sentido. Se numerarán con números impares las que vayan en un sentido y con número pares en el otro. En la figura 4 los números en las aristas indican los pesos de las mismas. En la figura 5 los números en las aristas indican una posible numeración de las mismas al guardarlas en la estructura. 20 1 12 2 30 11 22 40 4 3/4 1 5 7/8 2 5/6 1/2 3 9/10 11/12 4 Figura 4 y 5. Ejemplo de representación de grafo. 40 5 3 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. N: 5 M: 6 C: 0 Ini_salen -1 -1 aristas Arista d o p sig_d sig_o 1 4 1 11 -1 -1 -1 2 -1 3 4 Ini_llegan -1 -1 -1 5 6 7 8 -1 9 -1 10 -1 11 12 C = 11 Ini_salen 1 -1 aristas Arista d o p sig_d sig_o 1 4 1 11 -1 -1 -1 2 1 4 11 -1 -1 Ini_salen 1 -1 aristas Arista d o p sig_d sig_o 1 4 1 11 -1 -1 -1 3 5 2 -1 2 1 4 11 -1 -1 4 3 2 1 20 -1 1 4 Ini_llegan -1 -1 -1 6 8 9 Ini_llegan 2 -1 -1 5 7 6 7 8 1 -1 10 -1 9 -1 11 1 10 12 -1 11 12 C = 20 Ini_salen 3 -1 aristas Arista d o p sig_d sig_o 1 4 1 11 -1 -1 -1 2 1 4 11 -1 -1 2 3 2 1 20 -1 1 4 1 2 20 2 -1 Ini_llegan 3 2 -1 5 6 7 8 -1 9 1 10 -1 11 12 C = 20 Ini_salen 4 3 -1 2 -1 Ini_llegan 4 3 etc… 41 -1 1 -1 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 4.3. Implementación de las estructuras de datos. Las estructuras de datos eficientes son muy distintas entre sí. Pretenden la mayoría minimizar los costes temporales a la hora de encontrar el valor mínimo de entre todos los que guarda. Cada una de ellas hace hincapié en algún aspecto. Con matices, unas dedican sus esfuerzos en tener bien localizado siempre el valor mínimo con lo que los mayores tiempos de ejecución se dan a la hora de insertar los valores y otras los insertar sin preocuparse, teniendo que realizar las búsquedas posteriormente. Las hay desde simples listas hasta estructuras complejas como los heaps de Fibonacci. Todas ellas tienen en común su objetivo: almacenar un conjunto de valores y permitir la extracción del valor mínimo, así como de insertar nuevos elementos. Mayoritariamente estas estructuras tienen forma de árbol, o parecidas: elementos que guardan los valores enlazados con otros elementos. Por ello, es fácil pensar en implementaciones basadas en punteros, minimizando el espacio en memoria. Pero, como se hizo con la estructura del grafo, se sacrificará el uso de la memoria para obtener un código más veloz que navegue por los elementos de las estructuras. Por tanto, se implementarán casi siempre mediante vectores, implicando por ello tener que renunciar a diseños más intuitivos como podría ser en el caso de usar punteros de memoria. De todas formas, una vez implementados los métodos básicos y subiendo al siguiente nivel de abstracción, se podrá realizar casi una copia idéntica del algoritmo proporcionado por la documentación al código implementador. 42 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 5. Especificación del software. 5.1. Modelo conceptual. 5.1.1. Introducción. ¿Qué elementos deben aparecer en el modelo conceptual? Básicamente dos: el grafo donde se quiere encontrar el/los camino/s mínimo/s y la variante del algoritmo que se empleará para lograrlo. Debería existir una clase representativa (por ejemplo CamMin) de las variantes que, además, incluya una parte común a todas ellas. Esto hace pensar en aplicar un patrón controlador: la clase CamMin se encarga de recibir los parámetros de entrada (grafo, nodo origen y destino del grafo, variante de algoritmo deseada) y de guardar los de salida (solución del problema). Esto proporciona una total transparencia de cara al usuario. La interacción es únicamente con esta clase y no se conoce absolutamente nada de lo que hay detrás. Las distintas implementaciones de los algoritmos se podrán así modificar en un futuro sin que cambie el método de trabajo. Además, se facilita el poder añadir nuevas versiones de algoritmos o, en caso extremo, eliminar alguna. El siguiente paso es ver qué clases representarán a las implementaciones de los algoritmos. Una primera aproximación (la más simple) es crear una clase por versión que se encargue precisamente de ejecutar el algoritmo correspondiente pero observando las siete versiones propuestas es fácil ver que varios de ellos tienen mucho en común y que sería conveniente seguir con este diseño de detectar partes comunes y asignarlas a clases más genéricas. 43 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Los grupos serían: • Dijkstra. o o o • • • Dijkstra original. d-Heap. Fibonacci. Dial RadixHeap Corrector de etiqueta o o Corrector Dequeue La única diferencia que existe entre Dijkstra original, d-Heap y Fibonacci es la estructura de datos que guarda las etiquetas temporales de los nodos que se van visitando. Por tanto, la solución ideal es diseñar una clase Dijkstra general que resuelva el problema indicándole siempre qué estructura temporal se quiere usar. Esto permitirá asimismo poder en un futuro añadir nuevas estructuras de datos que resulten más eficientes que las actuales. La única tarea sería diseñar e implementar esta nueva estructura de datos y añadirla a la librería, sin modificar absolutamente nada la clase que implementa el algoritmo de Dijkstra. Algo parecido similar resulta en los algoritmos correctores de etiqueta, donde la única diferencia sigue estando en las estructuras de datos empleadas. Dial y RadixHeap sí que son algoritmos distintos y por lo tanto tendrán una clase cada uno que los implemente completamente. Finalmente, y como se podría imaginar, existirá una clase por cada estructura de datos a implementar. Concretamente: lista doblemente encadenada (para dijkstra, corrector, el TAD balde y el TAD radixheap), balde (para Dial), d-heap (para dijkstra con d-heap), fibonacci heap (para dijkstra con fibonacci heap), radix heap (para radix heap) y dequeue (para el método corrector con dequeue). 44 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 5.1.2 Aparición de nuevos algoritmos Una revisión detallada del algoritmo de RadixHeap muestra que en cada balde se tiene una lista doblemente encadenada donde se tienen etiquetas dentro de un rango. De todas estas etiquetas en un momento dado se busca la de menor valor. Es decir, se busca en una lista doblemente encadenada el elemento de valor mínimo. Una posible modificación que podría mejorar la eficiencia de este algoritmo sería guardar estos valores en d-heaps o en fibonacci heaps. Aparecerían entonces tres versiones del RadixHeap: con listas doblemente encadenadas, con d-heaps y con fibonacci heaps. A priori se desconoce si esta modificación será efectivamente una mejora pero un diseño que permita utilizar cualquier estructura de datos temporal para guardar estas etiquetas siempre será beneficiosa. Tal vez ninguna de estas versiones sea mejor pero en un futuro se puede encontrar una estructura de datos que sea realmente efectiva y su inserción en el algoritmo debería ser lo más simple y transparente posible. Por tanto, se añaden dos nuevos algoritmos más, haciendo un total de nueve: Dijkstra con listas doblemente encadenadas. Dijkstra con d-heaps. Dijkstra con fibonacci heaps. Dial. RadixHeap con listas doblemente encadenadas. RadixHeap con d-heaps. RadixHeap con fibonacci heaps. Corrector de etiqueta. Corrector de etiqueta con dequeues. 5.1.3. Herramienta de especificación y diseño La especificación y el diseño teórico del sistema ha sido desarrollado mediante la metodología del Lenguaje Unificado de Modelado (UML), generando los diagramas, clases y casos de uso que se presentan en los siguientes apartados. 45 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 5.1.4. Diagrama de clases Dlista DHeap FibHeap CamMín Grafo inserta(elem, clave) borra(elem) dameMinimo() : elem dameClave(elem) : clave esMiembro?(elem) : bool esVacio?() : bool borraMinimo() : elem modifica(elem, clave) imprime() dameClave(elem) : clave inserta(elem) inserta(elem, clave) modifica(elem, clave) borra (elem) borraMinimo() : elem dameMinimo() : elem imprime() esMiembro?(elem) : bool esVacio?() : bool origen : ent destino : ent padres : vector de ent dist : vector de ent * inserta(ent, ent) inserta_delante(ent, ent) borra(entero) dameMinimo() : ent printaLista() damePrimero() : ent dameValor(ent) : ent modifica(ent, ent) esMiembro?(ent) : bool esVacio?() : bool dameTam() : ent borraMinimo() : ent 1 ejecuta() damePadres() dameDistancias() dameDestino() * Dial Dijkstra ejecuta() damePadres() dameDistancias() dameDestino() Radix 1 Balde insNodo(nodo, ent, ent) contenido() : DLista borraNodo(nodo, ent) imprimeBaldes() pertRango?(valor, balde) : bool contenido(balde) : {DLista, DHeap, FibHeap} insNodo(nodo, valor, balde) insNodo(nodo, valor) borraNodo(nodo, balde) borraNodo2(nodo, valor) imprimeBaldes() dameMin() : nodo 1 * ejecuta() damePadres() dameDistancias() dameDestino() * n : entero m : entero C : entero damePrimSale(vertice) : arista damePrimLlega(vertice) : arista dameSigSale(arista) : arista dameSigLlega(arista) : arista dameDestino(arista) : vertice dameOrigen(arista): vertice damePeso(arista) : peso creaArista(vertice, vertice, peso) leeGrafo(fichero) : bool grabaGrafo(fichero) creaGrafoAzar(ent, doble, ent, ent) dimeArista(vertice, vertice) : arista ponPeso(arista, peso) 1 TAD 1 * RadixHeap Corrector Corrector2 ejecuta() damePadres() dameDistancias() dameDestino() ejecuta() damePadres() dameDistancias() dameDestino() ejecuta() damePadres() dameDistancias() dameDestino() 46 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Con este diseño conceptual se pretende sobretodo dos cosas: • Ofrecer tres partes bien diferenciadas de la que partir: Grafo, Algoritmo de camino mínimo y Estructura eficiente de datos. Con esto se logra que el usuario pueda ver de un simple vistazo el funcionamiento del sistema: El algoritmo de camino mínimo trabaja sobre un grafo empleando, quizá, estructuras eficientes de datos. A partir de ahí, se podrá profundizar más en los detalles, pero no habrán más partes involucradas en el proceso general. Este modelo permitirá a un futuro desarrollador implementar nuevos algoritmos o estructuras de datos (o incluso otras implementaciones de grafos) basándose en él. Simplemente deberá respetar el formato de los métodos de las clases CamMin y Grafo. • Aislar cada una de las partes que forman el sistema de forma que facilite futuras modificaciones. Una modificación en la implementación de una estructura eficiente de datos o de un algoritmo en concreto nunca significará tener que modificar absolutamente nada en otra estructura o algoritmo. La separación entre ellos es máxima. Incluso se pueden crear otras estructuras e insertarlas en el modelo para que las usen los algoritmos. El proceso no implicará cambiar ninguna línea de código en el resto de clases, excepto quizá en el nombre de la estructura cuando sea llamada pero, si por ejemplo se quiere sustituir una de las implementaciones de un TAD por otra que parezca ser más eficiente, no se modificará nada del código de los algoritmos. 47 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 5.1.5. Clases del dominio Clase Grafo: Implementa el grafo donde se buscarán los caminos mínimos. Proporciona métodos para su creación mediante funciones básicas, lectura de ficheros con formato específico o generadores aleatorios, así como métodos para la navegación. Clase CamMin: Clase de partida para las distintas implementaciones de los algoritmos de búsqueda de caminos mínimos. Su única función es proporcionar una interficie al usuario con los métodos para inicializar el algoritmo, ejecutarlo y recuperar los datos de la solución. Clase TAD: Clase de partida para las distintas implementaciones de la estructuras eficientes que emplearán los algoritmos de búsqueda de caminos mínimos. Su función es la de proporcionar una interficie común a todas las estructuras de datos de cara a los algoritmos. Clases Dijkstra, Dial, RadixHeap, Corrector y Corrector2: Son las implementaciones de los distintos algoritmos. Nunca se interactuará directamente con ellas, sino a través de la clase más general CamMin. Algunas de estas clases necesitan obligatoriamente usar estructuras de datos específicas. Otras, pueden ir asociadas a alguna de las estructuras de datos eficientes y, por tanto, implementan las versiones de los algoritmos de este proyecto. Clase Balde: Implementa la estructura Balde, utilizada por el algoritmo de Dial. Clase Radix: Implementa la parte del algoritmo de RadixHeap dedicada a la interacción con las estructuras de datos eficientes. Clases DLista, DHeap y FibHeap: Son implementaciones de las estructuras de datos eficientes que pueden emplear algunos de los algoritmos. 48 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 5.1.6. Casos de Uso Figura 5 El usuario de la librería únicamente puede interactuar con el grafo y con los algoritmos, a través de la superclase CamMin. Es cierto que los métodos de las demás clases son públicos, pero no tiene sentido llamarlos directamente en un uso normal del sistema, esto es, crear u obtener un grafo y buscar los caminos mínimos en él. 5.1.7. Diagrama de secuencia A continuación se mostrará el diagrama de secuencia para el caso de uso más general y frecuente: el de la creación de un grafo y ejecución de uno de los algoritmos para encontrar caminos mínimos. 49 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. G: Grafo : Dijkstra crear leeGrafo(fichero) Crear(G, origen, destino, padres, distancias) Ejecuta() n m : FibHeap esVacio?() borraMínimo() damePrimSale(i) a i dameSigSale(a) damePeso(a) dameDestino(a) j Inserta(j) dameDistancias() damePadres() Figura 6. Diagrama de secuencia general del sistema. 50 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 5.1.4. Pseudo código de las operaciones A continuación se muestra a modo de ejemplo el pseudocódigo de algunas de las clases más representativas del sistema. Grafo grafo (entero NN, entero MN) { N = NN; M = MM + 1; ini_llegan = vector vacío de aristas[N+1]; ini_salen = vector vacío de aristas[N+1]; aristas = vector vacío de aristaTAD[M+1]; n = N; m = 0; C = 0; para todos los vértices u del grafo hacer ini_salen[u] = ini_llegan[u] = nulo; } arista damePrimSale(vertice v) devuelve ini_salen[v]; arista damePrimLlega(vertice v) devuelve ini_llegan[v]; arista dameSigSale(arista a) devuelve aristas[a].sig_o; arista dameSigLlega(arista a) devuelve aristas[a].sig_d; vertice dameDestino(arista a) devuelve aristas[a].d; vertice dameOrigen(arista a) devuelve aristas[a].o; vertice damePeso(arista a) devuelve aristas[a].p; entero creaArista(vertice u, vertice v, peso p) { crea un aristaTAD con número asociado m +1 insertando la información proporcionada por los parámetros. m = m +1; si p > C entonces C = p; se actualiza la información de los vectores ini_salen e ini_llegan } booleano leeGrafo(fichero) { comprueba que el formato del grafo sea el correcto. inicializa las estructuras del grafo. mientras no se llegue a final de fichero hacer { leer arista e introducirla en el vector aristas 51 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. actualizar C si hace falta } actualizar los vectores ini_salen e ini_llegan y la información relativa a las listas de adyacencias devolver cierto si no ha habido error. } grabaGrafo(fichero) { para cada arista del grafo hacer insertar el origen, destino y peso de la arista en el fichero con el formato determinado } creaGrafoAzar(entero NN, doble p, entero costmax, entero separacion) { genera un grafo dirigido al azar de NN vértices con probabilidad de arista p. Esto se consigue creando aristas sólo desde vértices con número inferior hasta vértices con número superior. Las aristas se restringen para que sólo puedan unir vértices cuyos índices están como mucho a la separación indicada. Genera los pesos de las aristas uniformemente en el intérvalo [1, costmax]. } arista dimeArista(vertice o, vertice d) { arista sale = damePrimSale(o); mientras dameDestino(sale) diferente a d hacer sale = dameSigSale(sale); si sale igual a nulo devuelve 0 (no hay arista entre los vértices). si no devuelve sale (devuelve la arista que hay entre los dos vértices). } ponPeso(arista a, peso p) aristas[a].p = p; 52 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. CamMin CamMin(grafo graf, vertice o, vertice d, vertice pad[], entero dst[]) { G = graf; orig = 0; dest = d; padres = pad; dist = dst; } CamMin(grafo graf, vertice o, vertice pad[], entero dst[]) { G = graf; orig = 0; dest = -1; padres = pad; dist = dst; } ejecuta() { si dest igual a 1 entonces ejecuta1(); si no entonces ejecuta2(); } vertice* damePadres() devuelve padres; entero* dameDistancias() devuelve dist; vertice dameDestino() devuelve dest; 53 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Dijkstra ejecuta1() { vertice i,j; arista a; entero d = G.n/G.m si d < 2 entonces = 2; TAD V(G.n, d); para cada nodo i del grafo G hacer { padres[i] = nulo; dist[i] = ∞; } padres[orig] = orig; dist[orig] = 0; V.inserta(orig, 0); mientras V esté lleno hacer { i = V.borraMinimo(); si i igual a dest acabar; para toda arista saliente de a hacer { entero peso_a = G.damePeso(a); si peso_a igual a ∞ continuar; j = G.dameDestino(a); si dist[j] > (dist[i] + peso_a) hacer { dist[j] = dist[j] + peso_a; si (dist[j] < 0) entonces dist[j] = ∞; padres[j] = i; si V.esMiembro(j) entonces V.modifica(j, dist[j]); si no entonces V.inserta(j, dist[j]); } } } padres[orig] = nulo; } 54 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. ejecuta2() { vertice i, j, k; vertice h[G.n + 1]; int d2[G.n + 1]; arista a; int D = G.n/G.m; si (D < 2) entonces D = 2; TAD V(G.n, D); TAD VV(G.n, D); int permanentes[G.n + 1]; para cada nodo i del grafo hacer { h[i] = padres[i] = nulo; dist[i] = d2[i] = ∞; permanentes[i] = 0; } padres[orig] = orig; h[dest] = dest; dist[orig] = 0; d2[dest] = 0; V.inserta(orig, 0); VV.inserta(dest, 0); booleano fin = falso; int mini = 0; int minj = 0; int dist_min_dir = ∞; int dist_min_rev = ∞; int compara; mientras !fin hacer { si (mini igual a 0) entonces mini = V.dameMinimo(); si (minj igual a 0) entonces minj = VV.dameMinimo(); si (dist[mini] <= d2[minj]) entonces { i = mini; si (dist[mini] >= dist_min_dir) entonces { fin = cierto; k = dest; continua; } si (permanentes[i]) entonces { fin = cierto; k = i; continua; } si no entonces permanentes[i] = 1; si (fin igual a cierto) entonces continua; V.borra(i); para cada arista que sale de i hacer { entero peso_a = G.damePeso(a); si (peso_a igual a ∞) entonces continua; j = G.dameDestino(a); compara = dist[i] + peso_a; si (dist[j] > compara y compara >= 0) entonces 55 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. { dist[j] = compara; si (dist[j] < 0) entonces dist[j] = ∞; padres[j] = i; si (j == dest) entonces dist_min_dir = dist[j]; si V.esMiembro(j) entonces V.modifica(j, dist[j]); si no entonces V.inserta(j, dist[j]); } } mini = 0; } si no entonces { j = minj; si (d2[j] > dist_min_rev) entonces { fin = cierto; k = orig; continua; } si (permanentes[j] > 0) entonces { fin = cierto; k = j; continua; } si no entonces permanentes[j] = 2; si (fin == cierto) continua; VV.borra(j); para cada arista que llega a j hacer { entero peso_a = G.damePeso(a); si (peso_a igual a ∞) entonces continua; i = G.dameOrigen(a); compara = d2[j] + peso_a; si (d2[i] > compara y compara > 0) entonces { d2[i] = compara; si (d2[i] < 0) entonces d2[i] = ∞; h[i] = j; si (i igual a orig) entonces dist_min_rev = d2[i]; si VV.esMiembro(i) entonces VV.modifica(i, d2[i]); si no entonces VV.inserta(i, d2[i]); } } minj = 0; } construir la solución a partir de las soluciones parciales hacia delante y hacia atrás. } 56 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 5.2. Herramientas y base teórica 5.2.1 Programación orientada a objetos En el desarrollo de la aplicación que implementará la librería de caminos mínimos, se utiliza el método de programación orientada a objetos. Ésta se basa en los conceptos de objeto y clase. Una definición para un objeto puede ser “unidad atómica formada por la unión de un estado y de un comportamiento”. El objeto permite encapsulación, proporcionando una cohesión interna muy fuerte y un débil acoplamiento con el exterior. En este contexto, un objeto informático define una representación de las entidades de un mundo real o virtual, con el objetivo de controlarlos o simularlos. Todo objeto presenta las tres características siguientes: un estado, un comportamiento y una identidad. • Estado: Es la agrupación de los valores instantáneos de todos los atributos de un objeto, sabiendo que un atributo es una información que cualifica al objeto que la contiene. • Comportamiento: El comportamiento agrupa todas las competencias de un objeto y describe las acciones y reacciones de ese objeto. Cada átomo de comportamiento se llama operación. • Identidad: La identidad permite distinguir los objetos de forma no ambigua, independientemente de su estado. Esto permite distinguir dos objetos en los que todos los valores de atributos son idénticos. Una vez definido el concepto de objeto, se define el concepto de clase. Una clase es a un objeto como un tipo de datos es a una variable. En terminología de programación orientada a objetos se dice que un objeto es una instancia de una clase. Este tipo de programación ofrece una serie de ventajas respecto a la tradicional: • Es la máxima expresión de la abstracción de los lenguajes imperativos. • Permite una mayor modularidad del código, debido a que está claramente diferenciada la visión externa de un objeto, de la interna. La visión externa informa de los mensajes que es capaz de mandar y de recibir y de los cambios de estado que ese producen en el objeto en cuestión. Debido a que para la utilización de un objeto no hay que tener conocimientos de la visión interna ésta se puede cambiar sin afectar al sistema, con lo que la modularidad se hace patente. Esta característica se hace posible con la encapsulación de los diferentes objetos, de modo que todo acceso a dicho objeto sólo se pueda hacer a través de los mensajes y métodos definidos como públicos. 57 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. • La herencia permite el acceso automático a la información contenida por otras clases. Con la herencia todas las clases están clasificadas en una jerarquía estricta. Cada clase tiene una superclase, la clases superior en la jerarquía, y cada clases puede tener una o más subclases, las clases inferiores en la jerarquía. • El polimorfismo es la característica que permite implementar múltiples formas de un mismo método, dependiendo cada una de ellas de la clase sobre la que se realice la implementación. Esto hace que se pueda acceder a una variedad de métodos distintos utilizando exactamente el mismo medio de acceso. 5.2.2. C++ Por sus características y su adecuación a los objetivos, C++ ha sido el lenguaje de programación seleccionado para el desarrollo de la librería de caminos mínimos. C++ es un derivado del lenguaje C. Este lenguaje apareció en los años 70 de la mano de Dennos Ritchie para la programación en sistemas Unix, el cual surgió como un lenguaje generalista recomendado sobre todo para programadores ya expertos, pues no llevaba implementadas muchas funciones que hacen a un lenguaje más comprensible. Esto, sin embargo, permite un mayor control sobre lo que se está haciendo. C++ se creó unos años más tarde de la mano de Bjarne Stroustrup. Necesitaba de ciertas facilidades de programación, incluidas en otros lenguajes pero que C no soportaba, al menos directamente, como llamadas a clases y objetos, muy de moda por aquel entonces. Rediseñó el C, ampliando sus posibilidades pero manteniendo su mayor cualidad, la de permitir al programador en todo momento tener controlado lo que está haciendo, consiguiendo así una mayor rapidez que no se conseguiría con otros lenguajes. El sistema se desarrollará en este lenguaje. Las razones más importantes son: • Velocidad. C y C++ son dos de los lenguajes que generan código más veloz y eficiente. • Orientación a objetos. C++ permite programar con objetos, con todas las ventajas que ello conlleva anteriormente expuestas. • Integración. C++ resulta muy sencillo de integrar en tras aplicaciones aún cuando éstas no hayan sido desarrolladas en este lenguaje. Además, puede hacerse en cualquier plataforma. 58 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 6. Manual de Uso 6.1 Introducción La librería se compone del fichero libdijkstra.a y un conjunto de ficheros cabecera .h. Todas las implementaciones vienen representadas por una clase que, a su vez, deriva de una genérica CamMin. Así, el uso de ellas es idéntico para cada una. En la siguiente tabla se incluyen los nueve algoritmos, la clase que los implementa y las cabeceras necesarias a incluir en el programa que los use: Dijkstra básico. Dijkstra<DLista> #include “dijkstra.h” #include “dlista.h” Dijkstra con d-heaps. Dijkstra<DHeap> #include “dijkstra.h” #include “dheap.h” Dijkstra con fibonacci heaps. Dijkstra<FibHeap> #include “dijkstra.h” #include “fibo.h” Dial. Dial #include “dial.h” Radix básico. RadixHeap<Radix> #include “radixheap.h” #include “radix.h” Radix implementado con d-heaps. RadixHeap<RadixDHeap> #include “radixheap.h” #include “radixdheap.h” Radix implementado con fibonacci RadixFib<RadixFib> heaps. #include “radixheap.h” #include “radixfib.h” Corrector de etiqueta básico. Corrector #include “corrector.h” Corrector de etiqueta con “dequeue”. Corrector2 #include “corrector2.h” Las clases Dijkstra y RadixHeap usan una plantilla para ir acompañadas de la clase que implementa las estructuras de datos que utiliza. 59 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 6.2. Procedimiento para ejecutar un algoritmo de la librería Escoger la versión o algoritmo que se desee emplear. Anotar, según la tabla anterior, qué clase o clases son necesarias así como los ficheros de cabecera a incluir. Ejemplo: Queremos usar la versión Dijkstra con Heap de Fibonacci. Mirando en la tabla vemos que emplearemos la clase Dijkstra<FibHeap> y deberemos incluir los ficheros cabecera “dijkstra.h” y “fibo.h”. En el programa que vaya a utilizar la librería, incluir los ficheros de cabecera necesarios. Ejemplo: “include dijkstra.h” e “include fibo.h”. Crear el grafo donde se necesita buscar el/los camino/s mínimo/s. Para ello se pueden usar las funciones y métodos de la clase Grafo (será necesaria la inclusión también de la cabecera correspondiente). Uno de los métodos de esta clase permite leer un grafo completamente desde un fichero de texto con un formato predeterminado. Para más información véase creación del grafo. Grafo nombre_de_grafo(int número_de_nodos, int número_de_aristas); Ejemplo: Grafo migrafo(100, 300); Crear las variables solución: int* padres = new int[número_de_nodos + 1]; int* distancias = new int[número_de_aristas + 1]; Ejemplo: int* padres = new int[101]; int* distancias = new int[301]; 60 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Al acabar la ejecución del algoritmo, padres será el árbol de recubrimiento del grafo con raíz el nodo origen indicado o, para la versión bidireccional del algoritmo, será el camino desde origen hasta destino. El índice del vector representa el nodo del grafo y el valor que contiene, el nodo predecesor, ya sea en el árbol de recubrimiento o en el camino directo. Ejemplo: Reconstrucción del camino desde 1 hasta 7. 0 2 1 1 2 3 3 3 4 5 5 6 6 padre 7 nodo 4 4 2 1 3 2 1 2 5 6 3 7 2 Figura 7. Reconstrucción de la solución del problema. Similar a padres, distancias contendrá, para un nodo dado (índice del vector), la distancia al origen en el árbol de recubrimiento o camino directo según modo. 0 1 1 2 2 3 6 4 4 5 6 6 9 7 Figura 8. Vector solución de distancias. 61 distancia nodo Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Crear un nuevo objeto de la clase que implementa el algoritmo escogido. En este punto hay que tener decidido, si cabe, si se requiere el modo unidireccional o el bidireccional. En el modo unidireccional será necesario conocer el vértice origen desde donde se quieren encontrar los caminos mínimos. En el modo bidireccional habrá que conocerse la pareja de nodos origen-destino. 6. 2. 1. Unidireccional: Clase<TAD> nombre_objeto(*nombre_de_grafo, int nodo_origen, padres, distancias); Ejemplo: Dijkstra<FibHeap> mialgoritmo(*migrafo, 3, padres, distancias); 6. 2. 2. Bidireccional: Clase<TAD> nombre_objeto(*nombre_de_grafo, int nodo_origen, int nodo_destino, padres, distancias); Ejemplo: Corrector mialgoritmo2(migrafo, 3, 6, padres, distancias); Finalmente: nombre_objeto.ejecutar(); Ejemplo: mialgoritmo.ejecutar(); mialgoritmo.damePadres(); mialgoritmo.dameDistancias(); Las variables padres y distancias contendrán la solución al problema. 62 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 6. 3. Creación del grafo Existen dos formas para crear el grafo donde se buscará el camino mínimo. La más sencilla es usar la función leeGrafo(FILE*) que lee el grafo desde un fichero de texto simple. El formato del grafo es el siguiente: nº nodos nº aristas arista1 arista2 ... aristaM El formato de arista es: (nodo_origen,nodo destino,peso) Ejemplo: 6 9 (1,3,4) (1,2,6) (2,3,2) (2,4,2) (3,5,2) (3,4,1) (4,6,7) (5,4,1) (5,6,3) Si la aplicación donde vamos a usar los algoritmos de caminos mínimos genera el grafo de forma dinámica, habrá que crearlo paso a paso con las funciones de la clase Grafo. 63 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 6.4. Clases y métodos públicos 6.4.1. Grafo • grafo(num_nodos, num_aristas); Crea un grafo con num_nodos nodos y num_aristas aristas. • int n; Devuelve el número de nodos del grafo. • int m; Devuelve el número de aristas del grafo. • int C; Devuelve el peso de la arista con peso máximo del grafo. • arista damePrimSale(vertice v); Devuelve la primera arista que sale del vértice v. • arista damePrimLlega(vertice v); Devuelve la primera arista que llega al vértice v. • arista dameSigSale(arista a); Devuelve la siguiente arista que sale del origen de la arista a. • arista dameSigLlega(arista a); Devuelve la siguiente arista que llega al destino de la arista a. • vertice dameDestino(arista a); Devuelve el destino de la arista a. • vertice dameOrigen(arista a); Devuelve el origen de la arista a. • peso damePeso(arista a); Devuelve el peso de la arista a. • int creaArista(vertice o, vertice d, peso p); Crea una arista (o, d) de peso p. Devuelve el número de aristas del grafo. 64 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. • bit leeGrafo(FILE* f); Lee un grafo del fichero indicado. • void grabaGrafo(FILE* f); Graba un grafo en el fichero indicado. • void creaGrafoAzar(int NN, double p, int costmax, int separacion); Genera un grafo dirigido al azar de NN vértices con probabilidad de arista p. Esto se consigue creando aristas sólo desde vértices con número inferior hasta vértices con número superior. Las aristas se restringen para que sólo puedan unir vértices cuyos índices están como mucho a la separación indicada. Genera los pesos de las aristas uniformemente en el intérvalo [1, costmax]. • void creaGrafoCompleto(int NN, int costmax, int prob); Similar a creaGrafoAzar pero el grafo resultante es conexo. • arista dimeArista(vertice o, vertice d); Devuelve la arista que une los dos vértices. Esta función no tiene necesariamente coste de ejecución constante. • void ponPeso(arista a, peso p); Pone el peso p a la arista a. • void ponPeso2(arista a, peso p); Pone peso a la arista a y a su siguiente (para mejorar eficiencia en grafos bidireccionales). • void ponPesos(peso *pesos) Pone los pesos indicado en el vector a los pesos de las aristas del grafo. El índice del vector equivale al número de arista. Si no se quiere modificar el peso de alguna se indica poniendo el valor –1 en la posición del vector correspondiente. 65 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 6.4.2. CamMin • CamMin(grafo& grafo, vertice o, vertice dt, vertice* p, int* d) Versión bidireccional del algoritmo. Crea e inicializa el problema del camino mínimo que se quiere buscar en grafo, con vértice de origen o y vértice destino dt. En los vectores p y d se guardará la solución del problema. p será el árbol de recubrimiento de la solución y d las distancias mínimas al origen para cada vértice del grafo. • CamMin(grafo& grafo, vertice o, vertice* p, int* d) Versión normal del algoritmo. Crea e inicializa el problema del camino mínimo que se quiere buscar en grafo, con vértice de origen o. En los vectores p y d se guardará la solución del problema. p será el árbol de recubrimiento de la solución y d las distancias mínimas al origen para cada vértice del grafo. • void ejecuta() Ejecuta el algoritmo. • vertice* damePadres() Devuelve el árbol de recubrimiento de la solución del problema. • int* dameDistancias() Devuelve las distancias mínimas al nodo origen para cada vértice del grafo. • vertice dameDestino() Devuelve el nodo destino indicado en la inicialización del algoritmo. Si se está trabajando con la versión normal y, por tanto, no hay nodo destino, devuelve -1. 66 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7. Resultados 7.1. Preparativos Una vez desarrollada e implementada la librería, se procede a probar sus resultados. El primer punto principal a comprobar es que las salidas que proporcionan los algoritmos sean correctas. Este proceso puede implicar una consiguiente depuración del código para detectar posibles fallos, en mayor parte de implementación. Una vez se tenga el código depurado de errores, se realizan pruebas exhaustivas ejecutando los algoritmos en multitud de grafos insertando todo tipo de variables. La primera observación de que todo va por buen camino es que todos los algoritmos den la misma solución. De todas formas, hay que garantizar de algún modo que la solución sea correcta. Debido a la enorme dificultad de encontrar “a mano” el camino mínimo en un grafo de tamaño considerable, se trabajó con varias implementaciones del algoritmo básico de Dijkstra encontradas en Internet. Estas implementaciones, mayoritariamente applets en java, sirvieron para poder comparar soluciones reales con las de la librería desarrollada. Después de asegurarse de la correcta funcionalidad de los algoritmos implementados en la librería, queda el punto de la eficiencia. Como se expuso anteriormente, cada algoritmo se comporta teóricamente de forma distinta según el grafo de entrada pero hace falta conocer cómo es la eficiencia de cada uno en casos reales. Para estudiar la eficiencia del algoritmo se creó un pequeño programa cuyo código se muestra a continuación: int main(int argc, char* argv[]) { int n = atoi(argv[1]); int C = atoi(argv[2]); int prob = atoi(argv[3]); int version = atoi(argv[4]); 67 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Grafo G; G.creaGrafoCompleto(n,C,prob); vertice* p = new vertice[G.n+1]; int* d = new int[G.n+1]; int destino, origen; origen = 1; destino = -1; int i; printf(“GRAFO: %d %d %d\n”,G.n,G.m,G.C); if (version == 1) { Dijkstra<DLista> D(G,origen,p,d); for(i = 1;i<=1000;i++) D. ejecuta(); exit(0); } … Esta aplicación recibe como parámetros de entrada un tamaño de grafo, una densidad de aristas, un peso máximo de arista y la versión con la que se quiera que se resuelva. Crea un grafo con las características indicadas y resuelve el problema del camino mínimo versión un origen/todos los destinos con el algoritmo indicado. Mediante un sencillo fichero script se automatiza la tarea de ejecutar este programa multitud de veces con todas las combinaciones posibles de variables, analizando el tiempo necesario de ejecución para cada una (en realidad una media de varias muestras). 68 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.2. Resultados: D T C DIJK DHEAP FIBO DIAL RHEAP RDHEA RDFIB CORR CORR2 100 3000 1000 100,00 100,00 110,00 200,00 110,00 110,00 130,00 220,00 230,00 100 3000 100 100,00 100,00 100,00 120,00 100,00 110,00 120,00 110,00 110,00 100 3000 10 90,00 100,00 100,00 110,00 100,00 100,00 110,00 80,00 80,00 100 3000 2 90,00 100,00 100,00 110,00 100,00 100,00 100,00 80,00 80,00 100 1000 1000 11,12 11,67 13,40 14,71 12,05 12,82 18,78 23,45 22,74 100 1000 100 10,60 11,51 12,75 13,02 11,13 12,11 15,39 15,55 15,54 100 1000 10 10,34 11,02 11,82 11,92 10,73 10,55 13,49 9,38 9,54 100 1000 2 10,52 10,89 11,15 12,47 9,60 9,80 11,23 8,93 9,19 100 500 1000 3,42 3,71 4,54 5,61 4,07 4,33 7,09 8,04 8,34 100 500 100 3,29 3,42 4,17 4,19 4,04 4,12 6,22 6,73 6,76 100 500 10 3,23 3,77 3,88 4,31 3,89 3,84 4,90 3,20 3,23 100 500 2 3,39 3,48 3,76 3,86 3,51 3,81 3,56 2,96 4,12 100 100 1000 0,11 0,13 0,19 0,21 0,23 0,25 0,47 0,21 0,24 100 100 100 0,12 0,15 0,19 0,05 0,16 0,19 0,44 0,15 0,17 100 100 10 0,12 0,14 0,16 0,19 0,15 0,20 0,27 0,13 0,12 100 100 2 0,10 0,12 0,16 0,10 0,13 0,18 0,30 0,08 0,10 100 10 1000 0,01 0,01 0,01 0,05 0,01 0,01 0,03 0,01 0,01 100 10 100 0,01 0,01 0,01 0,03 0,01 0,01 0,02 0,01 0,01 100 10 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 100 10 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 100 5 1000 0,01 0,01 0,01 0,11 0,02 0,01 0,01 0,01 0,01 100 5 100 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 0,01 100 5 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 100 5 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 160,00 75 3000 1000 80,00 70,00 80,00 170,00 80,00 90,00 100,00 150,00 75 3000 100 80,00 80,00 80,00 100,00 80,00 80,00 90,00 90,00 90,00 75 3000 10 80,00 70,00 80,00 90,00 80,00 80,00 90,00 70,00 70,00 75 3000 2 80,00 70,00 70,00 80,00 80,00 70,00 80,00 60,00 60,00 75 1000 1000 8,76 9,05 10,64 11,40 9,26 10,10 15,05 21,35 21,02 75 1000 100 8,69 8,67 10,21 9,90 8,56 9,24 13,40 11,63 11,34 75 1000 10 8,97 8,84 9,96 10,15 8,91 8,57 10,58 7,45 7,43 75 1000 2 8,62 8,83 8,83 9,66 7,39 6,07 9,77 7,65 7,42 75 500 1000 2,57 3,19 3,56 3,84 3,30 3,87 7,01 6,19 6,06 75 500 100 2,51 2,65 3,21 2,85 2,82 3,24 5,02 4,00 4,04 75 500 10 2,48 2,64 2,92 3,19 2,65 2,93 3,84 2,37 2,28 75 500 2 2,48 2,39 2,65 2,90 2,66 2,65 3,13 2,08 2,05 75 100 1000 0,10 0,14 0,22 0,12 0,16 0,29 0,47 0,14 0,18 75 100 100 0,09 0,13 0,17 0,06 0,17 0,26 0,40 0,20 0,15 75 100 10 0,08 0,13 0,17 0,12 0,12 0,16 0,27 0,09 0,08 75 100 2 0,09 0,07 0,09 0,11 0,10 0,13 0,19 0,08 0,04 75 10 1000 0,01 0,01 0,01 0,03 0,01 0,01 0,01 0,01 0,01 69 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 75 10 100 0,01 0,01 0,01 0,02 0,01 0,01 0,03 0,01 0,01 75 10 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 75 10 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 75 5 1000 0,01 0,01 0,01 0,11 0,01 0,01 0,01 0,01 0,01 75 5 100 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 0,01 75 5 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 75 5 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 120,00 50 3000 1000 60,00 54,75 60,00 146,30 57,72 62,05 78,73 128,88 50 3000 100 60,75 50,87 54,88 70,54 55,23 56,79 69,31 62,39 65,48 50 3000 10 59,37 48,86 52,05 57,83 52,84 53,54 60,00 40,00 44,91 50 3000 2 60,00 49,15 49,87 56,29 50,80 50,67 54,73 41,05 41,05 50 1000 1000 6,55 6,41 7,78 8,67 6,14 7,75 12,45 14,63 14,84 50 1000 100 6,65 6,25 8,05 7,04 5,46 4,48 10,46 9,14 8,98 50 1000 10 6,52 6,23 6,76 5,95 5,50 5,51 9,22 5,53 5,95 50 1000 2 6,39 5,67 6,32 6,78 4,90 5,19 6,98 4,85 5,01 50 500 1000 1,98 2,15 3,12 3,25 2,88 2,87 4,50 4,43 4,56 50 500 100 1,86 1,81 2,36 2,13 2,23 2,69 4,27 3,06 3,16 50 500 10 1,92 1,85 2,16 2,12 1,91 2,10 3,20 1,66 1,73 50 500 2 1,88 1,67 1,94 2,00 1,87 1,96 2,15 1,45 1,47 50 100 1000 0,06 0,07 0,18 0,18 0,20 0,24 0,41 0,14 0,14 50 100 100 0,06 0,10 0,18 0,08 0,14 0,24 0,37 0,20 0,12 50 100 10 0,04 0,10 0,15 0,11 0,11 0,11 0,29 0,06 0,07 50 100 2 0,06 0,04 0,14 0,11 0,09 0,09 0,13 0,03 0,03 50 10 1000 0,01 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 50 10 100 0,01 0,01 0,01 0,04 0,01 0,01 0,01 0,01 0,01 50 10 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 50 10 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 50 5 1000 0,01 0,01 0,01 0,07 0,01 0,01 0,01 0,01 0,01 50 5 100 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 50 5 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 50 5 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 25 3000 1000 44,33 29,29 33,89 113,55 34,35 37,51 54,38 72,13 71,10 25 3000 100 42,82 27,70 31,87 43,78 31,34 33,92 45,95 36,59 37,46 25 3000 10 43,04 26,17 29,06 30,43 28,58 28,79 36,46 24,28 24,65 25 3000 2 42,27 25,21 27,36 29,14 26,95 26,42 32,77 21,85 21,87 25 1000 1000 4,73 4,79 6,01 5,75 3,74 4,71 9,02 8,57 8,54 25 1000 100 4,56 4,11 5,07 5,37 4,02 5,11 9,27 6,46 6,56 25 1000 10 4,84 4,14 4,70 4,01 3,84 3,96 7,05 3,42 3,37 25 1000 2 4,55 3,49 4,04 4,20 3,91 3,61 5,77 3,18 3,10 25 500 1000 1,28 1,39 2,13 2,03 1,60 2,09 3,73 2,47 2,69 25 500 100 1,29 1,32 2,09 1,96 1,99 2,46 3,93 2,85 2,46 25 500 10 1,51 1,23 2,65 2,40 1,85 2,02 2,38 1,21 0,99 25 500 2 1,17 1,25 1,29 1,25 1,35 1,25 1,47 0,96 0,90 25 100 1000 0,05 0,05 0,11 0,13 0,15 0,24 0,35 0,09 0,10 25 100 100 0,04 0,05 0,13 0,01 0,12 0,19 0,26 0,07 0,04 70 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 25 100 10 0,05 0,03 0,13 0,07 0,07 0,12 0,22 0,05 0,03 25 100 2 0,03 0,06 0,06 0,05 0,09 0,06 0,14 0,04 0,03 25 10 1000 0,01 0,01 0,01 0,03 0,01 0,01 0,01 0,01 0,01 25 10 100 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 25 10 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 25 10 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 25 5 1000 0,01 0,01 0,01 0,15 0,01 0,01 0,01 0,01 0,01 25 5 100 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 0,01 25 5 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 25 5 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 3000 1000 34,66 14,98 19,87 98,93 19,78 22,14 36,77 37,12 37,09 10 3000 100 34,65 14,09 18,16 26,79 18,20 19,18 32,14 20,74 20,72 10 3000 10 34,14 13,11 15,84 15,75 15,05 15,56 22,45 11,58 12,02 10 3000 2 35,04 12,48 13,90 14,47 14,01 13,68 18,70 11,06 11,08 10 1000 1000 3,13 2,45 3,90 4,59 3,02 4,55 8,85 4,36 4,49 10 1000 100 3,04 2,26 3,43 3,25 2,44 3,02 7,11 3,64 3,36 10 1000 10 3,78 2,10 3,15 1,92 1,77 1,92 4,71 1,86 1,95 10 1000 2 3,39 1,82 2,53 2,20 2,10 1,85 4,52 1,67 1,92 10 500 1000 0,66 0,82 1,34 1,38 1,16 1,45 3,38 0,89 0,96 10 500 100 0,76 0,68 1,18 0,91 0,92 1,36 2,98 0,99 1,09 10 500 10 0,73 0,63 1,01 0,70 0,86 0,82 1,30 0,51 0,51 10 500 2 0,71 0,49 0,74 0,49 0,64 0,75 1,22 0,38 0,37 10 100 1000 0,04 0,05 0,08 0,07 0,11 0,18 0,42 0,03 0,04 10 100 100 0,02 0,09 0,12 0,01 0,16 0,17 0,31 0,04 0,03 10 100 10 0,03 0,03 0,08 0,02 0,07 0,10 0,15 0,03 0,04 10 100 2 0,04 0,01 0,07 0,06 0,05 0,06 0,10 0,01 0,02 10 10 1000 0,01 0,01 0,01 0,04 0,01 0,01 0,03 0,01 0,01 10 10 100 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 0,01 10 10 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 10 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 5 1000 0,01 0,01 0,01 0,24 0,01 0,01 0,01 0,01 0,01 10 5 100 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 0,01 10 5 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 5 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 Los valores son en milisegundos. 71 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.3 Análisis de los resultados Una vez con los resultados en mano, se procede a un estudio de los mismos agrupando los grafos de entrada por valores comunes y modificando alguno de ellos de forma progresiva. La intención es observar el comportamiento de los algoritmos según la topología de los grafos de entrada. 7.3.1. Grafos completos. T 5 10 100 500 1000 3000 D 100 100 100 100 100 100 C 2 2 2 2 2 2 DIJK DHEAP FIBO DIAL RHEAP RDHEA RDFIB CORR CORR2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,10 0,12 0,16 0,10 0,13 0,18 0,30 0,08 0,10 3,39 3,48 3,76 3,86 3,51 3,81 3,56 2,96 4,12 10,52 10,89 11,15 12,47 9,60 9,80 11,23 8,93 9,19 90,00 100,00 100,00 110,00 100,00 100,00 100,00 80,00 80,00 Grafos completos con coste máximo mínimo. El algoritmo Corrector es el más eficiente para todos los tamaños de grafo de máxima densidad y coste máximo de arista mínimo. T 5 10 100 500 1000 3000 D 100 100 100 100 100 100 C 10 10 10 10 10 10 DIJK DHEAP FIBO DIAL RHEAP RDHEA RDFIB CORR CORR2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,12 0,14 0,16 0,19 0,15 0,20 0,27 0,13 0,12 3,23 3,77 3,88 4,31 3,89 3,84 4,90 3,20 3,23 10,34 11,02 11,82 11,92 10,73 10,55 13,49 9,38 9,54 90,00 100,00 100,00 110,00 100,00 100,00 110,00 80,00 80,00 Grafos completos con coste máximo de arista pequeño. Los dos algoritmos correctores son los que mejor se comportan temporalmente. De los dos parece que el primero es aún más eficiente. T D C DIJK DHEAP FIBO DIAL RHEAP RDHEA RDFIB CORR CORR2 5 100 100 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 0,01 10 100 100 0,01 0,01 0,01 0,03 0,01 0,01 0,02 0,01 0,01 100 100 100 0,12 0,15 0,19 0,05 0,16 0,19 0,44 0,15 0,17 500 100 100 3,29 3,42 4,17 4,19 4,04 4,12 6,22 6,73 6,76 1000 100 100 10,60 11,51 12,75 13,02 11,13 12,11 15,39 15,55 15,54 3000 100 100 100,00 100,00 100,00 120,00 100,00 110,00 120,00 110,00 110,00 Grafos completos con coste máximo de arista elevado. Los algoritmos correctores de etiqueta ya no se muestran tan favorables cuando el coste máximo aumenta. Dijkstra parece ser el más idóneo, aunque para un cierto tamaño de grafo Dial es, con diferencia, el más eficiente. Cuando el tamaño del grafo empieza a ser bastante importante la diferencia entre los algoritmos no es demasiado apreciable. 72 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. T D C DIJK DHEAP FIBO DIAL RHEAP RDHEA RDFIB CORR CORR2 5 100 1000 0,01 0,01 0,01 0,11 0,02 0,01 0,01 0,01 0,01 10 100 1000 0,01 0,01 0,01 0,05 0,01 0,01 0,03 0,01 0,01 100 100 1000 0,11 0,13 0,19 0,21 0,23 0,25 0,47 0,21 0,24 500 100 1000 3,42 3,71 4,54 5,61 4,07 4,33 7,09 8,04 8,34 11,67 13,40 14,71 12,05 12,82 18,78 23,45 22,74 100,00 110,00 200,00 110,00 110,00 130,00 220,00 230,00 1000 100 1000 11,12 3000 100 1000 100,00 Grafos completos con coste máximo de arista muy grande. Cuando el coste máximo de la arista es muy elevado, Dijkstra se convierte en el algoritmo más idóneo de todos. Dial es totalemente ineficiente cuando el grafo es de tamaño muy pequeño, completo y de coste máximo de arista muy elevado. Conclusiones Los algoritmos correctores de etiqueta son los más idóneos para grafos completos con costes máximos de arista muy pequeños. A medida que estos costes máximos aumentan, los algoritmos correctores se vuelven más ineficientes a favor del algoritmo original de Dijkstra, aunque Dial destaca en algún momento de esta transición. 7.3.2. Grafo completo pequeño. Aumentando coste máximo de arista. T 5 5 5 5 D C 100 2 100 10 100 100 100 1000 DIJK 0,01 0,01 0,01 0,01 DHEAP 0,01 0,01 0,01 0,01 FIBO 0,01 0,01 0,01 0,01 DIAL 0,01 0,01 0,02 0,11 RHEAP 0,01 0,01 0,01 0,02 RDHEA RDFIB 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 CORR CORR2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 Dial se comporta muy ineficientemente cuando el coste máximo de arista crece, para grafos completos muy pequeños. T 10 10 10 10 D C 100 2 100 10 100 100 100 1000 DIJK 0,01 0,01 0,01 0,01 DHEAP 0,01 0,01 0,01 0,01 FIBO 0,01 0,01 0,01 0,01 DIAL 0,01 0,01 0,03 0,05 RHEAP 0,01 0,01 0,01 0,01 RDHEA RDFIB 0,01 0,01 0,01 0,01 0,01 0,02 0,01 0,03 CORR CORR2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 Igual que antes, Dial empeora a medida que el valor máximo de arista crece. Los métodos RadixHeap tampoco parecen ser aconsejables. 73 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.3.3. Grafo completo grande. Aumentando coste máximo de arista. C DIJK DHEAP RHEAP RDHEA 3000 100 T D 2 90,00 100,00 100,00 110,00 100,00 100,00 100,00 80,00 80,00 3000 100 10 90,00 100,00 100,00 110,00 100,00 100,00 110,00 80,00 80,00 3000 100 100 3000 100 1000 FIBO DIAL RDFIB CORR CORR2 100,00 100,00 100,00 120,00 100,00 110,00 120,00 110,00 110,00 100,00 100,00 110,00 200,00 110,00 110,00 130,00 220,00 230,00 Los algoritmos correctores son los más idóneos cuando los costes máximos de arista son pequeños pero, a medida que éstos crecen, se vuelven menos eficientes y, al contrario, se ejecutan más velozmente Dijkstra y dHeap. T D C DIJK DHEAP FIBO DIAL RHEAP RDHEA RDFIB CORR CORR2 1000 100 2 10,52 10,89 11,15 12,47 9,60 9,80 11,23 8,93 9,19 1000 100 10 10,34 11,02 11,82 11,92 10,73 10,55 13,49 9,38 9,54 1000 100 100 1000 100 1000 10,60 11,51 12,75 13,02 11,13 12,11 15,39 15,55 15,54 11,12 11,67 13,40 14,71 12,05 12,82 18,78 23,45 22,74 Resultado similar. Correctores para costes máximo de arista pequeños y Dijkstra o dHeap para costes máximos de arista grandes. 7.3.4. Grafo pequeño. Disminuyendo densidad de aristas. T D C DIJK DHEAP FIBO DIAL RHEAP RDHEA RDFIB CORR CORR2 5 10 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 5 25 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 5 50 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 5 75 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 5 100 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 Para grafos de tamaño mínimo los resultados empíricos no proporcionan ningún dato relevante. Grafo de tamaño pequeño con coste máximo de arista mínimo. T D C 10 10 2 DIJK 0,01 DHEAP 0,01 FIBO 0,01 DIAL 0,01 RHEAP 0,01 RDHEA 0,01 RDFIB 0,01 CORR 0,01 CORR2 0,01 10 25 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 50 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 75 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 100 2 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 Tampoco los datos revelan mucha información. Únicamente que parece que el comportamiento de RadixHeap con Fibonacci es algo más ineficiente que el resto. 74 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. T D C 5 10 10 DIJK 0,01 DHEAP 0,01 FIBO 0,01 DIAL 0,01 RHEAP 0,01 RDHEA 0,01 RDFIB 0,01 CORR 0,01 CORR2 0,01 5 25 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 5 50 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 5 75 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 5 100 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 Igual que antes, no se puede apreciar ningún dato significativo. T D C 10 10 10 DIJK 0,01 DHEAP 0,01 FIBO 0,01 DIAL 0,01 RHEAP 0,01 RDHEA 0,01 RDFIB 0,01 CORR 0,01 CORR2 0,01 10 25 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 50 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 75 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 100 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 Ningún dato significativo. Se recomienda aumentar el número de ejecuciones de los algoritmos para obtener resultados más reveladores. 7.3.5. Grafo grande. Disminuyendo densidad de aristas. T D C DIJK FIBO DIAL 3000 10 2 35,04 12,48 13,90 14,47 14,01 13,68 18,70 11,06 11,08 3000 25 2 42,27 25,21 27,36 29,14 26,95 26,42 32,77 21,85 21,87 3000 50 2 60,00 49,15 49,87 56,29 50,80 50,67 54,73 41,05 41,05 3000 75 2 80,00 70,00 70,00 80,00 80,00 70,00 80,00 60,00 60,00 2 90,00 100,00 100,00 110,00 100,00 100,00 100,00 80,00 80,00 3000 100 DHEAP RHEAP RDHEA RDFIB CORR CORR2 Claramente, los métodos correctores son los más eficientes para cualquier densidad del grafo. El resto de algoritmos no modifican su eficiencia considerablemente al modificar la densidad, exceptuando Dijkstra que empeora al disminuirla. T D C 1000 10 2 3,39 1,82 2,53 2,20 2,10 1,85 4,52 1,67 1,92 1000 25 2 4,55 3,49 4,04 4,20 3,91 3,61 5,77 3,18 3,10 1000 50 2 6,39 5,67 6,32 6,78 4,90 5,19 6,98 4,85 5,01 1000 75 2 8,62 8,83 8,83 9,66 7,39 6,07 9,77 7,65 7,42 2 10,52 10,89 11,15 12,47 9,60 9,80 11,23 8,93 9,19 1000 100 DIJK DHEAP FIBO DIAL RHEAP RDHEA RDFIB CORR CORR2 Los métodos correctores se comportan eficientemente en casi todas las densidades de grafo. Cabe destacar, RadixHeap con dHeaps que, para densidades medias y altas es de los más eficientes. Dijkstra y RadixHeap con Fibonacci empeoran considerablemente la eficiencia al disminuir la densidad de aristas. 75 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. T D C DIJK FIBO DIAL 3000 10 10 34,14 13,11 15,84 15,75 15,05 15,56 22,45 11,58 12,02 3000 25 10 43,04 26,17 29,06 30,43 28,58 28,79 36,46 24,28 24,65 3000 50 10 59,37 48,86 52,05 57,83 52,84 53,54 60,00 40,00 44,91 3000 75 10 80,00 70,00 80,00 90,00 80,00 80,00 90,00 70,00 70,00 10 90,00 100,00 100,00 110,00 100,00 100,00 110,00 80,00 80,00 3000 100 DHEAP RHEAP RDHEA RDFIB CORR CORR2 El comportamiento es idéntico al observado con grafos con coste máximo de arista mínimo. T D C 1000 10 10 3,78 2,10 3,15 1,92 1,77 1,92 4,71 1,86 1,95 1000 25 10 4,84 4,14 4,70 4,01 3,84 3,96 7,05 3,42 3,37 1000 50 10 6,52 6,23 6,76 5,95 5,50 5,51 9,22 5,53 5,95 1000 75 10 8,97 8,84 9,96 10,15 8,91 8,57 10,58 7,45 7,43 10 10,34 11,02 11,82 11,92 10,73 10,55 13,49 9,38 9,54 1000 100 DIJK DHEAP FIBO DIAL RHEAP RDHEA RDFIB CORR CORR2 Exactamente igual que con costes máximos de aristas mínimos, aunque ahora RadixHeap original es el más idóneo para densidades medio bajas. 7.3.6. Grafo disperso pequeño. Aumentando coste máximo de arista. T D C 5 10 2 DIJK 0,01 DHEAP 0,01 FIBO 0,01 DIAL 0,01 RHEAP 0,01 RDHEA 0,01 RDFIB 0,01 CORR 0,01 CORR2 0,01 5 10 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 5 10 100 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 0,01 5 10 1000 0,01 0,01 0,01 0,24 0,01 0,01 0,01 0,01 0,01 Dial y los RadixHeap empeoran la eficiencia al aumentar el coste máximo de arista. T D C 5 25 2 DIJK 0,01 DHEAP 0,01 FIBO 0,01 DIAL 0,01 RHEAP 0,01 RDHEA 0,01 RDFIB 0,01 CORR 0,01 CORR2 0,01 5 25 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 5 25 100 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 0,01 5 25 1000 0,01 0,01 0,01 0,15 0,01 0,01 0,01 0,01 0,01 Dial y RadixHeap empeoran la eficiencia al aumentar el coste máximo de arista. T D C 10 10 2 DIJK 0,01 DHEAP 0,01 FIBO 0,01 DIAL 0,01 0,01 0,01 0,01 0,01 0,01 10 10 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 10 100 0,01 0,01 0,01 0,02 0,01 0,01 0,01 0,01 0,01 10 10 1000 0,01 0,01 0,01 0,04 0,01 0,01 0,03 0,01 0,01 76 RHEAP RDHEA RDFIB CORR CORR2 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. T D C 10 25 2 DIJK 0,01 DHEAP 0,01 FIBO 0,01 DIAL 0,01 RHEAP 0,01 RDHEA 0,01 RDFIB 0,01 CORR 0,01 CORR2 0,01 10 25 10 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 25 100 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 0,01 10 25 1000 0,01 0,01 0,01 0,03 0,01 0,01 0,01 0,01 0,01 Al aumentar los costes máximos de arista Dial y RadixHeap proporcionan peores tiempos de ejecución. 7.3.7. Grafo disperso grande. Aumentando coste máximo de arista. T D C DIJK 3000 10 2 35,04 DHEAP 3000 10 10 34,14 13,11 15,84 15,75 15,05 15,56 22,45 11,58 12,02 3000 10 100 34,65 14,09 18,16 26,79 18,20 19,18 32,14 20,74 20,72 3000 10 1000 34,66 14,98 19,87 98,93 19,78 22,14 36,77 37,12 37,09 3000 25 2 42,27 25,21 27,36 29,14 26,95 26,42 32,77 21,85 21,87 3000 25 10 43,04 26,17 29,06 30,43 28,58 28,79 36,46 24,28 24,65 3000 25 100 42,82 27,70 31,87 43,78 31,34 33,92 45,95 36,59 37,46 3000 25 1000 44,33 29,29 33,89 113,55 34,35 37,51 54,38 72,13 71,10 1000 10 2 3,39 1,82 2,53 2,20 2,10 1,85 4,52 1,67 1,92 1000 10 10 3,78 2,10 3,15 1,92 1,77 1,92 4,71 1,86 1,95 1000 10 100 3,04 2,26 3,43 3,25 2,44 3,02 7,11 3,64 3,36 1000 10 1000 3,13 2,45 3,90 4,59 3,02 4,55 8,85 4,36 4,49 1000 25 2 4,55 3,49 4,04 4,20 3,91 3,61 5,77 3,18 3,10 1000 25 10 4,84 4,14 4,70 4,01 3,84 3,96 7,05 3,42 3,37 1000 25 100 4,56 4,11 5,07 5,37 4,02 5,11 9,27 6,46 6,56 1000 25 1000 4,73 4,79 6,01 5,75 3,74 4,71 9,02 8,57 8,54 12,48 FIBO DIAL 13,90 14,47 RHEAP 14,01 RDHEA 13,68 RDFIB CORR 18,70 11,06 CORR2 11,08 Para costes máximos de arista mínimos, los Correctores son los más eficientes. Cuando estos costes aumentan, dHeap y RadixHeap son los más rápidos y, los Correctores, todo lo contrario. Se confirma que Dijkstra es poco eficiente cuando la densidad del grafo es pequeña. 77 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.4. Primeras conclusiones Se pueden sacar varias conclusiones después de este estudio empírico: • Los algoritmos no se comportan la mayoría de las veces tal como se esperaba en el estudio teórico. Algunos sí que siguen un comportamiento previsto pero la mayoría no. • Gran sorpresa con los algoritmos correctores de etiqueta. Poco atractivos eran sus costes temporales teóricos pero en la práctica, tal como anunciaba la bibliografía, se comportan realmente bien, incluso superando a sus hermanos etiquetadores. • Dificultad para distinguir una pauta de comportamiento general. Por ello, se muestra la siguiente tabla, que intenta ofrecer un consejo sobre qué algoritmos usar en cada caso, según el tamaño, densidad y coste máximo de arista del grafo. De todas formas, se recomienda siempre hacer estudios empíricos cuando se utilice la librería con los grafos que se emplearán, siempre que la topología de ellos sea similar. Por ejemplo, puede desconocerse el tamaño que tendrán los grafos donde se buscarán los caminos mínimos pero, si se conoce que los pesos de las aristas obtendrán valores muy elevados, puede descartarse el usar algún algoritmo (Dial en este caso). 78 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.5. Tabla consejo sobre utilización de los algoritmos según parámetros de grafo de entrada. D T C 100 3000 1000 100 3000 100 100 3000 10 100 3000 2 100 1000 1000 100 1000 100 100 1000 10 100 1000 2 100 500 1000 100 500 100 100 500 10 100 500 2 100 100 1000 100 100 100 100 100 10 100 100 2 100 10 1000 100 10 100 100 10 10 100 10 2 100 5 1000 100 5 100 100 5 10 100 5 2 75 3000 1000 75 3000 100 75 3000 10 75 3000 2 75 1000 1000 75 1000 100 75 1000 10 75 1000 2 75 500 1000 75 500 100 75 500 10 75 500 2 75 100 1000 75 100 100 75 100 10 75 100 2 DIJK DHEAP FIBO DIAL 79 RHEAP RDHEA RDFIB CORR DEQUE Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. D T C 75 10 1000 75 10 100 75 10 10 75 10 2 75 5 1000 75 5 100 75 5 10 75 5 2 50 3000 1000 50 3000 100 50 3000 10 50 3000 2 50 1000 1000 50 1000 100 50 1000 10 50 1000 2 50 500 1000 50 500 100 50 500 10 50 500 2 50 100 1000 50 100 100 50 100 10 50 100 2 50 10 1000 50 10 100 50 10 10 50 10 2 50 5 1000 50 5 100 50 5 10 50 5 2 25 3000 1000 25 3000 100 25 3000 10 25 3000 2 25 1000 1000 25 1000 100 25 1000 10 25 1000 2 25 500 1000 25 500 100 25 500 10 25 500 2 DIJK DHEAP FIBO DIAL 80 RHEAP RDHEA RDFIB CORR DEQUE Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. D T C 25 100 1000 25 100 100 25 100 10 25 100 2 25 10 1000 25 10 100 25 10 10 25 10 2 25 5 1000 25 5 100 25 5 10 25 5 2 10 3000 1000 10 3000 100 10 3000 10 10 3000 2 10 1000 1000 10 1000 100 10 1000 10 10 1000 2 10 500 1000 10 500 100 10 500 10 10 500 2 10 100 1000 10 100 100 10 100 10 10 100 2 10 10 1000 10 10 100 10 10 10 10 10 2 10 5 1000 10 5 100 10 5 10 10 5 2 DIJK DHEAP FIBO DIAL 81 RHEAP RDHEA RDFIB CORR DEQUE Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7. Aplicación. 7.1. Introducción A continuación se expondrá la aplicación de la librería desarrollada en un problema real, un problema que, lógicamente, usa de algoritmos de búsqueda de caminos mínimos y en el que se desea minimizar los tiempos de espera. 7.2 Descripción del problema. El problema trata sobre la protección de datos. Este problema surge cuando se quiere proteger información de individuos que puede ser obtenida de tablas estadísticas. Un método que soluciona este problema es el llamado de “eliminación de celda complementaria”. Existen varias técnicas para resolverlo y una de las más ventajosas en la teoría y en la práctica se basa en optimización linear sobre redes o grafos. Aunque aquí no se detallará el procedimiento de este método sí que hay que señalar que en la ejecución del mismo se resuelven gran cantidad de subproblemas de caminos mínimos en grafos, previa creación de los mismos. 7.3 Aplicación real Se dispone de una aplicación real (y de su código fuente en C) que resuelve este problema. El programa dispone de tres métodos para resolver los subproblemas de optimización lineal sobre redes siendo uno de ellos una implementación básica del algoritmo de Dijkstra basado en d-heaps. Se pretende sustituir este módulo por la librería desarrollada. Como los grafos que aparecen en los subproblemas tienen todos una misma topología es fácil pensar a priori que se podrá detectar cuál de todas las versiones de la librería es la más idónea para la aplicación. 82 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.4 Análisis de la aplicación El código de la aplicación no es demasiado intuitivo pues en su mayoría está desarrollado a muy bajo nivel y, además, hay que conocer la teoría sobre este tipo de problemas. De todas formas, con la ayuda de los comentarios, los nombres de las funciones y la organización en diversos ficheros se podrán encontrar las partes que se dedican a la implementación del algoritmo de Dijkstra. El algoritmo de Dijkstra lo implementan los ficheros dijkstra.h y dijkstra.c. En dijkstra.h está la declaración de la estructura del problema: /* Dijkstra data structure */ typedef EXPDLL_DIJKSTRA struct{ int nar; /* number of arcs */ int nnu; /* number of nodes */ int *mnk; /* origin nodes of arcs */ int *mnl; /* destination nodes of arcs */ int *mad; /* arc numbers comming out of each node */ int *ipa; /* position of 1st element of "mad" for each node */ int *mda; /* arc numbers arriving in each node */ int *iap; /* position of 1st element of "mda" for each node */ double *car; /* costs of arcs */ int mor; /* origin node in shortest path */ int mds; /* destination node in shortest path */ int lsp; /* length of shortest path (number of arcs in it) */ int *ksp; /* arcs in shortest path */ double spc; /* shortest path cost */ double infinity; /* infinity value: any cost greater or equal than this value is considered infinity */ } DSP; En ella se puede ver que se guarda tanto los parámetros de entrada (grafo de entrada, nodo origen, nodo destino…) como los parámetros de salida o solución (arcos del camino mínimo, longitud del camino mínimo). El resto de funciones implementan los subprocedimientos a bajo nivel del algoritmo basado en dheaps, a excepción de la función dijdmd que es la que implementa el algoritmo propiamente dicho. En este punto está ya más claro los pasos que hará el programa y que se deberán sustituir por los propios de la librería aquí desarrollada: 1. 2. 3. 4. Creación del grafo. Rellenar los parámetros de entrada de la estructura DSP. Ejecutar el algoritmo de Dijkstra. Extraer los parámetros solución de la estructura DSP. 83 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. El único sitio donde se llama a la función dijdmd es en la otra función llamada csp_SolveNF_Dijkstra(DSP *Dsp): int csp_SolveNF_Dijkstra(DSP *Dsp) /************************************************************************** **/ // Solves the network flow (shorthest path) problem // // Input parameters: // // DSP *Dsp : Dijkstra structure, already updated // // Returns: // 0: if everything was fine // -5: if error in solving shortest path problem /************************************************************************** **/ { /* Declarations */ /* Calling Dijkstra */ if (dijmd(Dsp)) return (-5); return(0); } En el mismo fichero donde está la función csp_SolveNF_Dijkstra se encuentra otra función más llamada csp_IniDijkstra(TABLE2D *tab, NFPro *Ncp, DSP *Dsp), cuya documentación dice lo siguiente: /************************************************************************** **/ // Loads Dsp->mnk, mnl, car of permanent cells and computes the adjacencies // for the Dijkstra algorithm // // Input parameters: // // TABLE2D *tab : 2D table // NFPro *Ncp : NF problem // DSP *Dsp : Dijkstra structure /************************************************************************** **/ Esta función rellena los siguientes campos de la estructura DSP: nar, nnu, infinity, mnk, mnl, car, ipa, iap, mad, mda. Es decir, crea el grafo donde buscar el camino mínimo. Los datos los obtiene a partir del parámetro de entrada *Ncp, que es el problema de protección de datos, y *tab, la tabla a proteger. De todas formas, no debería ser necesario conocer qué representan estos dos parámetros o estructuras, únicamente se deberá crear un equivalente a la estructura DSP con la librería de caminos mínimos. 84 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Esta función de inicialización de la estructura DSP es llamada en otra llamada csp_heur2D_Cox(TABLE2D *tab), sin que tenga más código referente al algoritmo de Dijkstra. Hasta este punto se tienen detectados los siguientes puntos: • • Creación del grafo. Ejecutar el algoritmo de Dijkstra. Para conocer el punto dónde se introduce el nodo origen y el nodo destino del grafo origen donde se quiere buscar el camino mínimo simplemente habrá que encontrar qué código rellena los valores mor y mds de la estructura DSP. Esto se hace en una función llamada csp_set_targetNF(TABLE2D *tab,NFPro *Ncp, DSP *Dsp, int target_i,int_target_j), a partir de valores del parámetro de entrada *Ncp. Finalmente, habrá que detectar la parte de código que recupera la solución después de haber ejecutado el algoritmo de Dijkstra. Los atributos de la estructura DSP referentes a la solución son lsp y *ksp. Éstos se recuperan en la función denominada csp_GetCycleNF. Por tanto, se ha detectado la parte de código que hace referencia a la inicialización, ejecución y recuperación de resultados del algoritmo de Dijkstra. El siguiente paso será estudiar cómo realizar la sustitución de este código por el proporcionado en la librería de caminos mínimos. 85 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.5. Cómo sustituir el código Una primera aproximación a la hora de proceder a la sustitución sería pensar en reemplazar la estructura DSP por otra propia de la librería. Todos los atributos relacionados con el grafo de entrada podrían ser fácilmente eliminados y sustituidos por la clase grafo. Los atributos de nodo origen y destino serían los orig y dest de la clase CamMin y la solución del problema, padres, hijos y dist. Pero esto significaría el tener que modificar muchas partes de código, con lo que el cambio no sería tan transparente como uno desearía. Lo ideal sería sólo modificar la función que ejecuta el algoritmo y no tocar la estructura DSP. Así, las inicializaciones y consulta de resultados permanecerían intocables. A la función dijmd que ejecuta el algoritmo de Dijkstra se le pasa como parámetro la estructura DSP. En esta estructura tenemos todos los datos necesarios para poder ejecutar el algoritmo de caminos mínimos. A partir de estos datos se crearía el grafo y se buscaría el camino mínimo en él dado un nodo origen y un destino. Finalmente, se rellenarían los campos de resultado en DSP. Esta alternativa tiene la ventaja expuesta de concentrar los cambios en un único punto pero la desventaja de la ineficiencia: en este caso, por ejemplo, el grafo se crearía dos veces. La mejor alternativa, por tanto, es una mezcla de las dos. Se intentará que los cambios se realicen en el mínimo número de ficheros pero sin sacrificar eficiencia. 86 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.6 Sustitución del código 7.6.1. Creación del grafo. Originalmente, el grafo se crea en la función csp_IniDijkstra, rellenando los atributos referentes al mismo en la estructura DSP. Como en la librería de caminos mínimos el grafo es en sí mismo una clase propia, se introducirá en la estructura DSP un campo que contenga el grafo y, en vez de introducir valores a los campos que hacían referencia al grafo, se llamarán a las funciones o métodos de la clase. Código inicial: iarc= 0; int inc= 2*N; for(j=0;j<N-1;j++) { Dsp->ipa[M+j]= imad; Dsp->iap[M+j]= imad; for(i=0;i<M-1;i++) { Dsp->mad[imad]= i*inc+iarc+1; Dsp->mda[imad]= i*inc+iarc; imad++; } Dsp->mad[imad]= i*inc+iarc; Dsp->mda[imad]= i*inc+iarc+1; imad++; iarc += 2; } j= N-1; Dsp->ipa[M+j]= imad; Dsp->iap[M+j]= imad; for(i=0;i<M-1;i++) { Dsp->mad[imad]= i*inc+iarc; Dsp->mda[imad]= i*inc+iarc+1; imad++; } Dsp->mad[imad]= (M-1)*inc+iarc+1; Dsp->mda[imad]= (M-1)*inc+iarc; imad++; Dsp->ipa[M+N]= imad; Dsp->iap[M+N]= imad; } void csp_IniDijkstra(TABLE2D *tab, NFPro *Ncp, DSP *Dsp) { int i, j, M, N; M = get_nrows2D(tab); N = get_ncolumns2D(tab); Dsp->nar = Ncp->n; Dsp->nnu = Ncp->m; Dsp->infinity= DBL_MAX/((double)Dsp->nnu); for (i=0; i< Ncp->n; i++){ Dsp->mnk[ i] = Ncp->mnk[ i]; Dsp->mnl[ i] = Ncp->mnl[ i]; if (Ncp->max_flx[ i] == 0.0 ) Dsp->car[ i] = Dsp->infinity; } int imad= 0; int iarc= 0; for(i=0;i<M-1;i++) { Dsp->ipa[i]= imad; Dsp->iap[i]= imad; for(j=0;j<N-1;j++) { Dsp->mad[imad]= iarc; Dsp->mda[imad]= iarc+1; imad++; iarc += 2; } Dsp->mad[imad]= iarc+1; Dsp->mda[imad]= iarc; imad++; iarc += 2; } Dsp->ipa[i]= imad; Dsp->iap[i]= imad; for(j=0;j<N-1;j++) { Dsp->mad[imad]= iarc+1; Dsp->mda[imad]= iarc; imad++; iarc += 2; } Dsp->mad[imad]= iarc; Dsp->mda[imad]= iarc+1; imad++; 87 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Código sustituido: void csp_IniDijkstra(TABLE2D *tab, NFPro *Ncp, DSP *Dsp) { int i, j, M, N; M = get_nrows2D(tab); N = get_ncolumns2D(tab); Dsp->nar = Ncp->n; Dsp->nnu = Ncp->m; Dsp->g = new grafo(Dsp->nnu, Dsp->nar); for (i = 0; i < M - 1; i++) { for (j = M; j <= M + N - 2; j++) { Dsp->g->creaArista(i+1, j+1, BIGINT); Dsp->g->creaArista(j+1, i+1, BIGINT); } Dsp->g->creaArista(j+1, i+1, BIGINT); Dsp->g->creaArista(i+1, j+1, BIGINT); } for (i = M; i <= M + N - 2; i++) { Dsp->g->creaArista(i+1, M, BIGINT); Dsp->g->creaArista(M, i+1, BIGINT); } Dsp->g->creaArista(M, M + N, BIGINT); Dsp->g->creaArista(M + N, M, BIGINT); } En otra parte del código se insertan los pesos de las aristas. El cambio simplemente será hacerlo esta vez con el método ponPeso de la clase grafo. 7.6.2. Nodos origen y destino del problema Los nodos origen y destino se introducen aquí: /* set origin and destination node of shortest path problem */ Dsp->mor = Ncp->mnl[ targetXp]; Dsp->mds = Ncp->mnk[ targetXp]; No se realizará ningún cambio. La función de ejecución del algoritmo ya se encargará de tomar los valores. Nótese que entonces se estarán doblando esfuerzos para conseguir estos parámetros pero, al ser simples asignaciones de valores enteros, la pérdida de eficiencia será despreciable, sobretodo si se compara con la ventaja de no tener que hacer ningún cambio en el código en esta función. 88 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 7.6.3. Ejecución del algoritmo Aquí es donde se realizará el cambio más importante. La función dedicada al algoritmo de Dijkstra es dijmd, a partir de la estructura DSP rellenada. La función implementa el algoritmo a muy bajo nivel. A continuación se muestra un pequeño fragmento de su código: while (meq < nnu && mqe < nnu) { /* CAMI DIRECTE */ j12[mll] = j12[mll] - 2; if (j12[mll] == 0) { /* l'arbre directe i l'arbre invers ja s'han trobat */ aux = etq[mll] + eqt[mll]; /* "aux": fita superior de "spc" */ /* cerca del cami minim */ mar=-1; /* no.d'arc entre "imd" i "idm" que resulta en spc<aux */ /* "imd": es aqui un nus etiquetat de l'arbre directe */ /* "idm": es aqui un nus etiquetat de l'arbre invers */ for (i = 0; i < nnu; i++) { if (j12[i] == 1) { ini=ipa[i]; ifi=ipa[i+1]-1; if (ini <= ifi) { for (j = ini; j <= ifi; j++) { iax = mad[j]; ibx = mnl[iax]; if (j12[ibx] == 2 && etq[i]+car[iax]+eqt[ibx] < aux) { mar = iax; aux = etq[i] + car[mar] + eqt[ibx]; imd = i; idm = ibx; } } } El código se sustituirá casi completamente por llamadas a las clases de la librería. En la aplicación original el algoritmo escogido era Dijkstra con d-heaps. Aquí se permitirá al usuario escoger qué versión de algoritmo de camino mínimo se desea utilizar. Por tanto, habrá que modificar también el código principal para permitirlo. 89 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Un ejemplo del código sería: int dijmd (DSP *ddsps) { extern int version; int ret_stat= 0; int* p = new int[ddsps->g->n+1]; int* d = new int[ddsps->g->n+1]; int k; int num_ar = 0; int hijo = ddsps->mds+1; ddsps->lsp = 0; ddsps->spc = d[ddsps->mds+1]; if (version == 1) { Dijkstra<DLista> Dsp(*ddsps->g,ddsps->mor+1,ddsps->mds+1,p,d); Dsp.ejecuta(); p = Dsp.damePadres(); d = Dsp.dameDistancias(); for (k = p[ddsps->mds+1]; k != ddsps->mor+1; k = p[k]) { ddsps->ksp[num_ar++] = ddsps->g->dimeArista(k, hijo)-1; ddsps->lsp++; hijo = k; } } … Y así con las sucesivas versiones. Se incluye ya la inserción de las soluciones en la estructura DSP. 7.7. Resultados Una vez compilado y depurado el programa con las modificaciones se comprobará empíricamente si el cambio ha mejorado la eficiencia temporal. Los grafos generados son de tamaño elevado y de una densidad de aristas medio-alta. Observando la tabla resumen de la eficiencia de los algoritmos según la topología de los grafos, es difícil decidir cuál de ellos será el más idóneo en esta aplicación. De todas formas, al haber insertado la posibilidad de escoger el método en los parámetros de entrada, no llevará mucho tiempo averiguar cuál de ellos emplear después de unas cuantas pruebas empíricas. Para ello se crearán varios juegos de prueba. Se ejecutarán con la aplicación original y luego con la nueva, comparando los tiempos de ejecución. Para esto se contará con la ayuda de un generador de 90 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. tablas para la aplicación y un script que genera diversas instancias de tablas y ejecuta el programa para resolverlas. El algoritmo que proporciona la mayor eficiencia temporal es el de Dijkstra con d-Heaps, alrededor de un 20% más rápido que el original. Por ello, se modifica el código para que sea el algoritmo empleado por defecto, aunque es posible escoger otro en la propia llamada a la aplicación mediante un parámetro de entrada. La razón más posible de que ninguno de los otros algoritmos con estructuras eficientes sea más ventajoso en esta aplicación puede ser debido a los pesos máximos de las aristas, que toman valores infinitos durante la ejecución del programa. Intentar arreglar este problema seguramente conllevaría modificar el código de la aplicación original en gran medida, algo que sale del objetivo de este proyecto, aunque también es cierto que los algoritmos podrían tener en consideración los pesos con valores infinitos. Estas modificaciones podrían sobrecargar el código y hacerlo más ineficiente. De todas formas es un caso puntual como pueden aparecer más cuando se aplique la librería a otros códigos. El diseño de la misma permitirá al futuro usuario poder realizar modificaciones sencillas para adecuarla a su propio gusto o necesidades. 91 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 8. Apéndice 8.1. Estructuras de datos A continuación se puede encontrar la información relativa a las llamadas estructuras de datos eficientes empleadas en los algoritmos de caminos mínimos. 8.2. d-Heaps Un heap (o cola con prioridades) es una estructura de datos para guardar y manipular eficientemente una colección H de elementos (u objetos) cuando cada uno de estos elementos (e ∈ H) tiene asociado un número real, denominado clave(i). Las operaciones que se requieren poder hacer a los elementos en el heap H son: • crear(H). Crea un heap H vacío. • insertar(e, H). Inserta un elemento e en el heap. • busca-mínimo(e, H). Busca un elemento e con la clave mínima en el heap. • borra-mínimo(e, H). Borra el elemento e con la clave mínima del heap. • borra(e, H). Borra un elemento arbritario e del heap. • decrementa-clave(e, valor, H). Decrementa la clave del elemento e a un valor más pequeño, indicado por valor. • incrementa-clave(e, valor, H). Incrementa la clave del elemento e a un valor más grande, indicado por valor. 8.2.1. Definición y propiedades de un d-Heap En un d-Heap, se guardan los elementos del heap como un árbol enraizado cuyos arcos representan una relación de padre-hijo. El árbol se almacena usando índices de predecesores y conjuntos de sucesores: pred(e): el padre del elemento e en el d-heap. La raíz no tiene predecesor, así que será igual a 0. SUC(e): el conjunto de hijos del elemento e en el d-heap. 92 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. En un d-heap se define la profundidad de un elemento e como el número de arcos en el camino único que va desde la raíz hasta e. Cada elemento en un d-heap tiene como mucho d sucesores, asumiendo que están ordenados de izquierda a derecha. Estos sucesores se llaman hermanos entre sí. El d-heap siempre satisface la siguiente propiedad que se mantiene invariable: Los elementos se han añadido al heap en orden creciente de su profundidad y, para los de misma profundidad, de izquierda a derecha. Esta propiedad se denomina propiedad de contigüidad, que implica los siguiente resultados: (a) Como mucho dk elementos tienen profundidad k. (b) Como mucho (dk + 1 – 1)/(d – 1) nodos tienen profundidad entre 0 y k. (c) La profundidad de un d-heap que contiene n elementos es de como mucho, ⎣logdn⎦. 8.2.2. Almacenamiento de un d-Heap. La estructura de un d-heap permite guardarlo en un vector y manipularlo de forma bastante eficiente. Los elementos se ordenan por su profundidad de forma creciente, y de izquierda a derecha en caso de que ésta sea la misma. Después, se insertan en orden en un vector. Además, se tiene un vector que guarda las posición de cada elemento. Otro parámetro es último, que especifica el número de nodos guardados en el vector. Esta forma de almacenar el d-heap puede parecer a simple vista poco adecuada para manipularlo posteriormente, pero tiene una curiosa propiedad que permitirá acceder a los predecesores y sucesores de cualquier nodo de forma eficiente: (a) El padre de un elemento en la posición i se encuentra en la posición ⎣(i – 1)/d⎦. (b) Los hijos de un elemento en la posición i se encuentran entre el siguiente rango de posiciones: [id – d + 2, ..., id + 1]. Así, esta propiedad implica que no se deberá mantener el índice del padre y el conjunto de hijos para cada elemento. Se podrá calcular cuando sea necesario hacerlo durante la ejecución de un 93 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. algoritmo. Por lo tanto, se ignorará el tiempo requerido para actualizar estas estructuras cuando el dheap cambie. 8.2.3. Propiedad Orden del Heap Un heap siempre satisface el siguiente invariante, llamado propiedad orden del heap: Invariante 1: La clave de un elemento e en el heap es menor o igual que la clave de cada uno de sus sucesores. Es decir que, para cada elemento e, clave(e) ≤ clave(j) para cada j ∈ SUC(e). Nótese que el invariante se puede violar temporalmente cada vez que se efectúa una operación en el heap, pero siempre se cumplirá al final de ésta. Como resultado de esta propiedad, se puede afirmar lo siguiente: La raíz del d-heap siempre tiene la clave más pequeña. 8.2.4. Intercambio En la estructura de datos d-heap, se puede reducir toda operación en una secuencia de operaciones básicas, llamadas intercambiar(i, j). Esta operación intercambia los elementos i y j. En términos del vector donde está guardada la estructura, el elemento i se guarda donde estaba el j, y viceversa. 8.2.5. Recuperar el orden del heap Normalmente, durante la ejecución de un algoritmo, frecuentemente se modificará el valor de alguna clave y, por tanto, durante un tiempo se estará violando el orden del heap. Por ejemplo, se decrementa la clave de un elemento i. Sea j = pred(i). Si después del cambio en el valor de clave(i), clave(j) ≤ clave(i), el heap sigue manteniendo el orden y no hay que hacer nada más. Sin embargo, si clave(j) > clave(i), se necesita recuperar la ordenación. El siguiente proceso sube(i) cumple esta tarea. acción sube(i) mientras i no sea la raíz y clave(i) < clave(pred(i)) hacer intercambia(i, pred(i)); fmientras facción 94 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Argumentos inductivos muestran que al acabar dicho proceso, el heap satisface la propiedad de orden. sube(i) requiere un tiempo O(logdn) porque cada ejecución del mientras decrementa la profundidad del elemento i en una unidad y, como se ha dicho anteriormente, la profundidad máxima de un elemento es O(logdn). Si lo que se hace es incrementar la clave de algún elemento i, después puede ocurrir que clave(i) ≤ clave(j) para todo j ∈ SUCC(i) y, por tanto, el heap sigue manteniendo la propiedad de orden. De otra manera, hay que modificar el heap para que la cumpla. El proceso baja(i) hace esta tarea. acción baja(i) mientras i no sea hoja y clave(i) > clave(hijomin(i)) hacer intercambia(i, hijomin(i)); fmientras facción Siendo hijomin(i) el elemento con clave más pequeña de los hijos de i. De nuevo, por inducción, se demuestra que al final de la acción el heap satisface la propiedad de orden. El proceso requiere un tiempo O(dlogdn) porque cada ejecución del mientras incrementa la profundidad del elemento i en una unidad y además requiere un tiempo O(d) para calcular hijomin(i). 8.2.6. Operaciones del Heap busca-mínimo(i, H). La raíz del heap es el elemento con clave más pequeña y se encuentra en la primera posición del vector donde se guarda éste. Por lo tanto, es una operación que requiere tiempo constante O(1). inserta(i, H). Se incrementa último en 1 y se guarda el nuevo elemento i en la última posición del vector. Después, se ejecuta la acción sube(i) para recuperar el orden del heap. Claramente, esta operación requiere un tiempo equivalente al de sube(i): O(logdn). decrementa-clave(i, valor, H). Se decrementa el valor de la clave del elemento i y se ejecuta el proceso sube(i) para recuperar el orden del heap. Por tanto, el tiempo de ejecución es O(logdn). incrementa-clave(i, valor, H). Se incrementa el valor de la clave del elemento i y se ejecuta el proceso baja(i) para recuperar el orden del heap. Por tanto, el tiempo de ejecución es O(dlogdn). borra-mínimo(i, H). La raíz del heap es exactamente el elemento con clave mínima. Sea j el elemento guardado en la última posición del vector. Primero se efectúa intercambia(i, j) y luego se 95 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. decrementa ultimo en 1. Después, se ejecuta baja(j) hasta recuperar el orden del heap. Esta operación requiere un tiempo O(dlogdn). borra(i, H). Se intercambia el elemento a borrar con el último del heap. Luego se ejecuta el proceso baja o sube según sea el caso para recuperar el orden del heap. El tiempo necesario es O(dlogdn). 8.3. Heaps de Fibonacci El heap de Fibonnaci es una estructura de datos que permite hacer operaciones de heaps de forma más eficiente que los d-heaps. Esta estructura realiza las operaciones insertar, buscar-mínimo y decrementa-clave en tiempo O(log n). 8.3.1. Propiedades El nombre de Fibonacci es debido a que las demostraciones de sus tiempos de cálculo usan propiedades de los conocidos números de Fibonnaci. Los números de Fibonnaci se definen recursivamente como F(1) = 1, F(2) = 1, y F(k) = F(k – 1) + F(k – 2), para todo k ≥ 3. Además, cumplen las siguientes propiedades: (a) Para k ≥ 3, F(k) ≥ 2(k – 1)/2. (b) F(k) = 1 + F(1) + F(2) + ... + F(k – 2). (c) Dada una serie de números G(·) que satisface que G(1) = 1, G(2) = 1, y G(k) ≥ 1 + G(1) + G(2) + ... + G(k – 2) para todo k ≥ 3, G(k) ≥ F(k). 8.3.2. Definición y almacenamiento de un Heap de Fibonacci Como se dijo anteriormente, un heap guarda un conjunto de elementos, cada uno con una clave de valor real. Un heap de Fibonacci es una colección de árboles dirigidos: cada nodo i del árbol representa un elemento i y cada arco (i, j) representa una relación padre-hijo: el nodo j es el predecesor del nodo i. Para representar un heap de Fibonacci numéricamente y poder manipularlo cómodamente, se necesitan las siguientes estructuras: pred(i): el predecesor del nodo i en el heap de Fibonacci. 96 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. Un nodo que no tenga padre es un nodo raíz y su predecesor será 0. Esto permite a simple vista ver si un nodo es raíz o no, mirando al índice de predecesores. SUC(i): el conjunto de hijos del nodo i. Este conjunto se guarda como una lista doblemente encadenada. rango(i): el número de sucesores del nodo i (o equivalentemente, |SUC(i)|). clavemin: el nodo con clave mínima. 8.3.3. Uniendo y cortando Como se vio en los d-heaps, aquí todas las operaciones también se reducen a otras más básicas, en este caso dos: une(i, j), corta(i). La operación une(i, j) se aplica a dos nodos raíz distintos i y j de igual rango; esto hace que se unan los dos árboles correspondientes para formar uno único. La operación corta(i) separa el nodo i de su padre y lo convierte, por tanto, en la raíz de un árbol. une(i, j). Si clave(j) ≤ clave(i), entonces se añade el arco (i, j) al heap de Fibonacci (haciendo que el nodo i sea el predecesor del nodo j). Si clave(j) > clave(i), entonces se añade el arco(j, i) al heap. corta(i). Se borra el arco (i, pred(i)) del heap (convirtiendo al nodo i en un nodo raíz). Hay que notar que la operación une(i, j) incrementa el rango de uno de los dos nodos en 1 unidad. Además, las dos cambian el predecesor, el conjunto de sucesores y la información de rango de, como mucho, dos nodos; en consecuencia, se pueden ejecutar en tiempo O(1). Mientras se manipula el heap de Fibonacci, se produce una secuencia de uniones y cortes. Hay una estrecha relación entre el número de uniones y cortes. Para observarla, consideraremos una función f definida como el número de árboles. Cada operación une decrementa f en 1 y cada operación corta la incrementa en 1. Lo máximo que puede crecer f está acotado por su valor inicial (que es n) más el incremento total de f. Por tanto, el número de operaciones une es como mucho n más el número de cortes. 97 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 8.3.4. Invariantes El heap de Fibonacci mantiene un conjunto de árboles que cambia dinámicamente mientras se efectúan las distintas operaciones de unir y cortar. Estos árboles satisfacen ciertos invariantes que son esenciales para derivar las acotaciones de los costes temporales de las operaciones. Los nodos en el heap siempre satisfacen la propiedad de orden, que dice que la clave de un nodo es menor o igual a las claves de sus sucesores. Además, se satisfacen los siguientes invariantes: Cada nodo no raíz ha perdido como mucho un sucesor después de haberse convertido en un nodo no raíz. No hay dos nodos raíz que tengan el mismo rango. Como siempre, aunque se puedan violar los invariantes en pasos intermedios en algunas operaciones, el heap siempre los satisfacerá en la conclusión de éstas. Otra importante consecuencia de los dos últimos invariantes es un lema que dice: El rango máximo posible de cualquier nodo es 2logn + 1. La siguiente propiedad surge directamente del Invariante 3 y del Lema X: Un heap de Fibonacci tiene como mucho 1 + 2logn árboles. 8.3.5. Restaurando el Invariante 2 Para restaurar el invariante 2, se mantiene un índice adicional llamado perdidos(i) que representa, para cada nodo i, el número de sucesores que el nodo ha perdido después de convertirse en un nodo no raíz. Para un nodo raíz r, perdidos(r) = 0. Supongamos que, mientras se manipula el heap de Fibonacci, se efectúa la operación corta(i). Sea j = pred(i). En esta operación el nodo j pierde un sucesor (el nodo i). Si el nodo j no es raíz, se incrementará en 1 unidad perdidos(j). Si perdidos(j) es 2, el invariante 2 requiere que j se convierta en nodo raíz. En ese caso, se ejecuta corta(j) y j pasa a ser nodo raíz. Sea k = pred(j). Este corte 98 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. incrementa perdidos(k) en 1. Si k es un nodo no raíz y perdidos(k) = 2 se debe convertir en raíz también, y así sucesivamente. Por lo tanto, un corte puede ocasionar numerosos cortes en efecto cascada: Se siguen efectuando estos cortes hasta que se llegue a un nodo que no haya perdido ningún sucesor o sea raíz. A estos cortes adicionales se les llama cortes en cascada y a la secuencia completa de pasos que siguen a un corte como multicascada. Se puede demostrar que el número total de cortes en cascada es inferior o igual al número total de cortes efectuado hasta el momento en el heap. 8.3.6. Restaurando el Invariante 3 El invariante 3 dice que no hay dos nodos raíz con el mismo rango. Para mantener esta propiedad, se necesita de un índice para cada posible rango k = 1, ..., K = 2 log n + 1. balde(k). Si el heap no contienen ningún nodo raíz con rango igual a k, entonces balde(k) = 0; y si algún nodo raíz i tiene rango igual a k, entonces balde(k) = i. Supongamos que mientras se manipula un heap de Fibonacci, se crea un nodo raíz j de rango k pero el heap ya tenía otro nodo raíz i con del mismo rango. Entonces se repite el siguiente proceso hasta restaurar el tercer invariante: Se ejecuta la operación enlaza(i, j), que junta los dos árboles en uno nuevo de rango k + 1. Supongamos que ese nodo l es la raíz del nuevo árbol. Entonces mirando el valor de balde (k + 1), se comprueba si el heap ya tiene un nodo raíz de rango k + 1. Si no, no hay que hacer nada más. En caso contrario, se efectúa otra operación de enlace para crear otro árbol de rango k + 2 con la consiguiente comprobación de si ya existía un nodo raíz de rango k + 2. Se repite el proceso hasta que el invariante nº 3 quede restaurado. Esta secuencia de enlaces se denomina multienlaces. Se puede demostrar que el número total de multienlaces es proporcional al número de enlaces efectuados hasta el momento en el heap de Fibonacci. 99 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 8.3.7. Operaciones A continuación, se muestra cómo se ejecutan varias de las operaciones del heap y se indica el tiempo que emplean. busca-mínimo(i, H). Simplemente devuelve i = clavemín, pues ésta contiene el nodo con la clave más pequeña. El tiempo de ejecución es de O(1). inserta(i, H). Se crea un nuevo nodo raíz i y se añade a H. Después de esto, el heap tal viole el invariante 3, por lo que posiblemente sean necesarios multienlaces posteriores para restaurarlo. El tiempo de ejecución es de O(1). decrementa-clave(i, valor, H). Primero se decrementa la clave del nodo i al nuevo valor. Después, cada nodo del subárbol que cuelga del nodo i sigue satisfaciendo la propiedad de orden del heap; el predecesor del nodo i puede, sin embargo, violarla. Sea j = pred(i). Si clave(j) ≤ valor, ya está. Si no, se ejecuta un corte, corte(i), para convertir a i en nodo raíz, y se actualiza clavemín. Después de este corte, el heap tal vez viole el segundo invariante, así que se necesitarán cortes en multicascada para restaurarlo. Estos cortes en multicascada generan nuevos árboles cuyas raíces se guardan en una lista. Entonces, uno por uno, se van borrando de la lista, añadiéndose al conjunto previo de raíces, y efectuando multienlaces para satisfacer el tercer invariante. El proceso finaliza cuando la lista se quede vacía. El tiempo de ejecución es de O(1). borrar-mínimo(i, H). Primero se pone i = clavemín. Entonces, uno por uno, se recorre la lista de sucesores de i, se cortan y se actualiza clavemín. Se aplica multienlace después de cada corte. Cuando se han cortado todos, se busca en todos los nodos raíz (guardados en balde(k), para k = 0, 1, ..., 2 log n + 1), identificando el nodo raíz h con la clave mínima, y poniendo clavemín = h. Como | SUC(i) | ≤ 2 log n + 1, esta operación hace O(log n) cortes, seguido de un número de cortes en cascada y enlaces. Después, se busca entre los O(log n) nodos raíz para encontrar el que tenga la clave mínima. El tiempo de ejecución es de O(log n). 100 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 9. Balances y conclusiones 9.1. Comparación tiempo estimado con tiempo real El tiempo empleado para la ejecución de este proyecto no ha diferido demasiado del previsto en un principio. La diferencia más destacable es el estudio de los algoritmos a insertar en la librería. Al principio se disponía de una bibliografía básica de donde se suponía que iban a salir todos los algoritmos pero poco a poco se fue encontrando multitud de documentos, sobretodo en revistas científicas, con propuestas nuevas de algoritmos y esto demoró en 1 mes aproximadamente el estudio de los mismos. En lo que sí difieren la planificación del trabajo y el resultado final es en la dedicación de horas por semana al proyecto. Debido a asuntos personales de el que escribe estas líneas hubo una temporada en la que prácticamente no se dedicó ninguna hora a la realización del proyecto, con la consecuencia de una demora importante en la presentación del mismo (aproximadamente 1 año). Cabe decir que la librería y la aplicación de la misma en la aplicación de protección de datos ya estaba lista a finales de 2003 y se entregó para su uso pero la redacción de los últimos capítulos de la memoria así como la preparación de la defensa del proyecto no se retomaron hasta finales de Noviembre de 2004. 101 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 9.2. Balance económico 300 horas de programación 30 €/hora 9000 € 100 horas de redacción memoria, 20 €/hora 2000 € estudios, etc. Equipo completo Pentium IV 2,8 800 € 800 € Ghz para la implementación del código Sistema Operativo Windows XP 150 € 150 € para la redacción de la memoria, presentación del proyecto y documentación Sistema Operativo Linux para la gratuito gratuito implementación del software Conexión ADSL 29 € / mes 290 € TOTAL APROXIMADO 12240 € Todos estos gastos corrieron a cuenta del proyectista e incluyen el uso de la librería en la aplicación de protección de datos. 102 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 9.3. Conclusiones Los objetivos propuestos al principio de este proyecto han sido cubiertos en gran medida. Se ha diseñado e implementado una librería de algoritmos que resuelven el problema del camino mínimo en redes o grafos de forma eficiente pensado para aplicaciones que usan exhaustivamente su tiempo de ejecución en este tipo de problemas. La librería ha sido suficientemente probada en todo tipo de situaciones y, salvo problemas derivados de agentes externos, será una buena herramienta que proporcionará un gran conjunto de algoritmos a escoger, algo hasta ahora inexistente. La librería, además, es de uso muy sencillo sin sacrificar en ningún momento la eficiencia temporal. Permite también a los desarrolladores implementar nuevas versiones teniendo que modificar un código mínimo. Además, se ha logrado incluir la librería en la aplicación de protección de datos, proporcionando una considerable velocidad de ejecución, que es lo que se buscaba. 103 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 9.4. Líneas abiertas Nunca se podrá decir que la librería esté cerrada a futuras modificaciones. Se ha diseñado pensando en posibles añadidos siempre que se encuentren nuevos algoritmos más eficientes o estructuras de datos que ofrezcan mayores velocidades de ejecución. En los dos casos la inserción de los mismos es muy sencilla, teniendo únicamente que modificar un mínimo de código, pero siempre muy específico y localizado. Obviamente es una librería pensada para ser usada en aplicaciones muy específicas que emplean el mayor tiempo en buscar caminos mínimos en redes o grafos y, por tanto, puede parecer limitada al querer usarse en otros tipos de aplicaciones que, aunque también necesitan resolver problemas de caminos mínimos, no requieren tanta eficiencia ni complicados algoritmos. Por tanto, cabría pensar en una librería más “estética” pero sacrificando enormemente la eficiencia temporal, con métodos mucho más sencillos e intuitivos, con impresión por pantalla de datos de ejecución, más opciones en la construcción de grafos (aceptación de formatos stándard, etc.) e incluso una versión gráfica como proporcionan algunas librerías existentes. 104 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. 10. Bibliografía 10.1. Libros R.K. Ahuja, T.L. Magnanti, J.B. Orlin, “Network flows: theory, algorithms, and applications”, Prentice Hall (1993). D.P. Bertsekas, “Linear network optimization: algorithms and codes”, The MIT Press (1990). C. Gómez, E. Mayol, A. Olivé, E. Teniente, “Enginyeria del software Disseny I”, Edicions UPC (2001). 10.2. Artículos en revistas U. Pape, “Implementation and efficiency of Moore-algorithms for the shortest route problem”, Mathematical Programming 7 (1974), North-Holland Publishing Company. R.K. Ahuja, K. Mehlhorn, J.B. Orlin, R.E. Tarjan, “Faster algorithms for the shortest path problem”. S. Pallotino, “Shortest path methods”, Networks 14 (1984). M.S. Hung, J.J. Divoky, “A computational study of efficient shortest path algorithms”, Computational Operations Research 15 (1988). F.B. Zhan, C.E. Noon “Shortest path algorithms: an evaluation using real road networks”, Transportation Science 32 (1988). F. Glover, D. Klingman, N. Phillips, “A new polynomially bounded shortest path algorithm”, Operations Research 33 1 (1985). M.L. Fredman, R.E. Tarjan, “Fibonacci heaps and their uses in improved network optimization algorithms”, Journal of the ACM 34 3 (1987). 105 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. J. Mondou, T.G.Crainic, S. Nguyen, “Shortest path algorithms: a computational study with the C programming language”, Operations Research 18 8 (1991). L.H. Cox “Network models for complementary cell suppression”, Journal of the American Statistical Association 90 432” (1995). 10.3. De internet: F. B. Zhan, C. E. Noon, “A comparison between label-setting and label-correcting algorithms for computing one-to-one shortest paths” T.A.J. Nicholson, “Finding the shortest route between two points in a network”. J. Larsen, I. Pedersen, “Experiments with the auction algorithm for the shortest path problem” (1997). J. Turner, “Algorithms and programs”. G. Righini, “Bidirectional Dijkstra’s algorithm” (1999). B. Liu, “Intelligent route finding: combining knowledge, cases and an efficient…” (1996). R. Jacob, M. Marathe, K. Nagel, “A computational study of routing algorithms for realistic transportation” (1998). E.M. Vieira, “The optimal path problem” (1998). D.P. Bertsekas, “Parallel asynchronous label correcting methods for shortest paths”. E.M. Vieira, “The k shortest paths problem”. D. Abuaiadh, J.H. Kingston, “Are Fibonacci heaps optimal?”, (1994). D. Abuaiadh, J.H. Kingston, “An efficient algorithm for the shortest path problem” (1993). D.P. Bertsekas, “Auction algorithms for network flow problems: a tutorial introduction”. 106 Desarrollo de una librería para caminos mínimos. Aplicación a un problema de protección de datos. D. Burton, “The inverse shortest paths problem with upper bounds on shortest paths” (1997). D. Burton, “On an instance of the inverse shortest paths problem” (1992). K. Mehlhorn, “Data structures and graph algorithms. Shortest paths”. A.V. Goldberg, “A simple shortest path algorithm with linear average time”. W. Pijls, “A general framework for shortest path algorithms”. A.V. Goldberg, R.E. Tarjan, “Expected performance of Dijkstra’s shortest path algorithm” (1996). S.G. Kolliopoulos, “Finding real-valued single-source shortest paths in o(n3) expected time”. S.Rao, “Shortest path algorithms: Dijkstra, Bellmand-ford and applications”, (2001). A.V. Goldberg, “Implementations of Dijkstra’s algorithm based on multi-level buckets” (1995). B.V. Cherskassky, A.V. Goldberg, “Buckets, heaps, lists and monotone priority queues” (1999). 10.4. De la aplicación de protección de datos J. Castro, "A fast network flows heuristic for cell suppression in positive tables", Lecture Notes in Computer Science, 3050 (2004), pp. 136-148. Volume Privacy in Statistical Databases, eds. Josep Domingo-Ferrer and Vicenç Torra, Springer, ISBN 3-540-22118-2. J. Castro, "A shortest paths heuristic for statistical disclosure control in positive tables ", Research Report DR 2004/10, Dept. of Statistics and Operations Research, Universitat Politècnica de Catalunya, 2004. Revised version submitted to INFORMS Journal on Computing. 107