1. INTRODUCCIÓN 4 4 5 6 6 7 2. ANÁLISIS DE

Anuncio
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
Descargar