p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p cahiers d’informatique cuadernosdeinformáticacomputingnotebooksquadernid’informatica p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p ppp pp ppp pp ppp pp ppp pp ppp pp pp pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp p pppppppppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp p ppppppppppp p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p Estructuras de datos y de la información II Heap y Grafos Simone Santini Escuela Politécnica Superior Universidad Autónoma de Madrid 2/2009 p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p pppppppppppppppppppppppppppppppppppppppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p p p p p p p p p p p p p p p p pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p p p p p p p p p p p p p p p p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p ppppppppppppppppppppppppppppppppppppppppp p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p cahiers d’informatique 2 The title of this series of technical reports constitute my homage to the legendary magazine Cahiers du cinéma, and to the role that it has played for many years in the creation of a widespread critical sense regarding film. (C) Simone Santini, 2009 grafos 3 I. HEAP Y COLAS DE PRIORIDAD. 1.1 Colas de prioridad Ya hemos encontrado en los capı́tulos precedentes, un tipo de cola: la cola FIFO (first in, first out). Una cola FIFO es una estructura datos en la cual se insertan elementos que después se sacan en el mismo orden en que se han insertado. Formalmente, una cola FIFO proporciona dos operaciones: insert(S,x); extract(S); inserta el elemento x en la cola S; extrae un elemento de la cola S y lo devuelve. La figura 1.1 contiene un simple programa compuesto de una secuencia de operaciones en una cola FIFO. La ejecución del programa produce la secuencia 4, 5, 6, 7. En una cola FIFO, el único parámetro que determina cuando un elemento será extraido es su tiempo de inserción, es decir, el orden de extración de los elementos de la cola FIFO depende sólo del orden de inserción de los mismos elementos. En el ejemplo precedente, el 5 es el secundo elemento que se saca porque se ha insertado en la cola después del 4 y antes del 6, 7 y 8. Una pila, recordamos, funciona de forma diferente: en este caso los último elementos que se han insertado son los primeros que se sacan. Si se ejecutara el programa precedente con una pila, en vez de una cola FIFO, con la misma secuencia de inserciones y extraciones (en este caso las inserciones se hacen por medio de la función push y las extracciones por medio de la función pop ), el resultado será diferente. La figura 1.1 muestra el estado de la ejecución del programa en el caso de una pila. El resultado será, en este caso, 4, 5, 8, 7, 6. A pesar de la diferencia de resultado y del estado de cahiers d’informatique 4 insert(S, 3) - insert(S, 4) - print extract(S) - insert(S, 5) - print extract(S) - insert(S, 6) - insert(S, 7) - insert(S, 8) print extract(S) - 8 - print extract(S) - print extract(S) - Figure 1.1: 3 - 3 - 4 - 4 - 5 - 6 5 - 7 6 5 - 7 6 5 - 8 7 6 - 8 7 - 8 - 4 5 Ejecución de un simple programa en una cola FIFO. la estructura, aún en el caso de una pila se cumple la propiedad que el orden de extración de los elementos depende sólo del orden de inserción. Hay casos en que necesitamos un mayor control sobre la secuencia de extración de los elementos. Un caso muy comune es el siguiente: cada vez que se inserta un elemento en la cola se le asigna un número, llamado la prioridad del elemento; cada vez que se ejecuta una extración queremo extraer el elemento con prioridad más alta entre los que están en la cola. Consideramos otra vez la secuencia de inserciones y extracciones de los ejemplos precedentes, pero esta vez asociamos a cada elemento un entero que represente la prioridad de ese elemento. Indiquemos con nm el elemento n insertado con prioridad m. Una posible secuencia de inserciones y extraciones es representada en figura 1.1. En este caso el programa produce 3, 4, 8, 7, 6. La diferencia entre esta estructura y las de antes es que en este caso el momento en que se extrae un elemento de la estructura no depende simplemente del momento en que se ha insertado, sino también por su prioridad. Por ejemplo, el tercer elemento que se inserta (el 5) nunca se saca de la cola, debido a su baja prioridad. El momento de la inserción también tiene su importancia. Por ejemplo, el elemento 3, con prioridad 4, se saca inmediatamente de la cola, mientras que el 6, que también tiene prioridad 4, se saca después de los elementos 7 y 8 que, si bien se han insertado después del 6, tienen prioridad más alta. Desde un punto de vista formal, una cola de prioridad es una estructura de datos que proporciona las operaciones siguientes (es decir, es una estructura de datos que presenta la siguiente interfaz): insert(S,x); inserta el elemento x (con su prioridad) en la cola S; max(S); devuelve el elemento con prioridad mas alta entre los que se encuentran en la cola S; extract(S); elimina el elemento de prioridad maxima de la cola S y lo devuelve como resultado. grafos 5 push(S, 3) push(S, 4) print pop(S) push(S, 5) print pop(S) push(S, 6) push(S, 7) push(S, 8) print pop(S) print pop(S) print pop(S) Figure 1.2: 3 3 4 - 3 3 5 - 3 3 6 6 7 7 3 3 3 3 3 - 6 6 6 - 7 8 - Ejecución de la misma secuencia de inserciones y extraciones en una pila. Hay muchas aplicaciones en que se necesita una cola de prioridad. Consideremos, por ejemplo, un sistema operativo multitarea. En cualquier momento hay varios procesos esperando que la CPU pueda ejecutarlos. En general, el sistema operativo determina un cuanto de tiempo (time slot) durante la cual la CPU ejecuta un proceso. Al final del time slot, la CPU suspende la ejecusión del proceso, guarda su estado, y pasa a ejecutar, para el time slot siguiente, otro proceso. Ası́, en secuencia, la CPU dedica tiempo a todo proceso. Por otro lado, hay procesos más criticos que otros. Muchos procesos del sistema operativo se ocupan de funciones criticas del sistema, y deben ejecutarse en seguida cuando la CPU esté disponible. Al fin de gestionar estas situaciones, cada proceso que se ejecuta tiene una prioridad, y todos los procesos que están esperando la CPU se ponen en una cola de prioridad. Cada vez que la CPU se encuentra disponible, el sistema operativo extrae el proceso a prioridad más alta entre los en espera y la CPU ejecuta durante el cuanto de tiempo siguiente. Nuevos procesos pueden ser insertados en la cola mediante la función insert. Otra aplicación tı́pica se encuentra en sistemas de simulación basados en eventos. Los elementos en la cola son los eventos que se tienen que simular, y la prioridad es el instante de tiempo en que ocurren. Los eventos deben ser simulados en el orden en que llegan y por esto se puede usar una cola de prioridad. Notamos que en este caso, ya que los evento se simulan desde el más antiguo hasta el más reciente, la prioridad más alta corresponde al tiempo mı́nimo, o sea, la prioridad de un evento que llega en el instante t es −t. Una forma sencilla de implementar una cola de prioridad es alumbrada el la figura 1.1: se pueden guardar los elementos ordenados, desde la prioridad más ata hasta la más baja y cada vez devolver el primer elemento de la cola. Si se implementa la cola como una lista enlazada, le extración del maximo se hace de forma inmediada, pero la inserción supone un coste medio O(n) para mantener la lsta ordenada. El coste medio de las operaciones de la interfaz es el siguiente: insert(S,x); max(S); O(n) Θ(1) cahiers d’informatique 6 insert(S, 34 ) 34 insert(S, 42 ) 34 42 print extract(S) 42 insert(S, 51 ) 42 51 print extract(S) 51 insert(S, 64 ) 64 51 insert(S, 75 ) 75 64 51 insert(S, 86 ) 86 75 64 51 print extract(S) 75 64 51 print extract(S) 64 51 print extract(S) 51 Figure 1.3: max extract(S); Ejecución de un simple programa en una cola de prioridad Θ(1) El problema de esta solución es el alto coste de la función insert. Por esto, las colas de prioridad se implementan tı́picamente usando un heap, que es la estructura de datos de que trataremos en seguida. El uso de heap proporciona una implementación eficiente de las operaciones de cola de prioridad, con las siguiente eficiencias asimptóticas: insert(S,x); max(S); max extract(S); 1.2 O(log n) Θ(1) O(log n) Heap Un heap es un árbol binario que cumple las dos propriedades siguientes: i) cada nivel del árbol, con la posible excepción del nivel más bajo, es completo; el nivel más bajo se completa ordenadamente desde la izquierda (propiedad de complitud) ; ii) cada nodo que no sea la raı́z contiene un valor no superior a él de su antecesor (propiedad heap). grafos 7 Según estas propiedades, el siguiente árbol es un heap : @ABC GFED 21 PP PPP ppp p PP p p p @ABC GFED @ABC GFED 20 15 >> } AAA ppp p } > p } pp @ABC GFED @ABC GFED @ABC GFED 89:; ?>=< 13 11 10 > 9 >> ?>=< 89:; ?>=< 89:; ?>=< 4 6 89:; 1 (1.1) mientras los dos siguientes no son heap : el primero no cumple la propiedad heap (el vértice 14 y el 7 contienen un valor mayor que 4, el valor de su antecesor), y en el segundo el nivel más bajo no se ha completado de izquierda a derecha (hay un hueco entre el 4 y el 1): @ABC GFED q 21 PPPP q q PPP qqq ?>=< 89:; @ABC GFED 6 20 A oo ::: AA } o o } o } o o @ABC GFED ?>=< 89:; @ABC GFED @ABC GFED 15 A 9 13 11 AA ?>=< 89:; @ABC GFED 89:; 4 10 ?>=< 1 @ABC GFED 21 PP PPP pp p p PP p p p @ABC GFED @ABC GFED 15 20 A >> pp AA } p p } > p } p p @ABC GFED 89:; ?>=< @ABC GFED GFED @ABC 10 > 9 13 11 } >> } }} 89:; ?>=< 89:; ?>=< 89:; ?>=< 4 6 1 (1.2) La propiedad de complitud nos permite representar el árbol muy fácilmente en una cadena (array). Dado un array A y un heap 1 , se almacena el heap en el array como sigue: i) la raı́z del árbol se pone en A[1]; ii) los hijos izquierdo y derecho del elemento almacenado en A[i] se almacenan en A[2i] y A[2i + 1] respectivamente. Con este macanismo, el antecesor, y los hijos de derecha y de izquierda del vértice almacenado en la posición i se calculan como parent(i) = bi/2c; left(i) = 2i; right(i) = 2i + 1; Recordamos que en cualquier ordenador moderno, el calculo de 2i y 2i + 1 no requiere multiplicaciones, sino se puede obtener con un shift y un or. Se trata, por eso, de operaciones muy veloces. La representación del heap (1.1) como array es la seguiente: 16 14 10 8 7 9 3 2 4 1 La propiedad heap se traduce en el requisito que para cada i tal que A[i] no es la raı́z, se cumpla A[parent(i)] ≥ A[i], es decir: ∀i > 1 A[bi/2c] ≥ A[i] (1.3) 1 La propiedad heap no es esencial para esta representación: cualquier árbol que cumpla la propiedad de cumplitud se puede representar en esta forma. Sin embargo, aquı́ la representación solo se usará para heap. cahiers d’informatique 8 Como veremos, la propiedad heap s la clave para una implementación eficiente de las funciones de cola de prioridad y del algoritmo heapsort. Las operaciones insert y extract funcionan insertando (o, respectivamente, removiendo) un elemento de un heap de una forma que "destruye" la propiedad heap y luego llamando la función heapify para "reconstruir" la propiedad. La función heapify es llamada con un array A y un ı́ndice i. Se asume que los sub-árboles left(i) y right(i) cumplen la propiedad heap, pero que A[i] puede ser menor que sus hijos, ası́ que el árbol con raı́z en i puede no cumplir la propiedad. Un tal caso se da en el árbol de izquierda de (1.2). Si se considera como nodo i el vértice con valor 4, es fácil darse cuenta que los dos árboles que cuelgan de eso (es decir, los árboles con raı́z en los vértices con valor 14 y 7), si considerados individualmente, cumplen la propiedad heap, pero que el árbol con raı́z en 4 no la cumple. En este caso, el vértice con valor 4 se encuentra el la segunda posición del array y este heap se puede corregir con la llamada heapify(A, 2). El algoritmo heapify es el siguiente: heapify(A, i) 1 l ← left(i); 2 r ← right(i); 3 if l ≤ heap size(A) and A[l] > A[i] then 4 largest ← l; 5 else 6 largest ← i; 7 fi 8 if r ≤ heap size(A) and A[r] > A[largest] then 9 largest ← r; 10 fi 11 if largest 6= i then 12 exchange A[i], A[largest] 13 heapify(A, largest); 14 fi Las lı́neas 3-10 determinan el más grande de los tres elementos A[i], A[left(i)] y A[right(i)], poniendo el indice correspondiente en la variable largest. Si A[i] es el más grande, entonces el sub-árbol con raı́z i ya es un heap, y el algoritmo termina. En caso contrario, a la lı́nea 12, A[i] se pone en lugar del más grande de los elementos A[left(i)] y A[right(i)]. A este punto, el vértice i es más grande que sus hijos y cumple la propiedad heap. Por otro lado, el vértice en el que se ha puesto el valor A[i] ha decrementado su valor, y puede no cumplir la propiedad heap. Para obviar al problema, en la lı́nea 13 se procede a llamar heapify recursivamente en este sub-árbol. Consideremos el árbol a la izquierda de (1.2). El vértice i contiene 4, y el valor más grande entre el de i el los de sus hijos es 14. Entonces, se procede a intercambiar el 4 con el 14, obteniendo el árbol: @ABC GFED 21 PP PPP ppp p PP p p p @ABC GFED @ABC GFED 15 20 >> } AAA ooo o } > o } oo ?>=< 89:; 89:; ?>=< @ABC GFED @ABC GFED 6> 9 13 11 > > ?>=< 89:; @ABC GFED 89:; 4 10 ?>=< 1 (1.4) grafos 9 El vértice i y sus hijos ahora cumplen la propiedad heap (el antecesor contiene 14, el valor más grande), pero el árbol en que se ha cambiado el 14 por un valor más peque~ no no la cumple. Por esto, se llama heapify con el nodo en que se ha puesto el 4. Ahora el valor más grande entre 4 y sus hijos es el 8. El 4 se intercambia con el 8, obteniendo el árbol siguiente, que es un heap : @ABC GFED 21 PP PPP pp p p PP p p p @ABC GFED @ABC GFED 20 15 >> } AAA ppp p } > p } p p @ABC GFED @ABC GFED @ABC GFED 89:; ?>=< 13 11 10 > 9 >> ?>=< 89:; ?>=< 89:; ?>=< 4 6 89:; 1 (1.5) Al fin de determinar el tiempo de ejecución de heapify, notamos que todas las operaciones son Θ(1), con la excepción de la llamada recursiva en la lı́nea 13. ¿Cuantas veces se llama recursivamente la función? Si se está analizando un nodo v, la función de llama a la lı́nea 13 usando como parametro uno de los hijos de v. Es decir, por cada llamada recursiva bajamos de un nivel en el árbol. Si la altura del árbol es h, la función se llama, en el caso peor, h veces, resultando en un tiempo de ejecución O(h). La propiedad de cumplitud nos asegura que el árbol no es degenerado, y que h = dlog2 ne, proporcionando un tiempo de ejecución O(log n). Para demostrar la corrección del algoritmo, sea heap(A, i) el predicado que sostiene que el sub-árbol de A con raı́z en i cumple la propiedad heap. Theorem 1.2.1. heap(A, left(i)) ∧ heap(A, right(i)) ∧ A → heapify(A, i) ⇒ heap(A, i) (1.6) Demostración. Si los sub-árboles de un árbol cumplen la propiedad heap, y el valor de la raı́z es mayor que el valor de las raı́ces de los sub-árboles, entonces el árbol completo cumple la propiedad heap. Es decir, en fórmulas, heap(A, left(i)) ∧ heap(A, right(i)) ∧ A[i] > A[left(i)] ∧ A[i] > A[right(i)] Sea h(A, i) la altura del sub-árbol de A con raı́z en el vértice i. inducción en h(A, i). ⇒ heap(A, i) (1.7) La demostración es por Si h(A, i) = 0, la propiedad es evidente. Supongamos que el teorema sea valido para h(A, i) = k, y consideramos un vértice j tal que h(A, j) = k + 1. i) si A[j] > A[left(j)], A[right(j)], entonces h(A, j) y, ya que el el algoritmo no cambia nada, h(A, j) sigue valido después de su ejecución; ii) supongamos ahora que A[left(j)] > A[j], A[right(j)]; después de heapify, se ha heap(B, right(j)), porque este sub-árbol no cambia; después del paso 12, se ha A[j] > A[left(j)], A[right(j)]. El subárbol A[left(j)] tiene h(A, left(j)) = k y heapify funciona en este árbol por la hipótesis de inducción, ası́ que se da heap(A, left(j)) y, consecuentemente, heap(A, j) por la (1.7); iii) el caso A[right(j)] > A[j], A[left(j)] es claramente simétrico al precedente. cahiers d’informatique 10 1.2.1 Construcción de un heap La función heapify se puede usar de una forma bottom-up (o sea, empezando por las hojas de una árbol y subiendo hasta la raı́z) para convertir un array A[1, . . . , n] en un heap. Como en la sección precedente, consideramos A como la representación de un árbol binario que cumple la propiedad de complitud. Los elementos A[bn/2c, . . . , n] son las hojas del árbol y por eso ya son heap de tama~ no 1. A partir del elemento A[bn/2c] hasta la raı́z, la procedura siguiente recorre los nodos del árbol llamando para cada uno heapify. Después de la llamada heapify (A, i) el árbol con raı́z en A[i] es un heap. build heap(A) 1 heap size(A) ← length(A); 2 for i ← blength(a)/2c downto 1 do 3 heapify(A, i); 4 od Es posible derivar una simple estima del tiempo de ejecución de build heap considerando que la función heapify tiene un tiempo de ejecución O(log n) y que en build heap se llama n/2 veces. Luego, el tiempo de ejecución es a lo peor O(n log n). Este lı́mite superior es correcto, pero no es una buena estima, en el sentido que, en realdad, la función build heap se ejecuta en un tiempo menor. Es decir, la estima no es asintóticamente precisa. Una estima mejor se puede conseguir con las consideraciones siguientes. Observamos que el tiempo de ejecución de heapify varia según la altura del nodo con que es llamado: el tiempo de ejecución es O(h), donde h es la altura del nodo. Se puede demostrar que en un heap hay, en el caso peor, dn/sh+1 e nodos de altura h ası́ que el tiempo de ejecución de build heap se puede expresar como blog nc blog nc X X h n (1.8) d h+1 eO(h) = O n 2 2h h=0 h=0 Para determinar el valor de esta suma, consideremos la igualdad ∞ X k=0 kxk = x (1 − x)2 (1.9) que, para x = 1/2 nos da ∞ X h 1/2 = =2 h 2 (1 − 1/2)2 (1.10) k=0 Luego, el tiempo de ejecución de la procedura de construcción es: ! blog nc ∞ X h X h O n =O n = O(n) 2h 2h h=0 (1.11) h=0 O sea, es posible construir un heap partendo de un array desordenado en un tiempo lineal. La figura 1.2.1 muestra los varios pasos de la construcción de un heap. grafos 11 89:; ?>=< 1 NN NNN nnn n n NN& nw nn 89:; ?>=< 89:; ?>=< 2 3= p @@@ p == p p @ p px p @ABC GFED 89:; ?>=< 89:; ?>=< 89:; ?>=< 10 6 7 4= ~~ == ~ ~ ~ z ~ 89:; ?>=< 89:; ?>=< ?>=< 8 9 89:; 5 ?>=< 89:; oo 1 NNNNN o o o NN& ow oo ?>=< 89:; 89:; ?>=< 2 3= oo @@@ o == o o @ o ow o ?>=< 89:; ?>=< 89:; 89:; ?>=< ?>=< 89:; 5 6 7 4= ~ == ~~~~ ?>=< 89:; ?>=< 89:; GFED 8 9 @ABC 10 1 2 3 4 10 6 7 8 9 5 heapify(5) cambios: 1 2 3 4 5 6 7 8 9 10 ?>=< 89:; 1 NN NNN nnn n n NN& nw nn ?>=< 89:; 89:; ?>=< 2 3= p @@@ p == p p @ p px p ?>=< 89:; @ABC GFED ?>=< 89:; 89:; ?>=< 9 == 10 6 7 ~~ ==== ~ ~ ~ | " z ~ ?>=< 89:; ?>=< 89:; 89:; 8 4 ?>=< 5 1 2 3 9 10 6 7 8 4 5 heapify(4) cambios: 89:; ?>=< 1 NN NNN nnn n n NN& nw nn 89:; ?>=< 89:; ?>=< 2 7 == p @@@ p ==== p p @ p | " px p 89:; ?>=< @ABC GFED 89:; ?>=< 89:; ?>=< 9 == 10 6 3 ~~ ==== ~ ~ ~ | " z ~ 89:; ?>=< 89:; ?>=< ?>=< 8 4 89:; 5 1 ?>=< 89:; 1 NN NNN ooo o o NN& o o w 89:; ?>=< @ABC GFED 7 10 @@@@@ ====== ooo o @ o | " $ s{ oo ?>=< 89:; ?>=< 89:; ?>=< 89:; 89:; ?>=< 9 == 5 6 3 ~~ ~ ==== ~ ~ | " z ~ ?>=< 89:; ?>=< 89:; 89:; 8 4 ?>=< 2 1 10 7 9 5 6 3 8 4 2 heapify(2) cambios: 1 1 2 7 9 10 6 3 8 4 5 heapify(3) cambios: 1 @ABC GFED 10 OO OOO ooo o o OO #+ oo { s 89:; ?>=< 89:; ?>=< 9 7 p ==== ====== ppp ==" p p | " t| p 89:; ?>=< 89:; ?>=< 89:; ?>=< 89:; ?>=< 8 == 5 6 3 ==== | " | 89:; ?>=< 89:; ?>=< ?>=< 1 4 89:; 2 2 10 9 7 8 5 6 3 1 4 2 heapify(1) cambios: Figure 1.4: Los pasos necesarios para la construcción de un heap. Para cada paso, de izquierda a derecha se muestran la llamada a heapify, el árbol en la apresentación estandar y representado como array, y el número de operaciones de intercambio hechas por heapify. Las aristas en lı́nea doble marcan las partes del árbol que ya cumplen la propiedad heap. 3 cahiers d’informatique 12 1.3 Ordenación con heap : heapsort El algoritmo heapsort utiliza una propiedad importante de un heap : el elemento más grande se encuentra siempre en la raı́z del heap, o sea en el primer elemento del array donde se almacena el heap. (Se vea el problema 8.) El algoritmo ese el siguiente: heapsort(A) 1. Build heap(A); 2. for i ← length[A] downto 2 do 3. swap(A[1], A[i]); 4. heapsize[A] ← heapsize[A] - 1; 5. Heapify(A, 1); 6. od; El algoritmo empieza con la construcción del heap. El elemento máximo se encuentra en la primera posición del array (la raı́z del heap ), y se pone en su posición final (al último elemento del array) con el cambio a la lı́nea 3. El nodo se "quita" del heap decrementando su tama~ no (lı́nea 4), ası́ que as operaciones siguienten no afecten el nodo que hemos puesto en su lugar definitivo. A este punto los dos sub-árboles de la raı́z cumplen la propiedad heap (ya que no los hemos alterados), pero la raı́z puede ser menor que uno de sus hijos. La situación se arregla con la llamada a heapify en la lı́nea 5. Esto nos deja un heap en el array A[1, . . . , n − 1]. El algoritmo repite el procedimiento en este array, y en todos los array de tama~ no decrecente hasta el tama~ no 2. La figura 1.3 muestra el funcionamiento del algoritmo después que se ha construido el heap. En cada paso se muestra la situación después de la lı́nea 4 y después de la llamada a heapify. Las lı́neas doubles marcan los cambios que se hacen en cada llamada a heapify. El heap inicial es el final de la figura 1.2.1. La llamada a Build heap, como hemos visto, supone un tiempo O(n); cada llamada a Heapify toma un tiempo O(log n), y se llama n − 1 veces. Luego, el tiempo de ejecución de Heapsort es O(n log n). 1.4 Uso de heap como colas de prioridad El algoritmo heapsort tiene una eficiencia asimptotica optimal pero, en práctica, hay algoritmos más eficientes como, por ejemplo, el algoritmo quicksort que, si bien tienen la misma eficiencia asimptotica, son más rapidos. Consecuentemente, los heap se usan raramente para la ordenacción. Por otro lado, los heap proporcionan una forma muy eficaz y sencilla de implementar colas de prioridad, o sea, de implementar las operaciones insert, extract, y max que hemos introducido en la sección 1. La función max es implementada en una manera muy sencilla: dado un heap implementado como array, la función max sólo tiene que devolver el primer elemento del array : gracias a la propiedad heap, este elemento es el máximo (se vea el problema 8). La función extract también devuelve el primer elemento del array pero, como el elemento tiene que eliminarse del heap, será necesario reparar el heap que se ha da~ nado con su eliminación. El algoritmo es el siguiente: extract(A) grafos 13 Tras la lı́nea 4 ?>=< 89:; 2 pp NNNNN p p NN& p p px ?>=< 89:; 89:; ?>=< 9= 7 p p == === p p p p px ?>=< 89:; ?>=< 89:; ?>=< 89:; 89:; ?>=< 8= 5 6 3 == ?>=< 89:; ?>=< 89:; ()*+ 1 4 /.-, 2 9 7 8 5 6 3 1 4 10 ?>=< 89:; p 2 NNNN p p NNN p px pp & ?>=< 89:; 89:; ?>=< 8 7 = r == === rr r r ry ?>=< 89:; ?>=< 89:; 89:; ?>=< ?>=< 89:; 5 6 3 4 ?>=< 89:; ()*+ /.-, 1 final del heap ? 2 8 7 4 5 6 3 1 9 10 ()*+ /.-, ?>=< 89:; 1 pp NNNNN p p NN& p p px ?>=< 89:; ?>=< 89:; 5 7 = == === vv v v z v ?>=< 89:; ?>=< 89:; 89:; ?>=< 89:; ?>=< 4 2 6 3 final del heap ? 1 5 7 4 2 6 3 8 9 10 ?>=< 89:; 3 pp NNNNN p p NN& p p px ?>=< 89:; ?>=< 89:; 5 6 = == vv v zvv ?>=< 89:; ?>=< 89:; ?>=< 89:; ()*+ /.-, 4 2 1 final del heap ? 3 5 6 4 2 1 7 8 9 10 Tras Heapify 89:; ?>=< 9 NN NNN ppp p p NN& p p | t 89:; ?>=< 89:; ?>=< 8 7 p = pp == === p p p p t| 89:; ?>=< 89:; ?>=< 89:; ?>=< 89:; ?>=< 4 == 5 6 3 ==== " 89:; ?>=< 89:; ?>=< 1 2 final del heap ? 9 8 7 4 5 6 3 1 2 10 89:; ?>=< pp 8 NNNN p p NNN p & t| pp 89:; ?>=< 89:; ?>=< 5 7 = = v = === === vv v " zv 89:; ?>=< 89:; ?>=< 89:; ?>=< 89:; ?>=< 4 2 6 3 89:; ?>=< 1 final del heap ? 8 5 7 4 2 6 3 1 9 10 89:; ?>=< 7 NNN ppp NNNN p p px p "* 89:; ?>=< 89:; ?>=< 6= v 5 == == v v = | zvv 89:; ?>=< 89:; ?>=< 89:; ?>=< 89:; ?>=< 4 2 1 3 final del heap ? 7 5 6 4 2 1 3 8 9 10 89:; ?>=< 6 pp NNNNN p p NN "* p p px 89:; ?>=< 89:; ?>=< 5 3 vv === v zvv 89:; ?>=< 89:; ?>=< 89:; ?>=< 4 2 1 final del heap ? 6 5 3 4 2 1 7 8 9 10 cahiers d’informatique 14 Tras la lı́nea 4 Tras Heapify ?>=< 89:; 1 pp LLLL p p LL p % px p ?>=< 89:; ?>=< 89:; 3 5 = v == vv v zv ?>=< 89:; ?>=< 89:; ()*+ /.-, 4 2 89:; ?>=< 5 pp HHHH p p p H$ px p 89:; ?>=< 89:; ?>=< 3 4 vv === v v vz 89:; ?>=< 89:; ?>=< 1 2 final del heap ? 1 5 3 4 2 6 7 8 9 10 final del heap ? 5 4 3 1 2 6 7 8 9 10 ?>=< 89:; 2 rr HHHH r r H$ ry r ?>=< 89:; ?>=< 89:; 3 4 v v v v zv ?>=< 89:; ()*+ /.-, 1 89:; ?>=< 4 vvvv HHHH v v v v H$ v~ vv 89:; ?>=< 89:; ?>=< 2 3 v v v v zv 89:; ?>=< 1 final del heap ? 2 4 3 1 5 6 7 8 9 10 /.-, ()*+z ?>=< 89:; 1 HH HH vv v H$ v zv ?>=< 89:; ?>=< 89:; 2 3 final del heap ? 1 2 3 4 5 6 7 8 9 10 ?>=< 89:; 1 vv v v z v ?>=< 89:; 2 # /.-, ()*+ final del heap ? 1 2 3 4 5 6 7 8 9 10 ()*+z /.-, ?>=< 89:; 1 final del heap ? 1 2 3 4 5 6 7 8 9 10 final del heap ? 4 2 3 1 5 6 7 8 9 10 89:; ?>=< 3 H vv HHHHHHHHH v zvv ( 89:; ?>=< 89:; ?>=< 2 1 final del heap ? 3 2 1 4 5 6 7 8 9 10 89:; ?>=< 2 vvvv v v v vvv ~ v 89:; ?>=< 1 final del heap ? 2 1 3 4 5 6 7 8 9 10 89:; ?>=< 1 final del heap ? 1 2 3 4 5 6 7 8 9 10 Figure 1.5: Ejecución del algoritmo heapsort. En cada paso se muestra la situación después de la lı́nea 4 y después de la llamada a heapify. Las lı́neas doubles marcan los cambios que se hacen en cada llamada a heapify. grafos 1 2 3 4 5 6 15 if heap size(A) < 1 then abort "heap underflow"; fi max ← A[1]; A[1] ← A[heap size(A)]; heap size(A) ← heap size(A) - 1; heapify(A, 1); return max; La lı́nea 2 copia el valor de la raı́z del heap (el máximo, por la propiedad heap ) para devolverlo. En las lı́neas 3 y 4 se pone el último elemento del heap en la raı́z y se reduce el tama~ no del heap. Esta operación deja un árbol que no cumple la propiedad heap : los sub-árboles que cuelgan de la raı́z son heap, pero la raı́z puede tener un valor menor que sus hijos. La llamada a heapify en la lı́nea 5 repara el heap. Todas las operaciones de este algoritmo se ejecutan en un tiempo O(1), con la excepción de heapify, cuyo tiempo de ejecución domina él del algoritmo, por lo cual T (extract) = O(log n), donde n es el numero de elementos en la cola. La inserción se hace de una manera que garantiza el mantenimiento de la propiedad heap. La idea es crear una nueva hoja vacı́a al final del árbol y bajar elementos de un nivel al siguiente, empezando con el antepasado de la hoja vacı́a, hasta encontrar un elemento más grande que el elemento que se va a insertar. El nuevo elemento se puede insertar como hijo de este, garantizando que la estructura resultante es un heap. En resumen: i) se incrementa el tama~ no del heap ; ii) empezando por el último elemento, y saltando desde un elemento a su antecesor, se bajan elementos, causando un "hueco" que sube hasta la cima del árbol; iii) cuando se encuentre un valor más grande que el valor a insertar, se introduce éste último en el hueco abierto. Supongamos que se inserte el valor 16 en el heap (1.1). El algoritmo empieza creando un "hueco" en la ultima posición del array. En este caso, el array contiene 10 elementos, y el nuevo elemento que se crea será el numero 11, correspondiente al hijo derecho del elemento 9, o sea, se creará el árbol siguiente: @ABC GFED 21 PP PPP pp p p PP pp p @ABC GFED @ABC GFED 20 15 > p } AAA >> pp p } p } pp @ABC GFED 89:; ?>=< @ABC GFED @ABC GFED 10 ; 9; 13 11 ;; ;;; ; ?>=< 89:; ?>=< 89:; ?>=< 89:; ?>=< 4 6 89:; 1 (1.12) El nuevo elemento (16) es más grande que el antecesor del hueco (9), entonces se mueve el antecesor en el hueco, de hecho "subiendo" el hueco hacia la raı́z: @ABC GFED r 21 PPPP r r PPP rrr @ABC GFED @ABC GFED 20 > q 15 88 >> q q q > 8 q qq @ABC GFED @ABC GFED @ABC GFED 89:; ?>=< 13 11 10 ; 88 ; 8 ; 8 ; ?>=< 89:; 89:; ?>=< ?>=< 89:; ?>=< 4 6 89:; 1 9 (1.13) cahiers d’informatique 16 Todavı́a el nuevo elemento (16) es más grande que el antecesor del hueco (15), ası́ que el procedimiento tiene que repitirse: @ABC GFED p 21 OOOO p p OOO p O ppp @ABC GFED ?>=< 89:;p 20 > ; r r ; >> r ; r ; > r rr @ABC GFED @ABC GFED @ABC GFED @ABC GFED 15 A 13 11 10 > AAA >> ?>=< 89:; ?>=< 89:; 89:; 89:; ?>=< 4 6 ?>=< 1 9 (1.14) Esta vez el antecesor del hueco (21) es más grande que el nuevo elemento, y el procedimiento termina insertando el nuevo elemento en el hueco: @ABC GFED 21 PP PPP nn n n PP n n n @ABC GFED @ABC GFED 16 20 A AA pp AA } p p A } p } p p @ABC GFED @ABC GFED @ABC GFED @ABC GFED 10 > 15 A 13 11 AAA } >> } }} ?>=< 89:; ?>=< 89:; 89:; 89:; ?>=< 4 6 ?>=< 1 9 (1.15) El algoritmo es el siguiente: insert(A, x) if heap size(A) > max array size then abort "heap overflow" fi heap size(A) ← heap size(A) + 1; i ← heap size(A); while i > 1 and A[parent(i)] < x do A[i] ← A[parent(i)]; i ← parent(i); od; A[i] ← x; El tiempo de ejecución del algoritmo es determinado por el numero de veces que se ejecuta el ciclo "while". Éste se ejecuta, en el caso peor, un número de veces igual al numero de niveles del árbol, o sea T (insert) = O(log n). grafos 1.5 17 Ejercicios 1. ¿Cuales de estos array representan heap ? array. Dibujar el árbol correspondiente a cada a) [9, 7, 5, 6, 4, 3] b) [15, 8, 10, 9, 7, 4, 1] c) [5, 5, 5, 5, 5] d) [7, 2, 6, 1, 2, 3, 4] e) [13, 8, 5, 7, 7, 7] 2. ¿Cuales de estos árboles son heap ? array. Para cada heap se escriba la representación como ?>=< 89:; 5 === ?>=< 89:; ?>=< 89:; 3 2 (a) ?>=< 89:; 5 === ?>=< 89:; ?>=< 89:; 5 5 (d) 89:; ?>=< 5= == 89:; ?>=< 2 (b) 89:; ?>=< 9 pp NNNNN p p NN& p p px 89:; ?>=< 89:; ?>=< 7 9= == 89:; ?>=< 89:; ?>=< 89:; ?>=< 8 9 7 (e) 89:; ?>=< 5 (c) 89:; ?>=< 9 pp p p p p px 89:; ?>=< 9= == 89:; ?>=< 89:; ?>=< 8 7 (f) 3. Dado un array A[1, . . . , n] que representa un heap, ¿en que posición se puede encontrar el elemento más peque~ no? 4. ¿Cual es el mı́nimo y el maximo número de elementos que puede tener un heap de altura h? 5. ¿Un sub-árbol de un heap es un heap ? 6. Mostrar que el tiempo de ejecución de Heapify es Ω(log n), o sea que existe un heap tal que heapify necesita un tiempo log n para ejecutarse. (Sugerencia: se construya un heap con n nodos y valores tales que heapify se llama recursivamente para cada nodo desde la raı́z hasta una hoja.) 7. Dado un heap en que ningún elemento se repite, demonstrar que el elemento más grande se encuentra en la raı́z del árbol. 8. Dado un heap cualquiera, demonstrar que uno de los elementos más grandes se encuentra en la raı́z del árbol. 9. Dado un heap de 127 elementos contenido en un array A, ¿cual es el efecto de la llamada heapify(A, 65)? 10. Se construya un heap insertando los elementos 1, 2, 3, 4, 5, 6, 7, 8 en este orden. Se muestren los heap intermedios que se construyen. 18 cahiers d’informatique 11. ¿Como se puede implementar una cola FIFO usando una cola de prioridad? ¿Y una pila? 12. En la representación de un árbol en un array, el hijo izquierdo del nodo almacenado en la posición i se almacena en la posición 2i, y el hijo derecho en la posición 2i + 1. ¿Que pasarı́a si se usara la convención opuesta, o sea, si se almacenara el hijo izquierdo el la posición 2i + 1 y el hijo derecho en la posición 2i? 13. Con la convención del ejercicio precedente, ¿cual serı́a el array correspondiente a los heap del ejercicio 2? ¿Cual serı́a la representación del heap (1.1)? grafos 19 II. INTRODUCCIÓN A LOS GRAFOS 2.6 Definiciones El grafo es una de las estructuras de datos más importantes y de uso más común en informática. Esta popularidad es debida principalmente a la gran expresividad y flexibilidad de los grafos como modelo computational de una gran variedad de problemas y situacciones prácticas. Cada vez que nos enfrentamos a un problema cuyos elementos se relacionan de forma binaria (es decir, se organizan en pares), podemos utilizar un grafo como modelo. En el lenguaje de los grafos, los elementos se llamarán vértices o nodos, y las conexiones entre ellos aristas o arcos. Hemos dicho que las relaciones son binarias, es decir, cada arista del grafo conecta exactamente dos vértices. La relación, en general, no será simetrica: el hecho de que u sea relacionado con v o sea, en el lenguaje de los grafo, de que haya una arista de u a v, no implica que v sea relacionado con u, o sea, no implica que haya una arista de v a u. Los siguientes son unos ejemplos de problemas que se pueden modelar con grafos: i) La red de una compa~ nı́a aerea. En este caso el modelo será un grafo cuyos vértices representarán a los aeropuertos servidos por la compa~ nı́a, y habrá una arista del vértice u al vértice v si hay un vuelo directo desde el aeropuerto representado por u al aeropuerto representado por v. Una red de tren o de metro se puede modelar de manera analoga. ii) Las prioridades de una carrera universitaria. En este caso los vértices del grafo representan asignaturas, y hay una arista del vértice u al vértice v si es necesario aprobar la asignature u antes de poder cursar v. iii) Relaciones de parentesco. Los vértices del grafo representan personas, y hay una arista de u a v si u es el padre o la madre de v. iv) Relaciones de nacionalidad. Los vértices del grafo representan personas, y hay una arista de u a v si v tiene la misma nacionalidad que u. cahiers d’informatique 20 v) Red bibliográfica. Los vértices del grafo representan libros, y hay una arista de u a v si el libro u cita el libro v. vi) El web. Los vértices representan páginas web, y hay una arista de u a v si la página u contiene un enlace a la página v. Como siempre, la vida no nos regala nada, y esta flexibilidad en modelación se paga en términos de algoritmos: los algoritmos que operan en grafos son más complejos que los que operan en las estructuras que hemos visto hasta ahora, y su ejecución es asintóticamente más lenta. Sin embargo, a pesar de su complejidad, los algoritmos para grafos tienen una gran importancia en muchos problemas de interés teórico y práctico, y se han publicado literalmente millares de artı́culos y estudios sobre la teorı́a de los grafos y sobre algoritmos que los utilizan como estructura de datos. Un estudio, por superficial que sea, de esta multitud de algoritmos serı́a claramente imposible en las circunstancias presentes. En este capı́tulo y en los siguientes nos limitaremos a estudiar las propiedades de los grafos de mayor interés en informática, y a analizar los algoritmos clásicos más importantes. Empecemos con la definición general de grafo: Definition 2.6.1. Un grafo G es un par G = (V, E), donde: i) V es un conjunto de elementos llamados vértices o nodos del grafo; ii) E ⊆ V × V es una relación binaria en V , es decir, un conjunto de pares de vértices llamados arcos o aristas del grafo. Un grafo se dice finito si V es finito (es fácil darse cuenta en este caso que E también es finito). Los grafos infinitos tienen un cierto interés en matemáticas pero, por razones bastante obvias, no se usan en informática, por tanto asumiremos siempre que V sea finito. Dado un grafo G se usará la notación V [G] para indicar el conjunto de sus vértices y E[G] para el conjunto de sus arcos. Definition 2.6.2. Un grafo G0 es un sub-grafo de G si V [G0 ] ⊆ V [G] y E[G] ⊆ E[G]. Definition 2.6.3. Dado un grafo G y el conjunto V 0 ⊆ V [G], el sub-grafo inducido por V 0 es el grafo G0 = (V 0 , E 0 ) donde E 0 = {(u, v) ∈ E : u ∈ V 0 , v ∈ V 0 } (2.16) Los grafos se representan en ordenadores utilizando los métodos que veremos en la próxima sección. Cuando, por contras, utilizamos papel y lápiz, los grafos se representan a menudo utilizando diagramas. Cada vértice vi se representa mediante un cı́rculo, y se pone una flecha del cı́rculo vi al cı́rculo vj si (vi , vj ) ∈ E[G]. Por ejemplo, el grafo G = ({1, 2, 3, 4}, {(1, 2), (1, 3), (2, 4), (3, 4), (4, 1)}) se representará como 89:; ?>=< 4 rr8 A r r r r r ?>=< 89:; 89:; / ?>=< 1 MM 2 MMM MM& 89:; ?>=< 3 (2.17) (2.18) grafos 21 El grafo siguiente es un sub-grafo de (2.18) 89:; ?>=< 1 ?>=< / 89:; 2 ?>=< / 89:; 4 (2.19) pero no es el sub-grafo inducido por {1, 2, 4}, ya que en (2.18) hay un arco entre 4 y 1. El sub-grafo inducido es 89:; ?>=< ?>=< (2.20) 1 gNNN / 89:; 2 NNN >>> > NNN > NNN >> NNN>> N 89:; ?>=< 4 Por otro lado, el grafo ?>=< 89:; ?>=< / 89:; 2 1> >> >> >> >> 89:; ?>=< 3 (2.21) no es un sub-grafo de (2.18), ya que contiene la arista (2, 3) que no se encuentra en (2.18). Un vértice v se dice adyacente a un vértice u si existe un arco de u a v, es decir, si (u, v) ∈ E[G]. El conjunto de todos los vértices adyacentes a un vértice u se llama el entorno (neighborhood) de u: NG (u) = {v|(u, v) ∈ E[G]} (2.22) Un camino (path) en un grafo es una lista de vértices que en el grafo son enlazados por aristas: Definition 2.6.4. Dado un grafo G, un camino en G es una lista [v1 , . . . , vn ] tal que 1 ≤ i ≤ n ⇒ vi ∈ V [G], y 1 ≤ i < n ⇒ (vi , vi+1 ) ∈ E[G]. Por ejemplo, en el grafo (2.18), los vértices [1, 3, 4] forman un camino, representado gráficamente como 89:; ?>=< ?>=< ?>=< / 89:; / 89:; (2.23) 1 3 4 La longitud de un camino p, indicada con |p|, es el número de vértices en la lista: el camino p = [v1 , . . . , vn ] tiene longitud |p| = n. Si hay un camino p entre los vertices u y v (o sea, si p = [u, v2 , . . . , vn−1 , v]), se dirá que v es alcanzable desde u a través del camino p p, y se escribirá u → v o, según el caso, simplemente u → v. Un camino es simple si todos los vértices que lo componen son distintos. En el grafo ?>=< 89:; ?>=< / 89:; 1 >^ @2 >> >> >> >> 89:; ?>=< 89:; ?>=< o 3 4 (2.24) los caminos [1, 3] y [1, 2, 4, 3] de 1 a 3 son simples, mientras los caminos [1, 2, 4, 1, 2, 4, 3], [1, 3, 2, 4, 3] o [1, 2, 4, 3, 2, 4, 3] no lo son. Un sub-camino de p = [v1 , . . . , vn ] es una sub-lista de sus vértices, o sea una lista pij = [vi , . . . , vj ], 1 ≤ i ≤ j ≤ n. cahiers d’informatique 22 El grado en entrada (input degree) de un vértice es el número de arcos que llegan en él: in(u) = |{v|v ∈ V ∧ (v, u) ∈ E}| (2.25) y el grado en salida (output degree) es el número de arcos que salen del vértice: out(u) = |{v|v ∈ V ∧ (u, v) ∈ E}| (2.26) la distancia entre dos vértices de un grafo es el número de arcos en el camino de menor longitud que los une: p minp:u→v |p| si u 6= v d(u, v) = (2.27) 0 si u = v Nótese que, a pesar del nombre, d no es una función distancia en el sentido matemático. Por ejemplo, en general no es simétrica. En el grafo ()*+ /.-, ~ _??? ~ ?? ~ ?? ~~ ~ ?? ~ ~ ?>=< ?>=< 89:; / 89:; v u (2.28) hay d(u, v) = 1 y d(v, u) = 2. En un grafo no dirigido, por otro lado, d es una distancia. El diametro de un grafo es el valor máximo de distancia entre vértices del grafo: D[G] = max d(u, v) (2.29) u,v∈V [G] = max min |p| (2.30) p u,v∈V [G] p:u→v Un ciclo (cycle) es un camino que empieza y termina en el mismo vértice. Por ejemplo, en (2.18), son ciclos los caminos [2, , 3, 2], [1, 2, 4, 1], [1, 2, 4, 1, 2, 4, 1], etc. Un ciclo es simple si ningún vértice en él se repite, excepto el primero y el últmo. El ciclo [1, 2, 4, 1, 2, 4, 1] no es simple. Definition 2.6.5. Un ciclo de un grafo G es un camino [v1 , . . . , vn ] de G tal que v1 = vn . Nótese que el siguiente no es un ciclo: 89:; ?>=< 1 ?>=< / 89:; @2 (2.31) 89:; ?>=< 3 La definición de camino no nos permite recorrer arcos "al revés": 1 a 3 y de 3 a 2, pero no hay arco para volver de 2 a 1. se puede ir, p.ej., de Dado un grafo G, su transpuesta GT es el grafo que se deriva de G cambiando la dirección de todas las aristas. Es decir, formalmente: Definition 2.6.6. Dado un grafo G = (V, E), la transpuesta de G es el grafo GT = (V, E T ) con E T = {(v, u)|(u, v) ∈ E} (2.32) grafos 23 El máximo número de arcos que se puede encontrar en un grafo se da cuando cualquier vértice está directamente conectado a todos los demás y a sı́ mismo. Es decir |E[G]| ≤ |V [G]|2 . * * * La definición que se ha dado es la de una familia muy general de grafos2 . Sin embargo, a veces es útil considerar familias más restringidas, que se obtienen imponiendo vı́nculos adicionales a la definición básica. Estas familias son importantes en informática porque hay algoritmos que se aplican sólo a ciertas familias y no a otras, o porque ciertos algoritmos son más eficientes si se aplican a grafos con vı́nculos adicionales. 2.6.1 Grafos dirigidos y no dirigidos Los grafos de la definición general se llaman también grafos dirigidos. Un grafo no dirigido es un grafo G en que la relación E es simétrica, es decir, para cualquier par de vértices u, v, (u, v) ∈ E[G] ⇔ (v, u) ∈ E[G]. En la representación gráfica, esta propiedad implica que si hay un arco que va de u a v, hay otro de v a u. En este caso se puede considerar el arco (u, v) como un arco sin dirección; un arco que nos permite de ir en ambos sentidos. Equivalentemente, se puede considerar la relación E como constituida por pares no ordenados. El grafo del ejemplo ii) es dirigido: dadas dos asignaturas u y v, so (u, v) ∈ E, entonces u es un prerequisito para cursar v. En este caso, claramente, (v, u) 6∈ E, porque si no v también serı́a un prerequisito para cursar u, y no serı́a posible cursar ninguna de las dos asignaturas. Por contra, el grafo del ejemplo iv) es no dirigido: si u tiene la misma nacionalidad que v ((u, v) ∈ E), claramente v tiene la misma nacionalidad que u ((v, u) ∈ E). Gráficamente, un grafo no dirigido se representa con arcos sin flechas. grafo no dirigido que corresponde al grafo dirigido (2.18) es GF ED 89:; ?>=< 89:; ?>=< 2 MM 1; MMM ;; MM ;; ;; 89:; ?>=< 4 ;; rr r r ; r r r 89:; ?>=< 3 Por ejemplo, el (2.33) Las definiciones de camino y ciclo se dan para un grafo no dirigido exactamente como para un grafo dirigido, con la diferencia que en un grafo no dirigido no se admiten arcos que empiezan y terminan en el mismo vértice: cada arco debe ser entre dos vértices distintos. En el caso de grafos no dirigidos, no se puede distinguir entre arcos que entran en un vértice y arcos que salen de un vértice. Consecuentemente no se distingue entre el grado en entrada y el grado en salida de un vértice, definiéndose el grado de un vértice u como el número de arcos que tienen un vértice en u: deg(u) = |{v : (u, v) ∈ E}| = |{v : (v, u) ∈ E}| (2.34) 2 La definición que se da aquı́ se refiere a un grafo simple, en el cual sólo se admite un arco entre dos vértices dados. Una definición un más general es la de multigrafo, en el cual se pueden encontrar varios arcos entre dos vértices dados. En un multi-grafo, E[G] no es un conjunto, sino un multi-conjunto (bag). cahiers d’informatique 24 Nota: En el caso de grafos no dirigidos en general se consideran los dos arcos (u, v) y (v, u) como un solo arco, que se pone en conjunto E como un par no ordenado; en este caso, en la cuenta de los arcos, (u, v) y (v, u) cuentan como uno. Ésto significa, por ejemplo, que en el caso (2.33) es |E| = 6. 2.6.2 Grafos conexos y no conexos Un grafo (dirigido o no) se llama conexo si cualquier par de vértices está conectado por un camino; un grafo dirigido y conexo se llama fuertemente conexo. Definition 2.6.7. Un grafo dirigido G = (V, E) es (fuertemente) conexo si para cada par p u, v ∈ V existe un camino p tal que u → v. El grafo 89:; ?>=< 1 O ?>=< / 89:; 2 89:; ?>=< 3 o 89:; ?>=< 4 (2.35) es fuertemente conexo: se puede ir de cada vértice a cada vértice simplemente dando una vuelta al grafo. El grafo siguiente, por otro lado, no es fuertemente conexo: 89:; ?>=< 1 O ?>=< / 89:; 2 O 89:; ?>=< 3 o 89:; ?>=< 4 (2.36) ya que desde ningún vértice se puede alcanzar el vértice 4. El grafo no dirigido del ejemplo iv) no es conexo, por lo menos si no consideramos personas con doble nacionalidad: desde, por ejemplo, un espa~ nol se podrá llegar a los demás espa~ noles pero no se podrá llegar a ningún italiano. (Aún si se considera la doble nacionalidad, el grafo sigue no conexo, ya que hay paises que no admiten doble nacionalidad.) Según esta definición, el siguiente grafo no es conexo OGF ED ?>=< 89:; ?>=< / 89:; 1 ;] 2 MM O MMM ;; MM& ;; ;; 89:; ?>=< 8 4 ;; rrr r ; r r r 89:; ?>=< 3 (2.37) porque, por ejemplo, no hay camino que conduzca desde vértice 2 hasta el vértice 3. Al contrario, si se ignora el sentido de las flechas, (2.37) se reduce a (2.33), que es conexo. Si un grafo no es conexo (resp. fuertemente conexo), es posible dividirlo en partes (particionarlo) de manera que cada parte sea conexa (resp. fuertemente conexa); estas partes se llaman las componentes conexas (resp. componentes fuertemente conexas ) del grafos 25 grafo. El grafo (2.37), por ejemplo, tiene dos componentes fuertemente conexas: una compuesta por los vértices 1, 2 y 4 y por los arcos que los unen, la otra sólo por el vértice 3. En el grafo del ejemplo iv) (sin doble nacionalidad), cada nacionalidad es una componente conexa. Cada componente, de hecho, representa un tipo particular de grafo: un grafo completamente conexo. En cada nacionalidad, cada persona está conectada directamente con todas las demás personas. Theorem 2.6.1. Si G = (V, E) es conexo y no dirigido, entonces |E| ≥ |V | − 1. Demostración. La demostración es por inducción sobre el número de vértices. Si n = 1 o n = 2 el grafo contiene 0 o 1 arcos, es decir, |V | − 1 arcos, y el teorema es cierto. Supongamos que la propiedad se cumpla para todo grafo con n − 1 vértices, y consideremos un grafo G = (V, E) con |V | = n. Eligimos un vértice u y el grafo inducido por V 0 = V − {u}. Este grafo es un sub-grafo de G formado por n − 1 vértices y, por la hipótesis inductiva, es |E 0 | ≥ |V 0 | − 1. Siendo G conexo, existe por lo menos un arco entre u y un vertice v ∈ V 0 . Claramente (u, v) 6∈ E 0 , porque u 6∈ V 0 , ası́ que E 00 = E 0 ∪ {(u, v)} contiene |E| + 1 arcos, y |E 00 | = |E 0 | + 1 ≥ |V 0 | − 1 + 1 = |V | − 1 (2.38) El grafo (V, E 00 ) es un sub-grafo de G, por lo cual |E| ≥ |E 00 | ≥ |V | − 1. Si G es dirigido y conexo, G contiene por lo menos tantos arcos como su correspondiente no dirigido, por lo cual el teorema se cumple también en el caso de grafos dirigidos fuertemente conexos. 2.6.3 Grafos con ciclos o acı́clicos Un grafo es acı́clico si no contiene ningún camino entre un vértice y si mismo. Solo cabe recordar que una clase muy importante en aplicaciones es la clase de los grafos dirigidos acı́clico, o DAG (Directed Acyclic Graph). En el caso de grafos no dirigidos, no se tienen en cuenta ciclos formados por dos vértices. En el caso siguiente, por ejemplo 89:; ?>=< 1 89:; ?>=< 2 (2.39) es posible ir del vértice 1 a sı́ mismo con el recorrido [1, 2, 1]. Si se consideraran estos ciclos, es evidente que todo grafo no dirigido tendrı́a ciclos, y la definición carecerı́a de interés. Por tanto, en el caso de grafos no dirigidos, estipulamos que un ciclo es un camino que empieza y termina con el mismo vértice y contiene por lo menos tres vértices distintos. Un grafo no dirigido es acı́clico si no contiene ningún camino con estas caraterı́sticas. Con esta definición, el grafo (2.39) y el grafo a la izquierda de (2.40) son acı́clicos, mientras el grafo a la derecha de (2.40) es cı́clico. ?>=< 89:; 1 ?>=< 89:; 3 89:; ?>=< 2 89:; ?>=< 1 89:; ?>=< 3 89:; ?>=< 2 (2.40) cahiers d’informatique 26 La distinción ente grafos dirigidos y no dirigidos es a veces delicada. grafo G = (V, E), con V E Consideremos el = {a, b} = {(a, b), (b, a)} Se puede considerar G como grafo dirigido ((2.41) a la izquierda) o no dirigido ((2.41) a la derecha). % ?>=< 89:; 89:; ?>=< 89:; ?>=< 89:; ?>=< a f a (2.41) b b En el primer caso el grafo tiene un ciclo, en el segundo no. Es necesario siempre clarificar de que tipo de grafo se está hablando, ya que la estructura puede ser ambigua. El grafo del ejemplo ii) es acı́clico: si hubiera un ciclo, nos encontrarı́amos frente a un conjunto de asignaturas mutuamente prioritarias, ası́ que no se podrı́a cursar ninguna de ellas. El grafo no dirigido del ejemplo 4 contiene ciclos (una nación tiene más de 2 ciudadanos). 2.6.4 Grafos con pesos o sin pesos En algunas aplicaciones, es necesario asignar un valor numérico, llamado peso o coste, a cada arco. Esto permitirá afrontar varios problemas como problemas de optimización. En la mayorı́a de los casos que se encontrarán los pesos sólo pueden tomar valores positivos; pero habrá también en las aplicaciones grafos con pesos negativos. Veremos que hay varios problemas que tienen solución en el primer caso y no el el segundo. El peso se representa como una función w : E → R. En un grafo como el del ejemplo i) es útil en muchas aplicaciones a~ nadir una función peso: el peso de una arista es igual a la distancia entre los aeropuertos correspondientes a los vértices conexos. Un ejemplo (con distancias en Km) es JFK vv 3932 vv v vv vv LAX SSS SSSS SSSS SS 10181 SSSS SS 2.6.5 5766 (2.42) MAD H HH HH959 HH 1263 HH LHR 780 CDG 831 FCO 856 BCN Árboles Un árbol libre es un grafo conexo, acı́clico, y no dirigido. ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, Por ejemplo: ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, (2.43) grafos 27 Theorem 2.6.2. Dado un grafo G = (V, E), las seis propiedades siguientes son equivalentes: i) G es un árbol libre; ii) cada par de vértices de G es conexa por uno y un solo camino; iii) G es conexo pero si cualquier arco es eliminado de E, el grafo resultante ya no es conexo; iv) G es conexo y |E| = |V | − 1; v) G es acı́clico y |E| = |V | − 1; vi) G es acı́clico, pero si se a~ nade un arco al conjunto E, el grafo resultante contiene un ciclo. Demostración. Para demostrar las equivalencias es suficiente demostrar las implicaciones i) ⇒ ii) ⇒ iii) ⇒ iv) ⇒ v) ⇒ vi) ⇒ i) i) ⇒ ii) Siendo un árbol conexo, cada par de vértices en V está conectado por un camino simple. Hay que demostrar que este camino es único. Supongamos que no lo es, es decir, que haya al menos un par de vértices u y v unidos por dos caminos simples p1 y p2 . La situacción en este caso es la siguiente: 89:; ?>=< o x k+ )i g' $d o o a! ooo 89:; ?>=< ?>=< ?>=< 89:; ?>=< z o/ o/ /o 89:; v u o/ o/ o/ 89:; w NN NNN ;8x { >~ NN 89:; ?>=< y 3s u5 (2.44) Sea w el vértice donde p1 y p2 se separan por primera vez: w se encuentra en p1 y en p2 , pero en p1 el sucesor de w es x, y en p2 es y, con x 6= y. Sea z el primer vértice en que los caminos vuelven a unirse, es decir el primer vértice que es un sucesor de w ya sea en p1 que en p2 : p1 p2 = = [u, . . . , w, x, . . . , z, . . . , v] [u, . . . , w, y, . . . , z, . . . , v] (2.45) (2.46) Consideremos los sub-caminos p0 p00 = [w, x, . . . , z] = [w, y, . . . , z] (2.47) (2.48) Estos caminos no contienen ningún vértice en común excepto los extremos, por lo cual el camino que se consigue recorriendo p0 seguido por p00 al revés es un ciclo: [w, x, . . . , z, . . . , y, w] pero si G es un árbol no puede contener ciclos: entre u y v nos lleva a una contradición. (2.49) la hipótesis que haya más de un camino cahiers d’informatique 28 ii) ⇒ iii) Siendo cada par de vértices conexo por uno y un solo camino simple (hipótesis ii), G es conexo. Consideremos un arco (u, v). Este es un camino simple entre u y v y, por la hipótesis, es único. Si removemos (u, v) no hay camino entre u y v, luego el grafo sin (u, v) no es conexo. iii) ⇒ iv) La hipótesis es que G sea conexo y que eliminando una arista cualquiera el grafo resultante no es conexo. Siendo el grafo conexo, por el teorema 2.6.1 es |E| ≥ |V | − 1. Demostraremos por inducción que |E| ≤ |V | − 1, lo que implica que |E| = |V | − 1. Es fácil averiguar que un grafo conexo con n = 1 o n = 2 vértices tiene exactamente n − 1 arcos. Supongamos que G tenga n ≥ 3 vértice y que cada grafo con k < n vértices que cumple iii) también cumpla la desigualdad |E| ≤ |V | − 1. G también cumple iii), entonces removiendo un arco el grafo se separa G en dos grafos G1 , G2 con menos de n vértices. Por la hipótesis inductiva, si los sub-grafos tienen k1 y k2 vértices, hay |E[G1 ]| ≤ k1 − 1 y E[G2 ] ≤ k2 − 1 y por tanto |E[G1 ]| + |E[G2 ]| ≤ k1 + k2 − 2 = n − 2 (2.50) A~ nadiendo el arco que habı́amos removido obtenemos |E[G]| = |E[G1 ]| + |E[G2 ]| + 1 ≤ n − 1 = |V | − 1 (2.51) iv) ⇒ v) Supongamos que G sea conexo y que |E| = |V | − 1. Tenemos que demostrar que G es acı́clico. Por contradición, asumimos que G tenga un ciclo con k vértices v1 , . . . , vk . Sea Gk = (Vk , Ek ) el sub-grafo de G que consiste en el ciclo. Notamos que |Ek | = |Vk | = k. Si k = n, el grafo contiene por lo menos n arcos, contradiciendo |E| = |V | − 1. Si k < n, ya que G es conexo, hay un vértice vk+1 ∈ V − Vk adyacente a un vértice vi de Vk . Definimos Gk+1 = (Vk+1 , Ek+1 ) con Vk+1 = Vk ∪ {vk+1 } y Ek+1 = Ek ∪ {(vi , vk+1 )}. Notese que |Ek+1 | = |Vk+1 | = k + 1. Si k + 1 < n podemos definir de forma analoga Gk+2 , Gk+3 , hasta llegar a un grafo Gn con |En | = |Vn | = n. Gn es un sub-grafo de G, lo que supone En ⊆ E, o |En | ≤ |E|, o sea |E| ≥ n = |V |, en contradición con la hipótesis que |E| = |V | − 1. Luego, G es acı́clico. v) ⇒ vi) Supongamos que G es acı́clico y que |E| = |V | − 1. Sea k el número de componentes conexas de G. Cada una es un árbol, ya que es conexa y acı́clica, y la implicación i) ⇒ v) nos dice que el total de arcos en todas las componentes es |V | − k o sea, por la hipótesis, k = 1: el grafo es conexo y consecuentemente es un árbol. Ya que i) ⇒ ii), cada par de vértices de G está conectado por un solo camino simple, ası́ que a~ nadiendo un arco a G se crea un ciclo. vi) ⇒ i) Supongamos que G es acı́clico, y que a~ nadiendo un arco se crea un ciclo. Para demostrar i) tenemos que demostrar que G es conexo. Sean u y v dos vértices arbitrarios de G. Si u y v son adyacentes por supuesto hay un camino entre ellos: el arco (u, v). Si no lo son, a~ nadimos el arco (u, v), lo que por hipótesis crea un ciclo. Todo arco de este ciclo, excepto (u, v), está en E, ası́ que en E hay un camino entre u y v, es decir, G es conexo. grafos 29 Un árbol con raı́z es un árbol donde se ha identificado un vértice (llamado la raı́z del árbol). Formalmente, un árbol con raı́z es un par (G, r), donde G es un árbol, y r ∈ V [G] es la raı́z. Considérese un vértice x en un árbol con raı́z: cualquier vértice y en el camino entre la raı́z y x se llama un antepasado de x y x es un descendiente de y. Si y es un antepasado de x, y (y, x) ∈ E, x es un hijo de y e y es el antecesor de x. Un árbol ordenado es un árbol con raı́z en que se asume un orden entre los hijos de cualquier vértice. 2.6.6 Ejemplos ?>=< / 89:; l A2 , 89:; ?>=< r8 3 r r rrrr ?>=< 89:; ?>=< / 89:; 5 4 I 89:; ?>=< 1 (2.52) /4 /3 Este grafo es dirigido, con ciclos (p.ej. 2 conexo (no hay ningún camino que termine en el vértice 1). El componentes conexas: una representada por los vértices 2, 3 y por el vértice 1 y otra representada sólo por el vértice 5. /2, o 4 / 4 ), y no grafo tiene tres 4, otra representada sólo 89:; ?>=< 1 89:; ?>=< 2 MM MMM MM 89:; ?>=< 3 rr r r r r r 89:; ?>=< 89:; ?>=< 5 4 Este grafo es no dirigido, con ciclos (p. ej. 1 (2.53) 2 89:; ?>=< 1 89:; ?>=< 2 89:; ?>=< 3 89:; ?>=< 4 89:; ?>=< 5 89:; ?>=< 6 4 5 1 ), y conexo. (2.54) Este grafo es no dirigido, acı́clico y no conexo. El grafo tiene dos componentes conexas: una representada por los vértices 1, 2, 4 y 5 y una representada por los vértices 3 y 6. Este grafo es un árbol. 89:; ?>=< 1 89:; ?>=< 2 89:; ?>=< 3 89:; ?>=< 4 89:; ?>=< 5 89:; ?>=< 6 (2.55) cahiers d’informatique 30 2.7 Representación de grafos En esta sección se supondrá siempre que los vértices de un grafo sean identificados con los enteros 1, . . . , n. Hay varias formas de representar grafos en la memoria de un ordenador; muchas de ellas sirven para la solución de problemas especı́ficos, y se consiguen modificando oportunamente una de las dos formas de base que se presentarán en esta sección: las listas de adyacencias y la matriz de adyacencia. 2.7.1 Listas de adyacencia En la representación por listas de adyacencias, los vértices del grafo se almacenan en una cadena (array ) A[1, . . . , n], donde n = |V |. Cada elemento A[i] de la cadena contiene un vértice, y es una estructura con dos elementos: A[i].v contiene el valor del vértice número i, y A[i].a es una lista que contiene los ı́ndices de todos los vértices del entorno de A[i]. El elemento A[a].a se llama la lista de adyacencia (adjacency list) del vértice y se implementa como una lista; sin embargo cabe recordar que el orden en que los vecinos de u aparencen en la lista es irrelevante y la lista se usa para representar lo que, formalmente, es un conjunto. (Se utilizan listas sobre todo porque las listas son fáciles de manejar, pero serı́a posible utilizar cualquiera de las representaciones para conjuntos.) En pseudo-código, las estructuras que se usan para representar un grafo son: type graph is array[1..n] of vertex; type vertex is begin val: nodeval; ady: nodelist; end type nodelist is begin idx: integer; next: →nodelist; end El tipo "nodeval" es cualquier tipo necesario para representar el contenido de un vértice. En los ejemplos, "nodeval" será casi siempre un número entero o una letra; en práctica, "nodeval" será casi siempre un puntero a una estructura apropiada que contendrá los datos que se almacenan en el grafo. La representación por lista de adyacencia del grafo (2.18) es ilustrada esquemáticamente en la figura 2.7.1 La representación por listas de adyacencias se puede extender fácilmente a grafos con pesos. En este caso, el peso del arco (u, v), w(u, v), se almacena junto a la ocurrencia del vértice v en la lista de adyacencias de u, y el tipo "nodelist" cambia en type nodelist is begin idx: integer; peso: real; grafos 31 3 idx = 2 1 v = 1 next = a = idx = 3 - next = 3 idx = 3 2 v = 2 next = a = idx = 4 - next = 3 idx = 4 3 v = 3 next = a = 3 idx = 1 4 v = 4 next = a = Figure 2.6: next: cmdend Representación esquematica de listas de adyacencia. →nodelist; La cantidad de memoria necesaria para representar un grafo G = (V, E) usando listas de adyacencia es O(V + E). La suma del tama~ no de todas las listas de adyacencia es |E| si el grafo es dirigido (dado que cada arco (u, v) aparece una vez el la lista de adyacencia de u) y 2|E| si el grafo es no dirigido (dado que cada arco (u, v) aparece dos veces: una en la lista de adyacencia de u y una en la de v). Dado un grafo con un determinado numero de vértices |V |, esta representación es tanto más económica cuanto más el grafo es disperso. La verificación de la existencia de un arco entre los vértices u y v es con esta representación una operación potencialmente costosa, en cuanto implica la búsqueda del vértice v en toda la lista de adyacencia de u. Si el grafo es denso, el coste de esta operación es O(V ). Si se necesita un acceso rápido a las arista de un grafo denso, es oportuno utilizar la representación por matrices de adyacencia, que ahora vamos a estudiar. 2.7.2 Matriz de adyacencia La matriz de adyacencia de un grafo G = (V, E) es una matriz A = (aij ) de tama~ no |V | × |V | tal que 1 si (i, j) ∈ E aij = (2.56) 0 si no La matriz de adyacencia de un grafo ocupa un espacio de memoria O(V 2 ), ya sea que el grafo sea disperso o bien que sea denso. En un grafo no dirigido un arco entre los vértices i y j existe si, y sólo si, un arco existe entre los vértices j y i, por lo cual, para un grafo no dirigido, aij = aji o sea, la matriz A es simétrica. Esta propiedad puede ser útil para almacenar la matriz en el ordenador ya que, en el caso de una matriz simétrica, es suficiente almacenar la diagonal principal y uno de los dos "triangulos", superior o inferior, por un total de n(n + 1)/2 elementos en lugar de n2 . Por ejemplo, la cahiers d’informatique 32 matriz de adyacencia del grafo (2.18) es 0 0 0 1 Mientras para el grafo (2.33), la 0 1 1 1 1 0 0 0 1 1 0 0 0 1 1 0 (2.57) matriz de adyacencia y su representación reducida son 0 1 1 1 1 1 1 0 1 1 0 1 1 (2.58) 0 1 1 0 1 0 1 1 0 La representación por matriz de adyacencia también se puede extender a grafos con pesos poniendo aij = w(i, j). Nótese que en este caso no se puede poner aij = 0 para indicar la ausencia de la arista (i, j), ya que ésto se confundirı́a con la presencia de una arista de peso 0. En el caso de grafos ponderados, entonces, será necesario definir un sı́mbolo especial, diferente de todas las representaciones de posible pesos, para indicar la ausencia de arco. Este sı́mbolo se indica tradicionalmente con ∞, ya que en muchos algoritmos la ausencia de la arista (i, j) es equivalente a la presencia de una arista (i, j) de peso infinito. Por ejemplo, la representación por matriz de adyacencia del grafo 89:; ?>=< 1 ?>=< / 89:; 1 vv v v 0 zvvv 0.5 89:; ?>=< 89:; ?>=< o 4 3 3 1 (2.59) 2 es dada por la matriz ∞ 1∞ 0 ∞ ∞ 0.5 ∞ ∞ ∞ ∞ ∞ ∞ 2 3 ∞ (2.60) Con esta representación, averiguar la existencia de una arista entre los vértices i y j es O(1), es decir, toma un tiempo constante, independiente del tama~ no del grafo. El precio que se paga para esta velocidad es la ocupación de memoria: la representación ocupa O(V 2 ), independientemente del número de arcos. Este compromiso entre velocidad y memoria es muy común en informática, y volveremos a encontrarlo muchas veces. En general, la representación por matriz de adyacencia es tanto más aprovechable cuanto más denso es el grafo (en un grafo denso el acceso a una arista con listas de adyacencia es O(V ), y la ocupación de memoria O(V + E) es decir, si el grafo es denso y E ≈ V 2 , comparable con la ocupación de la matriz), mientras en el caso de grafos poco densos se desperdicia mucha memoria sin ventajas apreciables sobre la representación por listas. * * * Si un grafo se representa con una matriz de adyacencia A, su transpuesta GT se representa con la transpuesta de A, AT 3 . Si G se representa con listas de adyacencia, su transpuesta se calcula con el algoritmo siguiente, que se ejecuta en un tiempo O(V + E): 3 De hecho, la denominación de transpuesta de un grafo deriva de esta propiedad. grafos transpose(V) V1: array[1,..|V|] of vertex; for i=1 to |V| do V1[i] ← V[i]; V1[i].ady ← []; od foreach u in V do foreach v in u.ady do insert(u, V1[v].ady); od od return V1; 33 cahiers d’informatique 34 2.8 Ejercicios 1. (mat.) ¿Cuántos grafos diferentes se pueden construir con n vértices? grafos no dirigidos? 2. (mat.) ¿Cuántos Demostrar que un grafo no dirigido no tiene ningún ciclo de longitud n < 3. 3. Representar los grafos siguientes con listas de adyacencia y con matriz de adyacencia 89:; ?>=< 89:; / ?>=< 2 h 6 1 O 89:; ?>=< 89:; 3 4= ?>=< == == ?>=< 89:; 5 89:; ?>=< ?>=< 89:; 2 1 JJ JJJttt t t JJ ttt J89:; 89:; ?>=< ?>=< 3 4 89:; ?>=< 1 89:; ?>=< 2 89:; ?>=< 1 89:; ?>=< 6 1T j * 89:; ?>=< 2 ?>=< 89:; 89:; / ?>=< 1= @2 == == ?>=< 89:; @ 3 == == = ?>=< 89:; >=< / ?89:; 4 5 89:; ?>=< 3 89:; ?>=< 4 y 89:; ?>=< 3 I 4. (mat.) En un grafo no dirigido, en grado de un nodo u, deg(u) es el número de arcos que tienen como vértice u. Demuestren que la suma de los grados de todos los vértices de un grafo es igual a dos veces el número de arcos: X deg(u) = 2|E[G]| (2.61) u∈V [G] 5. La propiedad precedente implica que la suma de los grados de los vértices de un grafo no dirigido es siempre par. Usen esta propiedad para demostrar que no es posible que en un grupo de 9 personas cada uno conozca exactamente a tres personas. 6. Dada la representación de un grafo dirigido G en forma de listas de adyacencia, escriban un algoritmo que derive la representación del grafo no dirigido correspondiente. El grafo no dirigido deberá: a) contener un arco entre u y v si el arco (u, v) o el arco (v, u) se encuentra en el grafo G; b) si G contiene ciclos de longitud 1, estos de deberán eliminar del grafo no dirigido. Asuman que las operaciones sobre listas (inserción de un elemento, eliminación, búsqueda) son disponibles. 7. Dada la representación de un grafo por listas de adyacencia, escriban un algoritmo para determinar el grado en ingreso y el grado en salida de cualquer vértice. 8. Repitan el ejercicio precedente con el grafo dado en representación con matriz de adyacencia. grafos 9. Un pozo es un vértice con grado de ingreso |V | − 1 y grado de salida 0, o sea, un vértice en que entran arcos desde todos los demás vertices y no sale ningún arco. Dado un grafo representado con listas de adyacencias, escriban un algoritmo para determinar si el grafo contiene un pozo. 10. Repitan el ejercicio precedente con el grafo dado en representación con matriz de adyacencia. 11. Escriban la representación con listas de adyacencias y con matriz de adyacencia de un árbol binario completo con 7 vértices. 12. Demuestren que si un grafo G es no dirigido se cumple GT = G. 13. Demuestren que un grafo G es acı́clico si y sólo si GT es acı́clico. 14. Calculen el tiempo de ejecución (asintótico) del algoritmo transpose. 35 36 cahiers d’informatique III. ALGORITMOS DE BÚSQUEDA Una componiente fundamental de muchos algoritmo de grafos es un algoritmo para "visitar" de forma sistemática todos los vértices que se puedan alcanzar desde un vértice fuente dado. Los algoritmos de búsqueda permiten esta visita. Como en el caso de los árboles, hay dos tipos de búsqueda que recorren los vértices de manera distinta: la búsqueda en anchura (breadth-first search, o BFS ), y la búsqueda en profundidad (depth-first search, o DFS ). A pesar de su nombre, los algoritmos de búsqueda no se utilizan principalmente para buscar vértices. Si nos interesan, por ejemplo, todos los vértices que cumplen una condición dada, la manera más rápida y sencilla de encontrarlos es mediante una búsqueda en el campo val de la representación por listas de adyacencia, o en la cadena vertex de la representación por matriz de adyacencia. Tal búsqueda es independiente de la estructura del grafo (es decir, de sus aristas). Pero, además de recorrer todos los vértices, los algoritmos BFS y DFS construyen una estructura (el árbol BFS y el árbol DFS, respectivamente) que proporciona información sobre la estructura del grafo y sobre la relación entre vértices. Como veremos, varios algoritmos usan los árboles construidos por los algoritmos de búsqueda para resolver varios problemas, entre ellos: i) la determinación de la existencia de ciclos; ii) la búsqueda de componentes conexas; iii) la determinación del diámetro de un grafo; iv) la búsqueda de caminos mı́nimos; v) la búsqueda de caminos que pasan por todos los vértices; vi) la búsqueda de caminos compuestos por el mı́nimo número de arcos. Hamilton). (caminos de Los algoritmos BFS y DFS para grafos tienen una estructura bastante parecida a sus homónimos para árboles. Sin embargo, hay diferencias estructurales importantes entre grafos 37 árboles y grafos que hacen que los sencillos algoritmos de búsqueda en árboles no funcionen en grafos. Los algoritmos BFS y DFS en árboles funcionan gracias a una importante propiedad de los árboles: dado cualquier vértice v, existe uno y un solo camino que une la raı́z del árbol a v. Esta propiedad no se extiende a otros tipos de grafos: dado un grafo, y identificado un nodo "fuente", s, generalmente hay varios caminos entre s y un vértice dado v. A causa de esta multiplicidad, si nos pusiéramos a explorar un grafo con el algoritmo BFS para árboles, correrı́amos el riesgo de visitar el mismo vértice muchas veces. Si el grafo contiene ciclos, podrı́amos pasar por el mismo vértice un número infinito de veces. Aplicando el algoritmo BFS-árboles a un grafo, es posible que la ejecución del algoritmo no termine. El caso del algoritmo DFS es análogo. Como veremos, para garantizar que cada vértice se visite una sola vez, los algoritmos BFS y DFS para grafos tendrán que "marcar" los vértices a medida de que se visitan, a~ nadiendo a todo vértice un estado. 3.9 Búsqueda en anchura Dado un grafo G, y un vértice fuente s, el algoritmo BFS explora los arcos de G llegando a cualquier vértice alcanzable desde s; en el curso de esta operación, BFS calcula la distancia (en numero de arcos cruzados) de s a cada vértice alcanzable, y determina un árbol (el árbol BF ) que contiene todos los vértices visitados y tiene s como raı́z. Para cada vértice v alcanzable desde s el camino entre s y v en el árbol corresponde al camino más corto en el grafo entre los mismos vértices. El algoritmo funciona en grafos dirigidos y no dirigidos; el nombre de búsqueda en anchura le deriva del hecho de que el algoritmo visita todo vértice a distancia k de la fuente antes de empezar la visita de los vértices a distancia k + 1. El algoritmo extiende uniformemente la frontera entre la parte del grafo analizada y la parte todavı́a desconocida. Para su funcionamiento, el algoritmo BFS asocia a cada vértice del grafo tres atributos: un estado, una distancia, y un antecesor, que definimos a continuación: Estado: un vértice se puede encontrar principio, todos los vértices se encuentra en estado V; cuando un exploración, su estado cambia de en estado visitado (V) o no visitado (N). Al encuentran en estado N, excepto la fuente, que se vértice se encuentra por primera vez en la N a V. Distancia: el atributo d[u] del vértice u contiene la distancia (expresada en número de arcos cruzados) entre u y s. Al principio, se pone d[u] = ∞ para u 6= s, y d[s] = 0. Antecesor: el desde s. juega el atributo algoritmo construye un árbol que contiene todos los vértices alcanzables Este árbol es identificado guardando, por cada vértice u, el vértice que papel de su antepasado en el árbol; tal antepasado se almacena en el π[u]. Al principio se pone π[u] = nil para cada vértice u. La representación por listas de adyacencia de este grafo "aumentado" supone la siguiente variación de la representación de la sección precedente: type vertex is begin val: nodeval; cahiers d’informatique 38 ady: nodelist; d: integer; estado : {V, N}; π: integer; end Los otros tipos En esta sección representacción con matrices de no cambian. La representación por matriz se cambia de manera análoga. y en las siguientes, los algoritmos se presenterán con referencia a la por listas de adyacencia. Las modificaciones necesarias para utilizarlos adyacencia son mı́nimas. El algoritmo usa una cola fifo (first in, first out), para determinar el orden de procesamiento de los vértices: BFS(G, s) 1. Q: queue of vertex; 2. foreach u in V[G] do 3. estado[u] ← N; 4. d[u] ← ∞; 5. π[u] ← nil; 6. od 7. estado[s] ← V; 8. d[s] ← 0; 9. Q.insert(s); 10. while ¬empty(Q) do 11. u ← Q.extract; 12. foreach v in u.ady do 13. if estado[v] = N then 14. estado[v] ← V; 15. d[v] ← d[u] + 1; 16. π[v] ← u; 17. Q.insert(v); 18. fi; 19. od 20. od A continuación, se explicará el funcionamiento del algoritmo con referencia al ejemplo de la figura 3.7. El estado de cada vértice se representará mediante su forma: los vértices que todavı́a no se han visitado se representarán con un cı́rculo sencillo y los que se han visitado con un cı́rculo doble. Dentro del cı́rculo que representa un vértice u se pondrá el valor d[u] como resulta en cada paso del algoritmo; los antepasados se representan con la dirección de las flechas en doble lı́nea (la dirección de la flecha es desde el antepasado al hijo (es decir π[u] ⇒ u). Las lı́neas 1--9 del algoritmo nos dejan en la situación en (a): todo vértice v 6= s se inicializa con d[v] = ∞, se pone d[s] = 0, y nadie tiene antepasado. Cada una de las figuras (b)--(i) representa el resultado de una ejecución del bucle while. En (b), por ejemplo, se ha sacado s de la cola, se ha cambiado el estado, la distancia, y el antepasado de cada uno de sus hijos (los vértices t y w), y se han puesto estos últimos vértices en la cola. En (c) se ha sacado t de la cola, se ha puesto la distancia de su hijo x a d[x] = d[t] + 1 = 2, se ha puesto el antepasado de x a π[x] = t, y se ha cambiado el estado de x. Además, se ha puesto x en la cola. grafos 39 s ?>=< 89:; ()*+ /.-, 0 (a) ?>=< 89:; ∞ ?>=< 89:; ∞ w ?>=< 89:; ()*+ /.-, 0 w ?>=< 89:; ()*+ /.-, 0 (e) w s ?>=< 89:; ()*+ /.-, 0 (g) w x ?>=< 89:; 89:; ?>=< ∞ ∞ ::: :: :: :: ?>=< 89:; ()*+ /.-, ?>=< 89:; 89:; ?>=< ∞ ∞ 2 s 89:; ?>=< ()*+ /.-, 0 t y z w x u v s t ?>=< 89:; ()*+ /.-, 1 s ?>=< 89:; ()*+ /.-, 0 ?>=< 89:; ()*+ /.-, 1 w Figure 3.7: u v ?>=< 89:; ()*+ /.-, 0 89:; ?>=< ()*+ /.-, 1 89:; ?>=< ()*+ /.-, 0 ?>=< ()*+ /.-, +3 89:; 1 ?>=< ()*+ /.-, +3 89:; 1 ?>=< ()*+ /.-, +3 89:; 1 z w x u v s t 89:; ?>=< ()*+ /.-, 0 89:; ?>=< ()*+ /.-, 1 y z w 89:; ()*+ /.-, +3 ?>=< 1 u v x ∅ y z x [y, v] t y [t, w] u y [x, z] u ?>=< ()*+ /.-, +3 89:; 1 y [u, y] u v y [v] (d) z v (f ) z v 89:; ?>=< ()*+ /.-, ?/.-, ()*+ >=< +3 89:; 4 ?G 3 66 6 66 66 6 89:; ?>=< ()*+ /.-, 89:; ? ()*+ /.-, >=< 89:; ?>=< ()*+ /.-, 3 + 2 3 4< 2 x (b) z 89:; ?>=< ()*+ /.-, 89:; ?>=< ∞ ?G 3 88 8 88 88 8 89:; ?>=< ()*+ /.-, ?/.-, ()*+ >=< 89:; ?>=< ()*+ /.-, +3 89:; 2 3 3; 2 y ?/.-, ()*+ >=< ?>=< 89:; ()*+ /.-, +3 89:; 4 ?G 3 66 6 66 66 6 ?>=< 89:; ()*+ /.-, ? 89:; ()*+ /.-, >=< 89:; ?>=< ()*+ /.-, 3 + 2 3 4< 2 v 89:; ?>=< 89:; ?>=< ∞ ∞ ::: :: :: :: 89:; ?>=< ()*+ /.-, 89:; ?>=< ?>=< ()*+ /.-, ∞ 2 2 3; 89:; 89:; ?>=< ()*+ /.-, 1 89:; ()*+ /.-, +3 ?>=< 1 u 89:; ?>=< 89:; ?>=< ∞: ∞ : : :: :: :: 89:; ?>=< 89:; ?>=< 89:; ?>=< ∞ ∞ ∞ ?>=< 89:; ()*+ /.-, 89:; ?>=< ∞ ?G 3 88 8 88 88 8 ?>=< 89:; ()*+ /.-, ()*+ /.-, >=< 89:; ?>=< ()*+ /.-, +3 ?89:; 2 3 3; 2 x [z, u, y] t w (i) z 89:; ()*+ /.-, +3 ?>=< 1 ?>=< 89:; ()*+ /.-, 1 t y x [w, x] t s s 89:; ?>=< ()*+ /.-, 1 89:; ()*+ /.-, +3 ?>=< 1 ?>=< 89:; ()*+ /.-, 1 v ?>=< 89:; 89:; ?>=< ∞; ∞ ; ; ; ;; ;; ; ?>=< 89:; ?>=< 89:; 89:; ?>=< ∞ ∞ ∞ x [s] t s (c) u t (h) z ?>=< 89:; ()*+ /.-, ?/.-, ()*+ >=< +3 89:; 4 ?G 3 66 6 66 66 6 ?>=< 89:; ()*+ /.-, ()*+ /.-, >=< 89:; ?>=< ()*+ /.-, +3 ?89:; 2 3 4< 2 Las fases sucesivas del funcionamiento del algoritmo BFS cahiers d’informatique 40 Nótese que al paso (g) el grafo ya está en su estado final: todos los vértices han sido alcanzados y han cambiado de estado. Sin embargo, el algoritmo no termina, ya que todavı́a hay dos vértices en la cola. Los dos vértices son analizados en los pasos (h) y (i) pero, como ningún vecino de ellos se encuentra en estado N, el test de la linea 13 del algoritmo no se cumple nunca, ası́ que estas iteraciones no alteran el estado del grafo. 3.9.1 Análisis del tiempo de ejecución El algoritmo contiene dos bucles anidados: el bucle while de la lı́nea 10 y, dentro de éste, el bucle que empieza por el foreach de la lı́nea 12. El bucle while se ejecuta un número de veces igual al número de vértices del grafo, por tanto la lı́nea 11 se ejecuta un número de veces O(V ). El bucle foreach de la lı́nea 12 también se ejecuta O(V ) veces. Cada bucle foreach, por otro lado, necesita un tiempo igual al tama~ no de de la lista de adyacencia del vértice correspondiente. Es decir, el número de veces que se ejecuta cada ciclo foreach depende de la naturaleza del grafo. Pero este ciclo se ejecuta una vez para cada vértice, y sabemos que el tama~ no total de las listas de adyacencias es igual al número de arcos. Es decir, aún si no sabemos cuantas veces las lineas 13--18 se ejecutarán para cada ciclo, sabemos que en total se ejecutarán un número de veces O(E). Formalmente, la ejecución del bucle foreach para el vértice u toma un tiempo O(|ady(U )|) y, siendo X |ady(U )| = |E| (3.62) u∈V el tiempo necesario para ejecutar el bucle en todos los vértices es O(E). el tiempo total de ejecución del algoritmo BFS es O(V + E). 3.9.2 Por lo tanto, Corrección del algoritmo El algoritmo BFS visita todos los vértices alcanzables desde la fuente s, determina el camino más corto entre la fuente y cada vértice y construye, con estos caminos, un árbol que contiene todos los vértices visitados. La distancia de camino más corto (shortest path distance) entre la fuente s y un vértice v se indicará con δ(s, v) y se define como el número mı́nimo de arcos en todos los caminos entre s y v, o ∞ si no existe camino entre s y v (se vea la definición (2.27), a la página 22. Un camino entre s y v consistente en δ(s, v) arcos es un camino más corto (shortest path). Antes de demostrar el teorema fundamental de esta sección, que demuestra que el algoritmo BFS calcula el camino más corto entre s y cualquier vértice, será necesario demostrar unos cuantos lemas preliminares. En estos lemas siempre se hará referencia a un grafo G = (V, E), dirigido o no dirigido (el teorema vale en ambos casos). Lemma 3.9.1. Sea s un vértice de G. Para todo arco (u, v) ∈ E se cumple δ(s, v) ≤ δ(s, u) + 1 (3.63) Demostración. Si u es alcanzable, v también es alcanzable, y δ(s, v) + 1 es la distancia entre s y u que se consigue recorriendo el camino más corto de s a v y luego cruzando el arco (u, v). El camino más corto de s a v no puede ser (por definición) más largo que este recorrido. Si u no es alcanzable desde s, δ(s, u) = ∞, y el lema se cumple independientemente del valor δ(s, v). grafos 41 Lemma 3.9.2. Supongamos que se ejecute BFS en G con fuente s. Al final del algoritmo, para todo vértice v se cumple d[v] ≥ δ(s, v), donde d[v] es el valor de la distancia calculado por el algoritmo BFS. Demostración. La demostración se realiza por inducción sobre el número de veces que se ha introducido un vértice en la cola Q, es decir, sobre el número de vértices que se introducen en la cola a lo largo del algoritmo. Sea q este número. i) Sea q = 0; esta es la situación en que el algoritmo se encuentra tras ejecutar la lı́nea 9. En este caso d[s] = 0 = δ(s, s) y, para cada vértice v 6= s, d[v] = ∞ ≥ δ(s, v). ii) Supongamos que el lema se cumple para los primeros q vértices que se han introducido en la cola, y que v es el vértice que se encuentra en este punto durante la búsqueda en el entorno de un vértice u. Para llegar a v, el vértice u se ha extraı́do de la cola en el paso 11, es decir, se ha insertado en la cola como uno de los primeros q vértices. La hipótesis inductiva nos dice que el lema se cumple para u, o sea que d[u] ≥ δ(s, u). Entonces: d[v] = d[u] + 1; ≥ δ(s, u) + 1 ≥ δ(s, v) (asignado en el paso 15) (hipótesis de inducción) (lema 3.9.1) (3.64) Una vez que el vértice v se ha introducido en la cola, no se va a introducir nunca más, porque en el paso 14 se le ha cambiado su estado, y el if del paso 13 sólo permite la introdución de vértices cuyo estado es N, por tanto el valor d[v] no se vuelve a cambiar y al final del algoritmo sigue cumpliéndose d[v] ≥ δ(s, v). Lemma 3.9.3. Supongamos que, en un determinado momento de la ejecución de BFS, sea Q = [v1 , . . . , vr ], siendo v1 la cabecera de la cola fifo y vr el último elemento que se ha insertado. Entonces, i) d[vr ] ≤ d[v1 ] + 1 y ii) para i = 1, . . . , r − 1 se cumple d[vi ] ≤ d[vi+1 ]. Demostración. La demostración es por inducción sobre el número de operaciones que se efectúan en la cola. Inicialmente, la cola sólo contiene s, y el lema se cumple banalmente. Supongamos ahora que el lema se cumple tras q operaciones. Debemos demostrar que el lema sigue válido tras la operación q + 1, la cual puede ser la inserción o la extracción de un vértice. Consideremos primero el caso en que la operación número q + 1 es la extracción de un vértice de la cola. Si después de esta extracción la cola queda vacı́a, el lema se cumple banalmente. Si no, v2 es la nueva cabecera. Para el punto i), se ha d[vr ] ≤ d[v1 ] + 1 ≤ d[v2 ] + 1 (la segunda relación deriva de la hipótesis de inducción); la propiedad ii) no cambia a causa de esta extracción, siendo válida tras la operación q, sigue válida tras la q + 1. Consideremos ahora el caso en que se está introduciendo un vértice. En este caso, tenemos que considerar el historial del estado de la cola. Cuando se inserta un vértice cahiers d’informatique 42 v en la cola (en el paso 17), se está analizando el entorno de un vértice u y, claramente, u es el último vértice que se ha extraı́do de la cola. La secuencia de operaciones en la cola, en este punto, es la siguiente: a) b) u [u, v1 , . . . , vp ] [v1 , . . . , vp ] . . . situación antes de extraer u justo después de extraer u c) d) u u [v1 , . . . , vp , . . . , vr ] [v1 , . . . , vr , vr+1 = v] se insertan unos vértices del entorno de u ésta es la situación en el paso q éste es el paso q + 1 (En la última lı́nea se ha introducido el vértice v en la cola llamándolo vr+1 por coherencia con la notación previa, y para conseguir una notación más clara en lo siguiente.) Por la hipótesis de inducción, hasta la operación número q las propiedades i) y ii) se cumplen. Es decir, i) y ii) se cumplen para las colas en a), b), y c). En particular, la cola c) garantiza que la propiedad ii) se cumpla para los vértices v1 , . . . , vr . La inserción de vr+1 en d) no cambia los valores de distancia de estos vértices ası́ que en la cola d) sigue cumpliéndose d[v1 ] ≤ d[v2 ] ≤ · · · ≤ d[vr ] (3.65) Para demostrar el teorema, tenemos que demostrar que i) y ii) se cumplen en la cola d). Para demostrar i) tenemos que demostrar que d[vr+1 ] ≤ d[v1 ] + 1; (3.66) para demostrar ii), gracias a la validez de (3.65), será suficiente demostrar que d[vr ] ≤ d[vr+1 ] (3.67) Observamos que vr+1 se analiza como parte del entorno de u y por tanto, en el paso 15, se le asigna un valor d[vr+1 ] = d[u] + 1. (3.68) Consideremos ahora la situación en a). Por la hipótesis de inducción, vale d[u] ≤ d[v1 ] y por tanto d[vr+1 ] = d[u] + 1 ≤ d[v1 ] + 1 (3.69) Esto demuestra que la propiedad i) se cumple para la cola d). Para la demostración del punto ii), tenemos que distinguir dos casos. Supongamos primero que vr+1 es el primer vértice del entorno de u que se inserta en la cola. Entonces las colas a) y c) coinciden, y la nueva cola es [v1 , . . . , vp , vr+1 ]. Ya que la propiedad ii) se cumple en la cola a), se da d[vr ] ≤ d[u] + 1 = d[vr+1 ] (3.70) y el punto ii) es demostrado. Supongamos ahora que vr+1 no es el primer vértice del entorno de u que se ha introducido en la cola, sino que antes se han introducido vp+1 , . . . , vr . Como todos estos vértices se encuentran en el entorno de u, en el paso 15 todos han recibido el mismo valor de d, o sea d[vp+1 ] = d[vp+2 ] = · · · = d[vr ] = d[vr+1 ] = d[u] + 1 la igualdad d[vr ] = d[vr+1 ] demuestra el punto ii) en este último caso. (3.71) grafos 43 Este lema establece que, en cada momento, sólo hay dos valores distintos de d en la cola. Si hubiera, digamos, 3, entonces a causa de la condición ii) serı́a d[vr ] ≥ d[v1 ] + 2, y la condición i) no se cumplirı́a. Este lema es importante para entender el funcionamiento del algoritmo porqué nos garantiza que el algoritmo no "profundiza" demasiado. En particular, nos garantiza que, antes de insertar un vértice a distancia d + 2 de la fuente, se habrán extraido todos los vértices a distancia d. Es decir, el algoritmo funciona en "capas": se extraen los vértices a distancia d y se van insertando sus vecinos a distancia d + 1. Sólo cuando todos los vértices a distancia d se hayan extraido, se empezarán a extraer los vértices a distancia d + 1, insertando contemporaneamente en la cola sus vecinos a distancia d + 2, y ası́ sucesivamente. Lemma 3.9.4. Ejecútese el algoritmo BFS en el grafo G a partir de una fuente s ∈ V [G], y sea v ∈ V [G] un vértice no alcanzable desde s. Entonces, a lo largo de su ejecución el algoritmo BFS no recorre v, o sea, no ejecuta las operaciones 14--17 sobre v. Demostración. Supongamos por contradicción que el algoritmo recorra v. Siendo v no alcanzable desde s, es δ(s, v) = ∞ y, por el lema 3.9.2, d[v] = ∞. Si el algoritmo recorriera v entonces, al paso 15 le asignarı́a un valor d[v] = d[u] + 1 = ∞, lo que supone que también u, el antepasado de v en el árbol BF, tendrı́a d[u] = ∞. Si v se ha visitado y se ha puesto π[v] = u en el paso 16, esto se ha hecho como parte del análisis del entorno de u después que u se ha extraı́do de la cola, por lo tanto, u también se ha visitado y se ha puesto d[u] = ∞. Siguiendo el mismo razonamiento, el antepasado de u también se ha visitado y tiene distancia ∞, y ası́ sucesivamente. Sea x el primer vértice al que se le asigna d[x] = ∞. Siendo d[s] = 0, tiene que ser x 6= s. Entonces, el valor d[x] fue asignado en el paso 15 y, razonando como en el caso de v, para que esto suceda, el antepasado de x, y, tiene que tener d[y] = ∞ y este valor tiene que haberse asignado al paso 15. Esto contradice la hipótesis de que x sea el primer elemento al que se asigna d[x] = ∞. Luego, la hipótesis que el algoritmo recorre v es falsa. Lemma 3.9.5. Ejecútese el algoritmo BFS en el grafo G a partir de una fuente s ∈ V [G]. Para cada v ∈ V [G], las operaciones siguiente se ejecutan como máximo una vez a lo largo de la ejecución del algoritmo: i) se pone estado[v] ← V ; ii) se pone d[v] = δ(s, v); iii) si v 6= s se pone π[v] = u, con u ∈ V [G] y δ(s, u) = δ(s, v) − 1; iv) se inserta v en la cola. (es decir, los pasos 14--17 del algoritmo se ejecutan como máximo una vez para cada vértice). Demostración. Si v no es alcanzable desde s entonces, por el lema 3.9.4, no se recorre, y las operaciones i)--iv) (pasos 14--17) no se ejecutan nunca. Si v es alcanzable desde s y las operaciones se ejecutan una vez, podemos razonar como en la demostración del lema 3.9.2: antes de poner v en la cola, su estado se pone igual a V y, desde entonces, el if de la lı́nea 13 impidirá que se vuelvan a ejecutar las lı́neas 14--17 para v. 44 cahiers d’informatique Una vez establecidos estos lemas, podemos proceder a demostrar el teorema principal de esta sección, o sea la correción del algoritmo BFS. Theorem 3.9.1. Supóngase que el algoritmo BFS se ejecute en el grafo G a partir de una fuente s. Entonces, a lo largo de su ejecución, BFS recorre todos (y sólo) los vértices accesibles desde s. Al final, para cada v ∈ V , se cumple d[v] = δ(s, v). Para cualquier v accesible desde s con v 6= s, uno de los caminos más corto4 desde s a v es el camino más corto desde s a π[v] seguido por el arco (π[v], v). Demostración. Si el vértice v no es alcanzable desde s no se recorre por el lema 3.9.4 y el lema 3.9.2 garantiza que, al final, d[v] = δ(s, v) = ∞. Consideramos entonces sólo los vértices alcanzables desde s. Sea Vk = {v ∈ V [G]|δ(s, v) = k} el conjunto de vértices a distancia k de s. Queremos demostrar que para cada v ∈ Vk las cuatro operaciones del lema 3.9.5 i) se pone estado[v] ← V ; ii) se pone d[v] = δ(s, v); iii) si v 6= s se pone π[v] = u, con u ∈ V [G] y δ(s, u) = δ(s, v) − 1; iv) se inserta v en la cola. se ejecutan una y sólo una vez. El lema 3.9.5 demuestra que estas operaciones se ejecutan como máximo una vez. Ahora tenemos que demostrar que se ejecutan al menos una vez para todo v ∈ Vk . La demostración se hace por inducción sobre k. Para k = 0, el único vértice a distancia 0 de la fuente es la misma fuente, por tanto V0 = {s}. La lı́nea 7 pone el estado de s a V , la lı́nea 8 pone d[s] = 0, y la lı́nea 9 inserta s en la cola (la operación iii) no nos interesa en este caso, ya que se ejecuta sólo si v 6= s): el teorema queda demostrado para k = 0. Supongamos ahora que las propiedades i)--iv) se cumplen para todo u ∈ Vk−1 . Nótese que la cola no se encuentra nunca vacı́a hasta el final del algoritmo y que, por el lema 3.9.5, una vez que un vértice se ha insertado en la cola, sus atributos d[u] y π[u] no cambian. Entonces, por el lema 3.9.3, si los vértices se insertan en la cola en el orden u1 , . . . , un , se cumple d[u1 ] ≤ d[u2 ] ≤ · · · ≤ d[un ] (3.72) es decir, los vértices se insertan en la cola en orden no decreciente de distancia. Consideremos v ∈ Vk arbitrario. Por el lema 2, al final del algoritmo se cumple d[v] ≥ k y, ya que el valor d[v] no se asigna más de una vez, d[v] ≥ k se cumple a lo largo de la ejecución. Para la hipótesis de inducción, para todo u ∈ Vk−1 es d[u] = k − 1 entonces, para la (3.72), si v se visita---y consecuentemente se inserta en la cola---se visita sólo tras haber visitado todo vértice u ∈ Vk−1 . Siendo δ(s, v) = k hay un camino de k arcos entre s y v y este camino, justo antes de llegar a v, tiene que pasar por un vértice u que se encuentra a distancia k − 1 de s, es decir, por un vértice u ∈ Vk−1 tal que (u, v) ∈ E. En general, existirán varios vértices que cumplen con estos requisitos; asumamos que u es el primero de estos vértices que se visita y se pone en la cola. 4 En un grafo pueden existir varios caminos de distancia mı́nima entre dos vértices: encontrará uno de ellos. el algoritmo BSF grafos 45 El algoritmo termina sólo cuando la cola se encuentre vacı́a, por lo cual antes del final del algoritmo el vértice u tendrá que extraerse de la cola, su entorno se analizará, y se descubrirá el vértice v. ¿Cuál es el estado de v en este momento? Para que el estado de v fuera V , v tendrı́a que estar en el entorno de un vértice que ya se ha extraı́do de la cola, pero: i) v no está en el entorno de ningún w ∈ Vj con j < n − 1. Si ası́ fuera, v se encontrarı́a en Vj+1 , contra la hipótesis que v se encuentre en Vk ; ii) v no se encuentra en el entorno de ningún w ∈ Vk−1 que ya se ha extraı́do de la cola porque hemos asumido que u es el primer vértice de Vk−1 en cuyo entorno se encuentra v que se extrae de la cola. Entonces, cuando se extrae u de la cola, se cumple estado[V ] = N . En este caso, la condición del if de la lı́nea 13 se cumple, y las lı́neas 14--17 se ejecutan, cumpliendo las operaciones i)--iv). Ya que v es arbitrario, la propiedad se cumple para todo v ∈ Vk . Esto termina la demostración por inducción. Esta y el lema 3.9.5 garantizan que las operaciones i)--iv) se ejecutan una y sólo una vez para cada vértice v alcanzable desde s. La operación ii) demuestra que, al final del algoritmo, es d[v] = δ(s, v). Para terminar la demostración observamos que, siendo π[v] ∈ Vk−1 , se puede encontrar el camino más corto hasta v tomando el camino más corto desde s hasta π[v] (con k − 1 arcos) y luego cruzando el arco (π[v], v), por un total de k arcos. 3.10 Árboles BF Tras la ejecución del algoritmo BFS, los vértices alcanzables desde s y los arcos (π[v], v), forman un sub-grafo de G con propiedades importantes. Las propiedades que nos interesan se resumen en la definición siguiente: Definition 3.10.1. Dado un grafo G = (V, E) y un vértice de salida s, un sub-grafo de G G0 = (V 0 , E 0 ) es un árbol BF si G0 es un árbol con raı́z s, V 0 está formado por todos los vértices accesibles desde s y para todo v ∈ V 0 el camino (único) desde s a v en G0 es además un camino de distancia mı́nima entre s y v en G. El teorema siguiente guarantiza que la estructura construida por el algoritmo BFS cumple estas propiedades. Theorem 3.10.1. Dado un grafo G = (V, E), y un vértice s ∈ V , sea el grafo Gπ = (Vπ , Eπ ) definido por Vπ Eπ = {v ∈ V : π[v] 6= nil} ∪ {s} = {(π[v], v) ∈ E : v ∈ Vπ − {s}} donde el atributo π[v] se ha asignado durante la ejecución de BFS(G, s). un árbol BFS. (3.73) Entonces, Gπ es Demostración. G es un árbol, porque es conexo (se puede derivar de la demostración de la correción del algoritmo BFS) y |Eπ | = |Vπ | − 1 (propiedad iv del teorema 2.6.2 a la página 27). La linea 16 del algoritmo BFS pone π[v] = u si y sólo si (u, v) ∈ E y δ(s, u) < ∞, o cahiers d’informatique 46 sea, si y sólo si v es accesible desde s, ası́ que Vπ está formado por todos los vértices accesibles desde s. Siendo Gπ un árbol, hay un solo camino desde s a cada v ∈ Vπ , y aplicando inductivamente la parte final de la demostración del teorema precedente, se demuestra que el camino es mı́nimo. La función print path recibe un grafo al que se ha aplicado el algoritmo BFS, e imprime el camino más corto entre la fuente s y un vértice dado v. print path(G, s, v) 1. if v = s then print s 2. else 3. if π[v] = NIL then 4. print "no path"; 5. else 6. print path(G, s, π[v]); 7. print v; 8. fi el tiempo de ejecución del algoritmo es lineal en el número de vértices en el camino más corto entre s y v, ya que cada llamada es para un camino más corto de un vértice. En el peor de los casos, el tiempo de ejecución es O(V ). 3.10.1 Variantes Definición implı́cita del estado En la práctica, es posible simplificar un poco la estructura de datos que se utiliza para la ejecución del algoritmo BFS. La introducción del estado de un vértice facilita la demostración de las propiedades del algoritmo, pero podemos observar que es, de alguna forma, redundante. Al principio del algoritmo se pone para cada vértice d[v] = ∞ y estado[v] = N . El teorema precedente nos dice que el estado de v cambia una sola vez a lo largo de la ejecución y cuando se cambia el estado de N a V también se pone d[v] = δ(s, v). Es decir, las dos proposiciones estado[v] = N estado[v] = V ⇔ d[v] = ∞ ⇔ d[v] < ∞ (3.74) (3.75) se cumplen a lo largo de la ejecución. Entonces, es posible usar el valor d[v] como indicador de estado: si d[v] = ∞ todavı́a no se ha visitado el vértice v (su estado es N ), mientras que si d[v] < ∞ el vértice ya se ha visitado (su estado es V ). El algoritmo se transforma como sigue: BFS-NoEst(G, s) 1. Q: queue of vertex; 2. foreach u in V[G] do 3. d[u] ← ∞; 4. π[u] ← nil; 5. od 6. d[s] ← 0; grafos 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 47 Q.insert(s); while ¬empty(Q) do u ← Q.extract; foreach v in u.ady do if d[v] < ∞ then d[v] ← d[u] + 1; π[v] ← u; Q.insert(v); fi; od od Determinación de todos los caminos Ya hemos observado precedentemente que en general el camino mı́nimo de la fuente s a un vértice v no es único. Por ejemplo, en el grafo 89:; ?>=< t ? ??? ?? ?? ?? ?>=< 89:; 89:; ?>=< s@ ?~ v @@ @@ ~~ @@ ~~ ~ @@ ~~ ~ 89:; ?>=< u (3.76) ?>=< / 89:; w hay un camino mı́nimo (de longitud 1) de s a t y de s a u, pero hay dos caminos mı́nimos, de longitud 2, de s a v---el camino [s, t, v] y el camino [s, u, v]---y dos, de longitud 3, de s a w---los caminos [s, t, v, w] y [s, u, v, w]. En este caso el algoritmo BFS retorna uno de los caminos. El camino elegido depende del orden en que se analizan los vecinos de un vértice en el foreach de la lı́nea 12 (la lı́nea 10 en el algoritmo modificado BFS-NoEst). Una modificación relativamente sencilla de BFS permite determinar todos los caminos mı́nimos. La idea es guardar, para cada vértice, en vez de un solo antepasado, una lista de antepasados, uno para cada camino mı́nimo que converge en el vértice. En el grafo (3.76), por ejemplo, se pondrı́a π[s] = [] π[t] = [s] π[u] = [s] π[v] = [t, u] π[w] = [v] (3.77) (3.78) Supongamos que se ha extraı́do de la cola un vértice u a distancia d[u] de la fuente y que se está analizando su entorno. Durante este análisis se encuentra el vértice v en estado V . Esto quiere decir que ya se ha encontrado un camino corto hasta v y por tanto, en el algoritmo estándar, no se vuelve a visitar v y no se cambia el valor de π[v]. Por contra, en este caso, tenemos que fijarnos en el valor d[v]. Si d[v] = d[u] + 1, entonces u nos proporciona otro camino mı́nimo de s a v (la longitud del camino de s a v que pasa por u es igual a la longitud del camino hasta u más el arco (u, v), o sea d[u] + 1 que, siendo 48 cahiers d’informatique igual al valor d[v] es también igual a δ(s, v).), por tanto, se a~ nade u a la lista π[u] de antepasados de v. Si el estado de v es N en el momento en que se descubre, se procede como en el algoritmo estándar. BFS-All(G, s) 1. Q: queue of vertex; 2. foreach u in V[G] do 3. estado[v] ← N; 4. d[u] ← ∞; 5. π[u] ← []; 6. od 7. estado[s] ← V; 8. d[s] ← 0; 9. Q.insert(s); 10. while ¬empty(Q) do 11. u ← Q.extract; 12. foreach v in u.ady do 13. if estado[v] = N then 14. estado[v] ← V; 15. d[v] ← d[u] + 1; 16. π[v] ← u; 17. Q.insert(v); 18. elseif d[v] = d[u] + 1 then 19. π[v] ← append(π[v], [u]); 20. fi 21. od 22. od Las operaciones que se han a~ nadido, lı́neas 18 y 19, se ejecutan, el el peor de los casos, un número de veces igual a la suma de los tama~ nos de todas las listas de adyacencia, o sea, un número de veces O(E). Por tanto esta modificación no cambia la complejidad del algoritmo, que sigue siendo O(V + E). A continuación se demuestran las propiedades del algoritmo, y su corrección. Lemma 3.10.1. En el momento en que se ejecuta la lı́nea 19 para un vértice v 6= s, el vértice v se encuentra en la cola. Demostración. El elseif de la lı́nea 18 implica que la lı́nea 19 se puede ejecutar sólo si el estado de v es V ya que, si el estado es N se ejecutará la primera parte del if. El único lugar donde se pone estado[v] ← V es la lı́nea 14, por tanto la lı́nea 19 se puede ejecutar para v sólo después que se hayan ejecutado las lı́neas 14--17, o sea, después que v se haya insertado en la cola. Si v ya se ha sacado de la cola, razonando como en la demostración del teorema 3.9.1 notamos que para cada vértice u que se extrae de la cola después de v, se cumple d[u] ≥ d[v]. La condición de la lı́nea 18 se cumple si d[v] = d[u] + 1, que supone d[u] < d[v] y que, como consecuencia de la observación precedente, no se verificará si u se ha extraido de la cola después de v. Theorem 3.10.2. Sea G un grafo en que se ejecuta el algoritmo BFS-All con fuente p /u / v si, y sólo si, s ∈ V [G]. Para cada v ∈ V [G] existe un camino mı́nimo s u ∈ π[v]. grafos 49 Demostración. Las lı́neas que se han a~ nadido, 18 y 19, no afectan d[v] ni el primer elemento de π[v]: para estos elementos sigue valido el teorema 3.9.1. En particular, si v ∈ Vk donde Vk es definido como en la demostración del teorema 3.9.1, se cumple d[v] = k y head(π[v]) ∈ Vk−1 . Tenemos que demostrar entonces que los demás elementos de π[v] también definen caminos mı́nimos y que cada camino mı́nimo hacia v pasa por ellos. Si v no es alcanzable desde s entonces, por el teorema 3.9.1, las lı́neas 12--14 no se ejecutan, v no se inserta en la cola y, para el lema 3.10.1, la lı́nea 19 nunca se ejecuta para v, ası́ que al final π[v] = []. Sea ahora v ∈ Vk . El teorema se demuestra por inducción sobre k. Para k = 0, V0 = {s} y, ya que d[s] = 0 y para todo u 6= s es d[u] > 0, la condicción de la lı́nea 18 nunca se cumple para s y la lı́nea 19 nunca se ejecuta. El teorema se cumple banalmente. Sea ahora el teorema cierto para todo v ∈ Vj , j ≤ k − 1, y consideremos v ∈ Vk arbitrario. p / v , con u = head(π[v]) /u El teorema 3.9.1 garantiza que d[v] = k y que el camino s es mı́nimo. Consideremos los dos casos del "si" y "sólo si". / v . Nos /u a) Sea u ∈ π[v]: tenemos que demostrar que existe un camino mı́nimo s interesa sólo el caso u 6= head(π[v]). En este caso, u se ha introducido en la lı́nea 19 y, por la lı́nea 18, d[u] = k − 1. Por el teorema 3.9.1 esto implica δ(s, u) = k − 1, por lo cual u ∈ Vk−1 , la hipótesis inductiva se cumple para u y el algoritmo p / u mı́nimos, con |p| = k − 1. Entonces, si p0 es el camino encuentra todo camino s p + [(u, v)] se cumple |p| = k: todo camino hasta v que pasa por u es mı́nimo. Ya que v es arbitrario, y u es cualquier elemento de π[v], concluimos que todo camino que el algoritmo encuentra hasta un vértice de Vk es mı́nimo. p p0 / v sea un camino mı́nimo hasta v ∈ Vk , con |p0 | = k, y sea u el p / v . Tenemos que demostrar que u ∈ π[v]. Si /u predecesor de v en p0 : s |p0 | = k entonces |p| = k − 1 y, siendo un sub-camino de un camino mı́nimo también mı́nimo, es u ∈ Vk−1 . Por el teorema 3.9.1, u se ha sacado de la cola antes que v y, al explorar el entorno de u se ha cruzado el arco (u, v) y encontrado v. b) Supongamos que s Si en este momento es estado[v] = N se ejecutan las lı́neas 14--17, y se pone u = head(π[v]). Si no, a este punto d[v] ya se ha asignado y, siendo p0 mı́nimo, se ha puesto d[v] = k. Cuando se saca u de la cola, siendo u ∈ Vk−1 también se ha puesto d[u] = k − 1 (d[u] se asigna cuando u se inttroduce en la cola), por lo cual la condición de la lı́nea 18 se cumple, y se inserta u en π[v]. La La arbitrariedad de v garantiza que el teorema es cierto para todo v ∈ Vk . cahiers d’informatique 50 3.10.2 Ejercicios 1. Ejecutar el algoritmo BFS en los grafos siguiente, tomando como fuente el vértice marcado doble. Se asume que las listas de adyacencias están ordenadas alfabeticamente por nombre de vértices. 89:; ?>=< a G GG ww w GG ww GG w w G w 89:; ?>=< 89:; ?>=< c3 b3 33 3 33 33 3 89:; ?>=< 89:; ?>=< 89:; ?>=< 7654 0123 89:; ?>=< g e f d ?>=< 89:; 89:; a = ?>=< b == == == ?>=< 89:; ?>=< 89:; 0123 7654 c d 89:; ?>=< b 89:; ?>=< c 89:; ?>=< d 89:; ?>=< e 89:; ?>=< f ?>=< /89:; b II II I$ 89:; ?>=< : e u u u u u 89:; ?>=< ?>=< / 89:; c d ?>=< ()*+ /.-, /89:; b II II I$ 89:; ?>=< : e u u u u u 89:; ?>=< ?>=< / 89:; c d 89:; ?>=< ()*+ /.-, a )89:; ?>=< ?>=< 89:; ?>=< /89:; c b i d O J V 89:; ?>=< ?>=< 89:; ?>=< 89:; ?>=< / 89:; g e f @h 89:; ?>=< 89:; ? >=< 89:; ? >=< 89:; ?>=< / / j i k l w 89:; ?>=< a O 89:; ?>=< a 89:; /?>=< b II II I$ ?>=< 89:; ()*+ e : /.-, u u u u u ?>=< 89:; >=< / ?89:; c d ?>=< 89:; a ?>=< 89:; a )?>=< 89:; 89:; ()*+ /.-, ?>=< 89:; /?>=< c b i d O J V ?>=< 89:; 89:; ?>=< 89:; ?>=< 89:; / ?>=< g e f @h ?>=< 89:; ? 89:; >=< ? 89:; >=< ?>=< 89:; / / j i k l 89:; ?>=< ()*+ /.-, a w ?>=< 89:; ()*+ /.-, a i O ?>=< 89:; a )?>=< 89:; >=< >=< /?89:; / ?89:; d @ bO =^ = @ cO @ = = == = ?>=< 89:; ?>=< 89:; ?>=< 89:; ?>=< 89:; g o e o f o h ?>=< 89:; a o 89:; ?>=< a ?>=< 89:; 89:; /?>=< c b 5Z O 5 D O dHHHH 5 H 55 ?>=< 89:; g 55 C 55 89:; ?>=< 89:; ?>=< 89:; ()*+ /.-, / ?>=< e f d o ?>=< ?>=< /89:; /89:; c b O O 89:; ?>=< ?/.-, ()*+ >=< ?>=< / 89:; / 89:; e f d g ( 89:; ?>=< ()*+ /.-, ?>=< ?>=< /89:; c b = 89:; d O O = = = = = = 89:; ?>=< 89:; ?>=< 89:; ?>=< 89:; ?>=< g e f 7 h 89:; ?>=< a o 89:; ?>=< ?>=< /89:; c b 5Z O 5 D O dHHHH 5 H 55 89:; ?>=< ()*+ /.-, g 55 C 55 89:; ?>=< 89:; ?>=< ?>=< / 89:; e f d o 89:; ?>=< 89:; ?>=< a8 b 88ssss s 8 s 8 89:; 89:; ?>=< 7654 0123 88 ?>=< c d t8t8 t t t t 89:; ?>=< ?>=< e JJ 89:; f JtJtt J t J tt 89:; 89:; ?>=< ?>=< g h 89:; ?>=< a o 89:; ?>=< /.-, ()*+ ?>=< /89:; c b 5Z O 5 D O dHHHH 5 H 55 89:; ?>=< g 55 C 55 89:; ?>=< 89:; ?>=< ?>=< / 89:; e f d o 2. Sea G un grafo no dirigido; llamen Gπ (s) el árbol BFS que se construye ejecutando el algoritmo BFS en G con fuente s. ¿Que propiedad expresa la fórmula lógica siguiente? ∀u, v, ∈ V [G] u ∈ V [Gπ (v)] ⇔ v ∈ V [Gπ (u)] (3.79) ¿Es cierta la propiedad? Demostrar que si o dar un contra-ejemplo. grafos 51 3. ¿Como cambia la respuesta a la pregunta precedente si G es dirigido? la propiedad se cumple o dar un contra-ejemplo. Demostrar si 4. El árbol marcado en el grafo siguiente se compone de caminos mı́nimos. Demostrar que, a pesar de esto, el árbol no es un árbol BFS, o sea que el algoritmo BFS con fuente a no encontrará este árbol, independientemente del órden en que los vértices aparecen en las listas de adyacencia. ?>=< 89:; ?>=< +389:; c uu 6> b 66 D u u u 66 uuuuu 89:; ?>=< ()*+ /.-, 6 a III 66 IIIIII I ( 6 89:; ?>=< ?>=< +3 89:; e d (3.80) 5. Un grafo G es bipartito si se puede dividir V [G] en dos sub-conjuntos separados: V [G] = V1 ∪ V2 , y V1 ∩ V2 = ∅ tal que todo arco del grafo une un vértice de V1 con uno de V2 , mientras dos vértices de V1 o dos de V2 no están conectado. Por ejemplo, el grafo siguiente es bipartido: 89:; ?>=< 89:; ?>=< a8 (3.81) b 88ssss ss88 89:; 89:; ?>=< 88 ?>=< c td tt88 t tt 89:; 89:; ?>=< ?>=< e f Usando el algoritmo BFS, se escriba un algoritmo para determinar si un grafo es bipartido. (Sugerencia: costruir el árbol BFS, y fijarse donde están los vértices v con d[v] par y donde están los con d[v] impar. ¿Que caracterı́sticas tienen los valores de d de los vértices unidos por los arcos que hacen el grafo bipartido?) cahiers d’informatique 52 3.11 Búsqueda en profundidad El algoritmo de búsqueda en anchura (BFS) visita los vértices de un grafo en orden de distancia desde la fuente: todo vértice a distancia k de la fuente se visita antes de visitar cualquier vértice a distancia k + 1. Cuando se analiza un vértice u se ponen sus vecinos---los que todavı́a no se han visitado---en la cola, y todos estos vecinos se analizarán antes de empezar a analizar vértices más distantes. Como se ha demostrado en la sección precedente, la clave para el funcionamiento del algoritmo BFS se encuentra en las operaciones de la cola FIFO. La estrategia de la búsqueda en profundidad es de alguna forma dual: en vez de analizar entornos ordenadamente, el algoritmo va de vértice en vértice sin explorar los entornos exaustivamente, "profundizando"; visitando cada vez un hijo del último vértice visitado hasta llegar a un "callejón sin salida": un vértice cuyos vecinos han sido todos visitados. Cuando esto occurre, el algoritmo re-visita al revés los vértices ya visitados hasta encontrar uno que todavı́a tiene vecinos no visitados (backtracking). Ya como en la búsqueda en anchura, el proceso sigue hasta que se hayan visitado todos los vértices alcanzables desde la fuente. Si en este punto quedarán en el grafo vértices por visitar, se eligirá una nueva fuente y se repitirá el algoritmo: el resultado de la búsqueda en profundidad es por tanto un bosque, mientras que el de la búsqueda en anchura era un árbol5 . Debido a las diferencias de estrategia, los árboles encontrados por el algoritmo de búsqueda en profundidad (árboles DF) son en general diferentes de los encontrados por el algoritmo BFS (se vean los ejemplos (a), (b), y (c) de la figura 3.11) aúnque a veces los dos pueden coincidir (como en el ejemplo (d) de la figura 3.11). Consecuentemente, en general, los árboles encontrados por el algoritmo DFS no contienen caminos mı́nimos de la fuente a los demás vértices del grafo. Por eso, el algoritmo DFS no se utiliza en problemas de optimización, sino que se utilizan las propiedades estructurales de los árboles DF en problemas como la ordenación topológica, la detección de ciclos o la determinación de componentes conexas. Tal como el algoritmo BFS, el DFS debe evitar que vértices ya visitados se visiten otra vez y, por ésto, asocia a cada vértice un indicador de estado. En DFS, el estado de un vértice puede tomar tres valores: N para un vértice que todavı́a no se ha descubierto, E para un vértice ya encontrado, y P para un vértice completamente procesado (es decir, un vértice cuya lista de adyacencia se ha explorado completamente). El algoritmo también asocia a cada vértice una marca de tiempo. El tiempo se mide en número de vértices procesados: se marca una unidad de tiempo cada vez que se visita un vértice y cada vez que se termina de procesar un vértice. Para cada vértice v, se asigna a d[v] el tiempo en que el vértice es visitado, y a f [u] el tiempo en que el nodo se termina de procesar. Dado que el contador de tiempo se incrementa dos veces para cada vértice (una vez cuando se descubre el vértice, y otra cuando se termina su procesamiento), se cumple 1 ≤ d[u] < f [u] ≤ 2|V |, (3.82) El vértice u se encuentra en estado N antes del instante d[u], en estado V entre d[u] y f [u], y en estado P después del instante f [u]. Además, el algoritmo mantiene en π[u] el antepasado de u en el árbol DFS que se va construyendo. La forma canónica del algoritmo utiliza un bucle sobre todos los vértices que, cada vez que se encuentra un vértice 5 ¿Por qué en el caso de la búsqueda en profundidad se repite el algoritmo y en el caso de la búsqueda en anchura no? Las razones son eminentemente practicas: la búsqueda en anchura produce un árbol abarcador mı́nimo, y se usa en problemas en que nos sirve un árbol con esta propiedad. La búsqueda en profundidad (cuyo árbol no es mı́nimo) se usa en problemas donde necesitamos visitar todo el grafo, aúnque esto se tenga que hacer con varios árboles. grafos 53 Árboles DFS Árboles BFS ?>=< 89:; ?>=< +389:; c b xx yy8@ EEE x x y x y x EE xxx y yyyyy " xx ?>=< 89:; ()*+ /.-, ?>=< 89:; aD d E DD z EE zzz DD z EE z D" x zzzz " ?>=< ?>=< 89:; +3 89:; e f ?>=< 89:; ()*+ /.-, a ?>=< 89:; c 89:; +3?>=< b GGG GGGG GGG ' 89:; ?>=< e w ww w wwwwwww >=< / ?89:; d ()*+ +3/.-,o ? ()*+ks /.-, ()*+ /.-, ()*+ /.-, ?>=< 89:; ()*+ /.-, a 89:; +3?>=< b ?>=< 89:; d ?>=< 89:; e ()*+ /.-, 89:; ?>=< ?>=< +389:; c b x yy8@ EEEEEEEE x y x y y EE& x| xx yyyyy 89:; ?>=< 89:; ?>=< ()*+ /.-, a DD d z EEE DDDDD z DDD zzz EE " & |z 89:; ?>=< ?>=< +3 89:; e f (a) 89:; ?>=< c (b) /.-, ()*+ 89:; +3?>=< c 89:; ?>=< ()*+ /.-, a (c) >=< / ?89:; f ()*+ /.-, ?>=< +389:; b GGG GGGG GGG ' 89:; ?>=< e w w{ www ?>=< +3 89:; d ()*+ /.-, +3 o ? ()*+o /.-, ()*+ /.-, ?>=< 89:; ()*+ /.-, a ?>=< +389:; b 89:; ?>=< d 89:; ?>=< e ()*+ /.-, /.-, ()*+ ?>=< +389:; c ?>=< / 89:; f (d) Figure 3.8: Ejemplos de árboles DFS y BFS. La fuente es marcada con un doble cı́rculo; se asume que la lista de adyacencia de cada vértice está ordenada alfabéticamente por nombre de vértices. cahiers d’informatique 54 todavı́a no visitado, llama al "verdadero" algoritmo de búsqueda, DFS-Visita, que construye un árbol DF con raı́z en ese vértice. En la versión más común del algoritmo, la que se presenta aquı́, DFS-Visita es una función recursiva (en la sección 3.11.3 se presentará una versión no-recursiva): DFS(G) 1. foreach u in V[G] do 2. estado[u] ← N; 3. π[u] ← NIL; 4. od 5. t ← 0; 6. foreach u in V[G] do 7. if estado[u] = N then 8. DFS-Visita(G, u); 9. fi 10. od DFS-Visita(G, u) 1. estado[u] ← E; 2. t ← t + 1; 3. d[u] ← t; 4. for v in u.ady do 5. if estado[v] = N then 6. π[v] ← u; 7. DFS-Visita(v); 8. fi 9. od 10. estado[u] ← P; 11. t ← t+1; 12. f[u] ← t; La figura 3.9 representa su N (todavı́a no encuentran en encuentran en ilustra el funcionamiento del algoritmo DFS. La forma de un vértice estado: los vértices representados por un cı́rculo se encuentran en estado se han visitado); los vértices representados por un doble cı́rculo se estado E (encontrados) y los vértices representados por un rectángulo se estado P (toda la lista de adyacencia ha sido visitada). El algoritmo empieza por el vértice a, que se pone en estado E, asignandole un tiempo d[u] = 1. Luego (t = 2) se pasa a un vértice de su lista de adyacencia (el vértice d), a uno de la suya, y ası́ sucesivamente hasta que se encuentre un vértice (el vértice c, a t = 5) cuya lista de adyacencia ha sido completamente visitada. Este vértice se marca P y se le pone un tiempo de fin de procesamiento (f [c] = 5). Luego se vuelve al vértice precedente y, como éste también tiene su lista de adyacencia completamente visitada, se marca P y se vuelve al nodo precedente (d). Éste tiene todavı́a vértices marcados N en su lista (el vértice e), por lo cual se procede, a t = 7, a visitar este vértice. Se continúa ası́ hasta que, a t = 12, todos los vértices estén marcados como procesados. grafos 55 a b c e f a ()*+ /.-, /.-, @ABC GFED ?>=< 89:; /()*+ 1/ >> ? O B O >> >> ()*+ /.-, ()*+ /.-, ()*+ //.-, / d z d a t=1 b x @ABC GFED ?>=< 89:; @ABC GFED ?>=< 89:; 1/ 3/ >> ;C O >> >> >> @ABC GFED ?>=< 89:; ()*+ /.-, / 2/ d a c a a ()*+ //.-, e t=4 b f e t=7 b f d t=5 a e t = 10 e t=2 b d f a c e t=8 b f Figure 3.9: e t = 11 d e f f c x @ABC GFED 89:; ?>=< +3 4/5 1/ 3/6 @@ :B O = O @@~~~ | | ~ | @ ~ ~ | ~~~~~~ @@@ ||| @ABC GFED 89:; ?>=< ()*+ /.-, ()*+ / | //.-, 2/ a c b x @ABC GFED 89:; ?>=< +3 4/5 1/ 3/6 @@ :B O @@~~~ |= O ~@@ || ~ | ~ ~ | ~~~~~ @ || @ABC GFED 89:; ?>=< GFED 89:; ?>=< +3 @ABC +3 8/9 2/ 7/ d c w @ABC GFED 89:; ?>=< +3 4/5 1/ E 3/6 EE 8@ O = O z y EEyyy z z y E z y zz yyyy E" 3 + +3 8/9 2/11 7/10 d f a e t=3 b d f c @ABC GFED 89:; ?>=< @ABC GFED 89:; ?>=< // .-, ()*+ 1/ 3/ >> ;C O A O >> >> >> ()*+ @ABC GFED 89:; ?>=< ()*+ /.-, / //.-, 2/ f e t=6 b b y c x @ABC GFED 89:; ?>=< +3 4/5 1/ 3/6 @@ :B O @@~~~ |= O ~@@ || ~ | ~ ~ | ~~~~~ @ || GFED 89:; ?>=< @ABC GFED 89:; ?>=< GFED 89:; ?>=< +3 @ABC +3 @ABC 8/ 2/ 7/ c x @ABC GFED ?>=< 89:; +3 4/5 1/ B 3/6 BB 9A O = O z BB|||| z z | B z | zz |||||| B! @ABC GFED ?>=< 89:; 3 + +3 8/9 2/ 7/10 d a c x @ABC GFED 89:; ?>=< GFED @ABC 89:; ?>=< +3 4/5 3/ 1/ >> ;C O > O >> ~ ~ >> ~ >> ~~~ @ABC GFED 89:; ?>=< ()*+ /.-, ()*+ /.-, / ~ / 2/ @ABC ?>=< 89:; 3+ GFED 4/ ? O c x @ABC GFED ?>=< 89:; +3 4/5 1/ 3/6 @@ :B O @@~~~ |= O ~@@ || ~ | ~ ~ | ~~~~~ @ || ()*+ /.-, @ABC GFED ?>=< 89:; GFED 89:; ?>=< +3 @ABC / 2/ 7/ d b ()*+ /.-, // .-, ()*+ GFED @ABC 89:; ?>=< 1/ :: A O C O :: ::: /.-, @ABC GFED 89:; ?>=< ()*+ ()*+ //.-, / 2/ z a v e t=9 b f c +3 4/5 3/6 EE y 8@ O = O z EyEyyy z z E z y yyyy E" zz 3 + +3 8/9 2/11 7/10 1/12 d Funcionamiento del algoritmo DFS e t = 12 f cahiers d’informatique 56 3.11.1 Análisis del tiempo de ejecución Para el análisis del tiempo de ejecución, notamos que los bucles a las lı́neas 1--4 y 6--9 toman un tiempo Θ(V ), excepto por el tiempo necesario para ejecutar la funcion DFS-Visita. La función DFS-Visita se llama una (y sólo una) vez para cada vértice v ∈ V , ya que DFS-Visita se llama sólo si el vértice está en estado N, y en la función el estado de v se cambia en seguida a E. Durante una llamada a DFS-Visita, el bucle 4--9 se ejecuta |ady[v]| veces. Como X |ady[v]| = Θ(E) (3.83) v∈V el costo total de la ejecución de este ciclo es Θ(E). O(V + E). 3.11.2 El tiempo de ejecución de DFS es Propiedades de la búsqueda en profundidad Tras la ejecución del algoritmo DFS es posible definir el grafo Gπ = (Vπ , Eπ ) formado por todos los vértices del grafo (recordamos que el algoritmo DFS se ejecuta hasta que se hayan visitado todos los vértices), y por los arcos (π[u], u) con π[u] 6= N IL. En el caso del algoritmo BFS este grafo era un árbol: el árbol BF del grafo. En este caso, como ya se ha observado, el grafo Gπ es un bosque con un árbol construido por cada llamada a DFS-Visita. Cada árbol de Gπ es un árbol DF del grafo. Observamos que π[v] = u si y sólo si DFS-Visita(v) se ha llamado durante la exploración de la lista de adyacencia de u (lı́neas 6 y 7 de DFS-Visita). Una propiedad importante del algoritmo de búsqueda en profundidad es la propiedad del paréntesis : si se representa la primera visita a un vértice con un paréntesis abierto y la finalización del vértice con un paréntesis cerrado, toda ejecución de DFS resulta en una estructura de paréntesis bien anidados. Por ejemplo, en la ejecución de figura 3.9, se empieza visitando el primer vértice (parentésis abierto), el segundo, el tercero y el cuarto (tres paréntesis abiertos), luego se cierra el cuarto y el tercero, y ası́ siguiendo. El resultado es la estructura siguiente, donde encima de cada paréntesis se pone el nombre del vértice seguido por + si se trata de la primera visita, por - si se trata de la finalización: a+ d+ b+ c+ c− b− e+ f + f − e− d− a− ( ( ( ( ) ) ( ( ) ) ) ) (3.84) Esta propiedad, expresada aquı́ de forma intuitiva, se enuncia formalmente en el teorema siguiente. Theorem 3.11.1. (del paréntesis) Sean u y v dos vértices cualesquiera de un grafo al que se ha aplicado el algoritmo DFS, y sean definidos los intervalos Iu = [d[u], f [u]] y Iv = [d[v], f [v]]. Entonces una (y sólo una) de las siguientes proposiciones se cumple: i) Iu y Iv son disjuntos; ii) Iu ⊂ Iv y u es un descendente de v en el árbol DF; iii) Iv ⊂ Iu y v es un descendente de u en el árbol DF. Demostración. Supongamos d[u] < d[v]. Hay que considerar dos casos: d[v] > f [u] o d[v] < f [u] (el algoritmo asigna a cada paso un solo valor de tiempo, ya sea a un "d" o a un "f": por eso no se puede dar el caso d[v] = f [u], ni ninguna otra igualdad de tiempos). grafos 57 En el primer caso, por la (3.82), se cumple d[u] < f [u] < d[v] < f [v], y consecuentemente los intervalos Iu y Iv son disjuntos. En el segundo caso, si d[u] < d[v] < f [u], v se ha visitado mientras que u estaba todavı́a en estado E. Entonces, cuando se llama DFS-Visita(v), ya se ha llamado DFS-Visita(u) (en cuanto d[v] > d[u]) pero la DFS-Visita(u) todavı́a no ha retornado (ya que d[v] < f [u]). Por tanto, DFS-Visita(v) está anidado en DFS-Visita(u). Esto implica que entre la llamada DFS-Visita(u) y la llamada DFS-Visita(v) hay una serie de llamadas a DFS-Visita con vértices que en el árbol DF forman una cadena de u a v. Entonces v es un descendiente de u. Además, como DFS-Visita(v) está anidado en DFS-Visita(u), retorna antes que DFS-Visita(u), o sea f [v] < f [v], se modo que Iv ⊂ Iu . En el caso en que d[v] < d[u] se sigue el mismo razonamiento, invirtiendo los papeles de u y v. Corollary 3.11.1. (anidamiento de los intervalos de los descendientes) El vértice v es un descendiente de u en el árbol DF de un grafo si y sólo si d[u] < d[v] < f [v] < f [u]. Demostración. Si d[u] < d[v] < f [v] < f [u], la propiedad sigue directamente del teorema del paréntesis. Supongamos que v sea un descendiente de u en el árbol DF, y sea u = w0 → w1 → · · · → wn = v la rama del árbol de u a v. Demostraremos por inducción sobre k que d[u] < d[wk ] < f [wk ] < f [u]. Si k = 1, w1 es un vértice en la lista de adyacencia de u, por tanto, DFS-Visita(w1 ) se llama desde DFS-Visita(u) (cuando se visita u, w1 no se ha visitado, si no no podrı́a ser un hijo de u en el árbol), por tanto d[u] < d[w1 ]. Como las llamadas se encuentran anidadas, DFS-Visita(w1 ) retorna antes que DFS-Visita(u), por tanto f [w1 ] < f [u]. Supongamos ahora que d[u] < d[wk ] < f [wk ] < f [u], (3.85) y demostremos la propiedad en el caso de wk+1 . Aplicando el razonamiento precedente al camino desde wk a v se ve que d[wk ] < d[wk+1 ] < f [wk+1 ] < f [wk ] y considerando la (3.85) se obtiene d[u] < d[wk+1 ] < f [wk+1 ] < f [u]. El corolario segue del caso k = n. Ejemplo: Para el grafo de figura 3.10.a, y el árbol DF de figura 3.10.b, se da la situación representada en figura 3.10.c; la estructura de paréntesis correspondiente se representa en figura 3.10.d Si en al tiempo d[v] (cuando se visita v por primera vez) el estado de un vértice u es V , entonces v descenderá de u en el árbol DF. La razón es que si al tiempo d[v] el estado de u es V , entonces u ya se ha visitado, es decir d[u] < d[v] < f [v]. Por el teorema del paréntesis, se cumple d[u] < d[v] < f [u] < f [v] y, por el corolario, u es un antepasado de v en el árbol. La condiciones bajo las cuales un vértice v es descendiente de otro vértice u en el árbol DF no son inmediatamente evidentes. La presencia de un arco (u, v) no garantiza que sea π[v] = u. El la figura 3.10, hay un arco (g, h), pero h no es hijo (ni descendiente) de g. La razón es que h se ha descubierto analizando otro camino antes de llegar a g, por lo tanto en el momento en que se analiza g el vértice h ya se encuentra en estado P. Si v es adyacente a u, y el estado de v es N en el momento en que se discubre u, entonces v será cahiers d’informatique 58 c b d ()*+ /.-, ()*+> /.-, /.-, ()*+ /()*+ / OO //.-, ? O O >> OOO O >> OOO > O' /.-, ()*+o ()*+ /.-, ()*+ O/.-, ()*+ //.-, / a a e g f c b d +3 2/9 S +3 3/6 / 4/5 1/16 SSS S <y O O HHHHH S SSSS HHHHH y SSySyySy HH ' SS %/ 7/8 +3 13/14 11/12 ks 10/15 h e g f (a) h (b) 1 4 8 12 16 t a b f c e g d (c) a+ b+ c+ d+ d− c− h+ h− b− f + e+ e− g+ g− f − a− ( ( ( ( ) ) ( ) ) (d) ( ( ) ( ) ) ) Figure 3.10: Ejemplo del teorema del paréntesis: Para el grafo (a), con el árbol DF (b), la estructura de intervales se muestra en (c), y la estructura de paréntesis correspondiente en (d). un descendiente de u en el árbol DF en cuanto o se discubre v a través de un camino que sale de u---en cuyo caso v será descendiente de u---o bien se descubre cruzando el arco (u, v) mientras que se analiza la lista de adyacencia de u---en cuyo caso v será un hijo de u en el árbol DF. Por otro lado, si v es accesible desde u (pero no adyacente a u), y el estado de v es N cuando se descubre u, v no será necesariamente un descendiente de u en el árbol DF. Por ejemplo, en el grafo u(3/4) ck (3.86) ' 6> uuuu u u u u uuuu y(2/7) +3 v(5/6) x(1/8) el vértice v es accesible desde u pero no es su descendiente en el árbol DF. Si el análisis del grafo empieza por x, u y v serán hijos de y, pero v no será un descendiente de u. El teorema siguiente caracteriza este caso con más precisión: proporciona las grafos 59 condiciones necesarias y suficientes para que un vértice v accesible desde un vértice u en el grafo sea su descendiente en el árbol DF. Theorem 3.11.2. (del camino blanco) En un árbol DF de un grafo G = (V, E) (dirigido o no dirigido), el vértice v es un descendiente de un vértice u si, y sólo si, en el momento d[u] en que u es visitado por primera vez, v es accesible desde u a través de un camino formado enteramente por vértices de estado N . Demostración. Supongamos que v sea un descendiente de u en u → · · · → w → · · · → v el camino entre u y v en el árbol DF, del camino. Por el teorema del paréntesis, d[u] < d[w], por no se habı́a todavı́a visitado, o sea, estaba en estado N . del camino, se concluye que todo nodo del camino estaba en el árbol, y sea donde w es un nodo cualquiera lo cual en el instante d[u], w Como w es un nodo cualquiera estado N en el instante d[u]. Supongamos ahora que al tiempo d[u] exista un camino de u a v formado por vértices en estado N . (En particular, al instante d[u], v se encuentra en estado N .) Demostramos que, en este caso, Iv ⊂ Iu . Por contradicción, se suponga que Iv 6⊂ Iu y consideramos, en el camino entre u y v, el primer vértice y tal que Iy 6⊂ Iu . Sea x el predecesor de y en dicho camino. Como y es el primer vértice para el cual la inclusión no vale, será Ix ⊂ Iu , o sea d[u] ≤ d[x] < f [x] ≤ f [u] (la igualdad vale en el caso x = u). Ya que y es parte del camino blanco, al instante d[u] y se encuentra en estado N y por tanto debe ser d[y] > d[u]. Pero y es parte del entorno de x y por tanto será seguramente descubierto antes que se termine la análisis de x, o sea d[u] < d[y] < f [x] ≤ f [u]. Pero, para el teorema del paréntesis, esto quiere decir que Iy ⊂ Ix , que es una contradición del hipótesis. Entonces, v es un descendente de u en el árbol. 3.11.3 Versión no recursiva de DFS El algoritmo DFS se ha presentado en una versión recursiva, ya que esta versión es más sencilla y facilita la demostración de las propiedades del algoritmo. Existe una versión no recursiva que pone en evidencia como el DFS es un algoritmo de alguna forma dual al BFS. Hemos visto que la clave para el funcionamiento del BFS es la cola FIFO en que se insertan los vértices en orden de distancia de la fuente, y de donde se van sacando en el mismo orden en que se han insertado: de los más cercanos hacia los más distantes. Ahora bien, si en el BFS se remplaza la cola FIFO con una pila (LIFO: last in, first out) se consigue un algoritmo que visita los vértices en el mismo orden que el DFS. Con una pila no se procede en orden de distancia, sino que, a cada paso, se sigue yendo "en profundidad" explorando el último vecino insertado en la pila. El algoritmo que se consigue no es exactamente igual al DFS porque el DFS supone dos modificaciones adicionales: la definición del estado P para vértices que ya se han explorado completamente, y de los contadores de tiempo. Estos cambios no modifican el orden de visita de los vértices ni los árboles DF que se construyen, pero nos proporcionan información adicional que, como veremos, se utilizan en las aplicaciones. Sin embargo, estas modificaciones a~ naden ciertas complicaciones al algoritmo ya que, tras exporar los vecinos de un vértice, hay que volver a visitarlo para poner su estado a P y fijar su tiempo de finalizaciı́on. Esta visita adicional es algo que en BFS no se hacı́a, ya que una vez insertado un vértice en la cola no se volvı́a a cambiar. Con el fin de hacer la presentación más clara, se presentará el algoritmo "DFS-Visita-no-rec" en dos tiempos. Por el momento, ignoraremos el estado P y el tiempo de finalización, y escriberemos un algoritmo que determina un árbol DF sin poner los vértices en estado P y sin asignar un tiempo de finalización. Este algoritmo es el dual de BFS: 60 cahiers d’informatique DFS-Visita-no-rec-1(G, s) 1. Q: stack of vertex; 2. t d[s] ← 0;←3.t; 4. t ← t+1; 5. Q.push(s); 6. while ¬empty(Q) do 7. u ← Q.pop; 8. estado[u] ← E; 9. d[u] ← t; 10. t ← t+1; 11. foreach v in u.ady do 12. if estado[v] = N then 13. π[v] ← u; 14. Q.push(v); 15. fi; 16. od 17. od Para la versión completa, observamos que, cuando visitamos por primera vez el vértice u (es decir, cuando lo descubrimos y lo encontramos en estado N) tendremos que volver a visitarlo, tras haber visitado sus vecinos, para finalizarlo. La idea entonces es que cuando se saca u de la pila y se encuentra en estado N, se pone su estado a E y se vuelve a poner en la pila, antes de insertar sus vecinos. Cuando se hayan visitado todos los vecinos, se volverá a sacar u de la pila y, encontrando su estado en E, nos daremos cuentas que está listo para finalizar. En ste caso, en vez de visitar sus vecinos, se pondrá su estado a P, se asignará su tiempo de finalización, y no se volverá a poner en la cola. DFS-Visita-no-rec(G, s) 1. Q: stack of vertex; 2. t ← 0; 3. d[s] ← t; 4. t ← t+1; 5. Q.push(s); 6. while ¬empty(Q) do 7. u ← Q.pop; 8. if estado[u] = N then 9. estado[u] ← E; 10. d[u] ← t; 11. t ← t+1; 12. foreach v in u.ady do 13. if estado[v] = N then 14. π[v] ← u; 15. Q.push(v); 16. fi; 17. od 18. elseif estado[u] = E then 19. estado[u] ← P; 20. f[u] ← t; 21. t ← t+1; 22. fi grafos 23. 61 od La figura 3.11 ilustra el mismo ejemplo de la figura 3.9 ense~ nando, a cada paso, el estado de la pila (los vértices que contiene y el estado de dichos vértices). Nótese que a b c e f a ()*+ /.-, @ABC GFED ?>=< 89:; // .-, ()*+ 1/ >> ? O B O >> >> ()*+ /.-, ()*+ /.-, ()*+ /.-, / / b c a b c ()*+ /.-, @ABC GFED 89:; ?>=< // .-, ()*+ 1/ :: A O C O :: ::: //.-, @ABC GFED 89:; ?>=< ()*+ ()*+ //.-, 2/ @ABC GFED 89:; ?>=< @ABC GFED 89:; ?>=< ()*+ //.-, 1/ 3/ >> ;C O A O >> >> >> ()*+ /.-, @ABC GFED 89:; ?>=< ()*+ //.-, / 2/ [a(E), e(N ), d(N )] a c b x @ABC GFED ?>=< 89:; @ABC GFED ?>=< 89:; G?>=< 89:; FED +3 @ABC 1/ 3/ 4/ >> ;C O ? O >> >> >> ()*+ /.-, @ABC GFED ?>=< 89:; ()*+ / //.-, 2/ e f d [a(E), e(N ), d(E), e(N ), b(N )] a c b x @ABC GFED 89:; ?>=< @ABC GFED 89:; ?>=< +3 4/5 3/ 1/ >> ;C O > O >> ~~ >> ~ >> ~~~ ~ ()*+ @ABC GFED 89:; ?>=< ()*+ /.-, //.-, / 2/ e f d [a(E), e(N ), d(E), e(N ), b(E), c(N )] a c b x @ABC GFED 89:; ?>=< +3 4/5 1/ 3/6 @@ :B O @@~~~ |= O ~@@ || ~ ~ | ~ ~~~~~ @@ ||| @ABC GFED 89:; ?>=< ()*+ /.-, ()*+ /.-, / | / 2/ e f d [a(E), e(N ), d(E), e(N ), b(E), c(E)] a c b x @ABC GFED ?>=< 89:; +3 4/5 1/ 3/6 @@ :B O @@~~~ |= O || ~~~@@ | ~ | ~ ~~~~ @ || ()*+ @ABC GFED ?>=< 89:; GFED ?>=< 89:; +3 @ABC //.-, 2/ 7/ e f d [a(E), e(N ), d(E), e(N ), b(E)] a c b x @ABC GFED 89:; ?>=< +3 4/5 3/6 1/ @@ :B O @@~~~ |= O || ~~~@@ | ~ | ~ ~~~~ @ || GFED 89:; ?>=< GFED 89:; ?>=< @ABC GFED 89:; ?>=< +3 @ABC +3 @ABC 8/ 7/ 2/ e f d [a(E), e(N ), d(E), e(N )] a c b x @ABC GFED 89:; ?>=< +3 4/5 1/ 3/6 @@ :B O @@~~~ |= O || ~~~@@ | ~ | ~ ~~~~ @ || GFED 89:; ?>=< @ABC GFED 89:; ?>=< +3 8/9 +3 @ABC 7/ 2/ e f d [a(E), e(E), d(E), e(E), f (N )] a c b x @ABC GFED ?>=< 89:; +3 4/5 3/6 1/ B BB 9A O = O z | BB||| z z | B z | zz |||||| B! @ABC GFED ?>=< 89:; +3 8/9 3 + 7/10 2/ e f d [a(E), e(E), d(E), e(E), f (E)] a c b w @ABC GFED 89:; ?>=< +3 4/5 3/6 1/ E EE 8@ O = O z y EEyyy z z y E z y zz yyyy E" +3 8/9 3 + 7/10 2/11 e f d [a(E), e(E), d(E), e(E)] a c b v +3 4/5 3/6 1/12 = O EE y 8@ O z EyEyyy z z E z y zz yyyy E" +3 8/9 3 + 7/10 2/11 d d z e f [a(E), e(E), d(E)] Figure 3.11: d z e f [a(E), e(P )], [a(E)] y d e [] f Funcionamiento del algoritmo DFS en versión no recursiva. el vértice e se inserta dos veces en la pila: una vez en cuanto vecino de a y una en cuanto vecino de d. El vértice se procesa como vecino de d cuando se extrae su versión más reciente al instante 6, y se finaliza al instante 10. En este momento, el vértice sigue en la pila (en segunda posición) pero, cuando se extrae otra vez de la cola, su estado ya es P, ası́ que el algoritmo simplemente lo ignora (se vea como al tiempo 11 hay dos extracciones de la pila sin que cambie el marcador de tiempo). cahiers d’informatique 62 En fin, cabe notar que la versión recursiva del algoritmo también se basa en una pila, aún si esta no aparece explicitamente en el algoritmo: se trata de la pila de las llamadas a DFS-Visita y de sus parámetros, que es definida y gestionada por el compilador y que tiene la misma estructura de la pila que se ha definido aquı́. Por esta razón, no es fácil dar una versión recursiva de BFS: los compiladores guardan las llamadas siempre en una pila, y nunca en una cola FIFO. 3.11.4 Clasificación de los arcos El algoritmo de búsqueda en profundidad nos proporciona una clasificación de las aristas del grafo que, a la vez, nos proporciona información sobre la estructura del grafo. Se pueden definir cuatro tipos de arcos, en relación con el bosque Gπ resultante de la aplicación del algoritmo DFS: arcos del árbol: son los arcos incluidos en los árboles DF de Gπ ; el arco (u, v) es un arco del árbol si v se ha descubierto por primera vez durante la exploración del entorno de u; arcos hacia atrás: son arcos (u, v) tal que v es un antecesor de u en un árbol DF; árcos entre un vértice y si mismo se consideran arcos hacia atrás; arcos hacia adelante: son arcos (u, v) que conectan un vértice v a un descendente v en un árbol DF (excepto en el caso que el arco sea un arco del árbol); arcos de cruce: los demás; los arcos (u, v) donde u y v no son uno el antecesor del otro. Los arcos se pueden clasificar (parcialmente) durante la ejecución del algoritmo DFS, notando el estado del vértice v que se accede cuando el arco se explora por primera vez. Si se está analizando el entorno de u, y se llega al vértice v, entonces i) si estado[v] = N el arco (u, v) es un arco del árbol; ii) si estado[v] = E el arco (u, v) es un arco hacia atrás; iii) si estado[v] = P, el arco (u, v) es un arco hacia adelante o un arco de cruce. El primer caso es obvio. En el segundo, los vértices marcados E corresponden a una pila de llamadas a DFS-Visita, y forman una rama de un árbol DF, ası́ que encontrar un vértice en estado E quiere decir encontrar un antecesor en la rama del árbol. Esta clasificación no permite distinguir entre arcos hacia adelante y arcos de cruce. Para esto, será necesario analizar el grafo después la aplicación del algoritmo: se puede demostrar que un arco (u, v) es un arco hacia adelante si d[u] < d[v], y de cruce si d[v] < d[u]. La clasificación de un arco depende de las modalidades de ejecución del algoritmo DFS. Consideremos el grafo siguiente, en que los vértices se visitan en orden alfabético: ?>=< 89:; c cruce ?>=< / 89:; a arbol ?>=< +389:; b (3.87) Se empieza por el vértice a, y el arco (a, b) será parte del árbol DF con raı́z en a. En el momento en que se visita c, a ya se encuentra en estado P y, siendo d[c] > d[a], el arco (c, a) se clasificará como arco de cruce. Por otro lado, si los vértices se visitan en grafos 63 orden alfabético inverso, empezando por c, los dos arcos serán arcos del árbol DF con raı́z en c: arbol +3 89:; arbol +389:; ?>=< ?>=< ?>=< 89:; a c (3.88) b Sin embargo, hay ciertas propiedades de la clasificaión que dependen sólo de la estructura del grafo, como demuestra el teorema siguiente: Theorem 3.11.3. Un grafo dirigido G es acı́clico si y sólo si el algoritmo DFS no produce ningún arco hacia atrás. Demostración. Supongamos que el grafo sea acı́clico. Si se da un arco hacia atrás (u, v), entonces v es un antecesor de u en el árbol. Sea v → w1 → · · · → wn → u el camino de u a v en el árbol: dicho camino más el arco (u, v) es un ciclo, y se contradice la hipótesis que el grafo sea acı́clico. Supongamos ahora que G contenga un ciclo. Sea v el primer vértice del ciclo que se visita, y sea u el vértice que lo precede en el ciclo. Cuando v es visitado, los demás vértices del ciclo están en estado N (¡v es el primer vértice que se visita!), por tanto hay un camino de vértices en estado N hasta u. Por el teorema del camino blanco, u desciende de v en el árbol, y (u, v) es un arco hacia atrás. El teorema no garantiza que siempre se encontrará el mismo arco hacia atrás. Por ejemplo, en el grafo siguiente, si se visitan los vértices en orden alfabético, el arco (c, b) es un arco hacia atrás 89:; ?>=< ?>=< ?>=< /89:; / 89:; a (3.89) b >^ d >> >> 89:; ?>=< c Por otro lado, si se visitan los vértices hacia atrás. Pero, en todo caso, como el garantiza que siempre se encontrará uno y encuentra sólo uno, veanse los ejercicios * en orden alfabético inverso, el arco (b, d) es grafo contiene un ciclo, el teorema nos un sólo arco hacia atrás. (Sobre el porqué se al final de esta sección.) * * En un grafo no dirigido, esta clasificación es ambigüa, ya que los arcos (u, v) y (v, u) son en realidad el mismo arco. En este caso, el arco es clasificado según la dirección en que se cruce. La clasificación de un grafo no dirigido no contiene arcos hacia adelante ni arcos de cruce. Theorem 3.11.4. En la búsqueda en profundidad de un grafo no dirigido G, cada arco es un arco del árbol o un arco hacia atrás. Demostración. Sea (u, v) un arco de G. Ya que el arco se puede escribir como (u, v) o (v, u), se asumirá que está escrito en el orden tal que d[u] < d[v]. Entonces, v se descubre antes de que se acabe la exploración de u, ya que se encuentra en su lista de adyacencia (por el teorema del camino blanco). Si el arco (u, v) se recorre primero en dirección de u a v, entonces (u, v) es un arco del árbol. Si se recorre primero en dirección de v a u, entonces ed un arco hacia atrás, ya que en ese momento u se encuentra en estado E. cahiers d’informatique 64 3.11.5 Ejercicios 1. Ejecute el algoritmo BFS en los grafos siguientes, tomando como fuente el vértice marcado doble. Asuma que las listas de adyacencias están ordenadas alfabéticamente por nombre de vértices. ?>=< 89:; 89:; a = ?>=< b == == == ?>=< 89:; ?>=< 89:; 0123 7654 c d 89:; /?>=< b II II I$ ?>=< 89:; ()*+ /.-, e u: u u u u ?>=< 89:; >=< / ?89:; c d ?>=< 89:; a 89:; ?>=< a w GGG w w GG w w GG ww G w 89:; ?>=< 89:; ?>=< c3 b3 33 3 33 33 3 89:; ?>=< 89:; ?>=< 89:; ?>=< 7654 0123 89:; ?>=< g e f d 89:; ?>=< d 89:; ?>=< e 89:; ?>=< f 89:; ?>=< ()*+ /.-, a )89:; ?>=< ?>=< 89:; ?>=< /89:; c d b i O J V 89:; ?>=< ?>=< 89:; ?>=< 89:; ?>=< / 89:; g e f h @ 89:; ?>=< 89:; ?>=< 89:; ? >=< 89:; ?/ >=< / j i l k w 89:; ?>=< a O 89:; ?>=< ()*+ /.-, a w ?>=< 89:; ()*+ /.-, a i O ?>=< 89:; a 89:; ?>=< 89:; /?>=< c b 5Z O 5 D O dHHHH 5 H 55 ?>=< 89:; g 5 55 C 5 89:; ?>=< 89:; ?>=< 89:; ()*+ /.-, / ?>=< e f d o 89:; ?>=< c ?>=< ()*+ /.-, /89:; b II II I$ 89:; ?>=< e u: u u u u 89:; ?>=< ?>=< / 89:; c d )?>=< ?>=< 89:; 89:; ()*+ /.-, 89:; /?>=< c b i d O J V ?>=< 89:; 89:; ?>=< 89:; ?>=< 89:; / ?>=< g e f h @ ?>=< 89:; ? 89:; >=< ?89:; ?>=< 89:; / / >=< j i k l ?>=< 89:; a o 89:; ?>=< b ?>=< /89:; b II II I$ 89:; ?>=< e u: u u u u 89:; ?>=< ?>=< / 89:; c d 89:; ?>=< a ?>=< 89:; a )?>=< 89:; >=< >=< /?89:; / ?89:; @ bO =^ = @ cO @d == === ?>=< 89:; ?>=< 89:; ?>=< 89:; ?>=< 89:; g o e o f o h 89:; ?>=< a ?>=< ?>=< /89:; /89:; c b O O 89:; ?>=< 89:; ? ()*+ /.-, >=< ?>=< / / 89:; e f d g ( 89:; ?>=< ()*+ /.-, ?>=< ?>=< /89:; c b = 89:; d O = == O = == 89:; ?>=< 89:; ?>=< 89:; ?>=< 89:; ?>=< g e f 7 h 89:; ?>=< a o 89:; ?>=< 89:; ?>=< a8 b 88ssss ss88 89:; 89:; ?>=< 7654 0123 88 ?>=< c td 8 t tt 8 tt ?>=< 89:; ?>=< e JJ 89:; f JtJtt J t tt J89:; ?>=< 89:; ?>=< g h 89:; ?>=< a o ?>=< 89:; ?>=< /89:; c b 5Z O 5 D O dHHHH 5 H 55 89:; ?>=< ()*+ /.-, g 5 55 C 5 89:; ?>=< 89:; ?>=< ?>=< / 89:; e f d o ?>=< 89:; ?>=< /.-, ()*+ /89:; c b 5Z O 5 D O dHHHH 5 H 55 89:; ?>=< g 5 55 C 5 ?>=< 89:; ?>=< 89:; ?>=< / 89:; e f d o 2. Escriba la estructura de paréntesis para cada uno de los árboles (o bosques) DF del ejercicio 1. 3. De un ejemplo de grafo y un bosque DF en tal grafo, con un vértice u con la siguiente propiedad: existe un árbol DF que contiene sólo u, a pesar del hecho que existe un arco que sale de u y un arco que entra en u. Dibuje el grafo dando etiquetas a sus vértices. Asuma que el bucle principal del algoritmo DFS analiza grafos 65 los vértices en orden alfabetico, y que todas las listas de adyacencias son ordenadas alfabeticamente. 4. Determine un grafo conexo en que las etiquetas de los vértices son tales que cada vértice constituye un árbol DF separado. Asuma que el bucle principal del algoritmo DFS analiza los vértices en orden alfabetico, y que todas las listas de adyacencias son ordenadas alfabeticamente. 5. Suponga que se hace una clasificación de los arcos de un grafo dirigido, similar a la que se ha hecho en esta sección, tras ejecutar el algoritmo BFS. Demuestre que en ningún caso hay arcos hacia adelante. 6. En el mismo caso del ejercicio precedente, demuestre que, si el grafo es no dirigido, no hay ni arcos hacia atrás ni arcos hacia adelante. 7. En el ejemplo (3.89) se muestra que dependiendo del orden de la visita, el arco hacia atrás que se encuentra puede cambiar. Demuestre que, dado un grafo con un sólo ciclo, el algoritmo DFS siempre encontrará un sólo arco hacia atrás. (Sugerencia: se puede utilizar el hecho de que si un grafo contiene un solo ciclo y se elimina un arco de dicho ciclo, el grafo resultante es acı́clico.) 8. Generalice el teorema precedente al caso de n ciclos. cahiers d’informatique 66 A B C D E F G H I J K L a~ nadir un vaso de vino blanco; abrir una lata de tomate (¡San Marzano, de Napoli!); poner la cebolla en la sartén y freir; a~ nadir trozos de hojas de albahaca; picar la cebolla; quitar el ajo del fuego; poner los tomates en la sartén; poner los dientes de ajo en la sartén; a~ nadir sal, al gusto; pelar dos dientes de ajo; dejar cocer durante cinco minutos y servir; untar una sartén con aceite y ponerla a fuego moderado; Table 4.1: Operaciones necesarias para preparar una salsa de tomate. IV. APLICACIONES DE LA BÚSQUEDA 4.12 Ordenación topológica Una ordenación topológica de un grafo dirigido acı́clico (Directed Acyclic Graph, DAG) G = (V, E) es una ordenación de los vértices de V tal que para cada arista (u, v) ∈ E, u precede v en la ordenación. La ordenación topológica se define sólo para grafos acı́clicos en cuanto si el grafo contiene un ciclo, no existe ninguna permutación con la propiedad requerida. En forma gráfica, una ordenación topológica se puede representar poniendo todos los vértice del grafo uno al lado del otro de forma tal que todos los arcos van de izquierda a derecha. Para introducir el concepto de ordenación topológica con un ejemplo, consideremos el problema de preparar una salsa de tomate. De manera un poco esquemática, podemos considerar que las operaciones necesarias para preparar la salsa son las que se representan (en orden casual) en la tabla 4.12. Claramente, para conseguir una buena grafos 67 salsa y no un desastre culinario, estas operaciones no se pueden efectuar en un order cualquiera. Por ejemplo, es claramente necesario picar la cebolla (operación E) antes de ponerla en la sartén (operación C). En este caso, se dirá que la operación E tiene prioridad, o que es un prerequisito para poder efectuar la operación C. Se pueden representar las operaciones como los vértices de un grafo, poniendo una arista desde un prerequisito a las operaciones que dependen de él. En el caso de las operaciones E y C, se pondrá ?>=< 89:; ?>=< / 89:; (4.90) C E Otras operaciones no tienen este tipo de dependencia. Por ejemplo, se puede picar la cebolla (operación E) antes o después de pelar el ajo (operación J). Todas las dependencias entre operaciones se pueden representar en un grafo en que, como en el caso de las operaciones E y C arriba, los vértices representan operaciones y las aristas relaciones de prioridad/dependencia. En el caso de la salsa de tomate, este grafo de prioridad se presenta como: ?>=< 89:; J> >> > ?>=< 89:; ?>=< / 89:; F> @ H) >> )) > ) )) 89:; ?>=< ?>=< 89:; ?>=< 89:; ?>=< B / 89:; L G> @K )) >> > )) 89:; ?>=< )) @A )) ?>=< 89:; ?>=< ?>=< / 89:; E C / 89:; I (4.91) El grafo nos da toda la información necesaria para hacer una salsa de tomate, pero lo que de verdad queremos es una receta: una secuencia determinada de operaciones que, ejecutadas en el orden especificado, nos consigan una buena salsa. Esta lista ordenada debe respetar las prioridades entre las operaciones: si, como es el caso, E tiene prioridad sobre C, C debe aparecer en la lista después de E, y ası́ para todas las demás prioridades. Esto supone encontrar una ordenación de los vértices tal que cada vez que haya una arista (u, v) ∈ E, u preceda v en la ordenación. Es decir, la receta tiene que ser una ordenación topológica del grafo de prioridades. La definición formal de ordenación topológica es la siguiente: Definition 4.12.1. Dado un grafo G = (V, E), con V = {v1 , . . . , vn }, una ordenación topológica de G es una permutación Vπ = [vπ1 , . . . , vπn ] tal que si (vi , vj ) ∈ E, entonces πi < πj . No todos los grafos se pueden ordenar topológicamente. El lema siguiente caracteriza una clase de grafos en que no se puede definir una ordenación topológica. Lemma 4.12.1. Si G = (V, E) contiene ciclos, entonces no existe ninguna ordenación de sus vértices que cumpla la propiedad de ordenación topológica. Demostración. Supongamos que G posee un ciclo [v1 , v2 , . . . , vn , v1 ] de n vértices distintos y una ordenación topológica. Escribimos u v si el vértice u precede a v en dicha ordenación. Notamos que (v1 , v2 ) ∈ E ası́ que, por la propiedad de ordenación topológica será v1 v2 . Siguiendo ası́ vemos que para cada i, vi vi+1 o sea, finalmente, v1 v2 · · · vn v1 (4.92) cahiers d’informatique 68 Pero v1 vn v1 ⇒ v1 = vn , en contradicción con la hipótesis que todos los vértices fuesen distintos. Un grafo no dirigido tampoco tiene ordenación topológica. En un grafo dirigido, para cada arista (u, v) ∈ E hay otra (v, u) ∈ E, por tanto en una ordenación topológica, u tendrı́a que aparecer a la vez antes de v (ya que (u, v) ∈ E) y después (ya que (v, u) ∈ E). Pero, siendo la ordenación una permutación de los vértices, u sólo puede aparecer una vez, y por tanto la ordenación no existe. Por otro lado, el algoritmo que analizaremos en esta sección proporciona una ordenación topológica para todo grafo dirigido acı́clico, por tanto el algoritmo, junto al lema 4.12.1 contituye una demostracción del teorema siguiente. Theorem 4.12.1. Un grafo G posee una ordenación topológica si y sólo si G es un grafo dirigido acı́clico. Consideremos otra vez el grafo (4.91), relativo a la salsa de tomate. ordenación de vértices es una ordenación topológica del grafo: B G J E L H F C I A La siguiente K Para convencerse, será suficiente dibujar las aristas y averiguar que, con los vértices en este orden, todas van hacia la derecha, es decir, desde un vértice a uno que lo sigue en la ordenación: ?>=< 89:; B >=< / ?89:; G ?>=< 89:; J ?>=< 89:; E ?>=< 89:; L ?>=< /( 89:; H ?>=< / 89:; F ?>=< C 45 89:; ?>=< / 89:; I ?>=< 4/ 89:; A ?>=< /* 89:; K (4.93) La ordenación topológica de un grafo no es única. Por ejemplo, la siguiente es otra ordenación topológica del grafo de la salsa de tomate: ?>=< 89:; J ?>=< 89:; B >=< / ?89:; G ?>=< 89:; L */ ?89:; >=< H ?>=< / 89:; F 89:; ?>=< E ?>=< /4 89:; C ?>=< / 89:; I ?>=< /4 89:; A ?>=< /+ 89:; K (4.94) La ordenación topológica tiene muchas aplicaciones de gran relevancia práctica (además, por supuesto, de conseguir recetas para la salsa de tomate). Dos aplicaciones muy comunes son las siguientes: i) Sistemas de gestión de proyectos. Un proyecto es un conjunto de actividades. En un proyecto hay actividades que se pueden ejecutar en cualquier momento y actividades que sólo se pueden ejecutar tras haber completado otras. En la construcción de una casa, las ventanas se pueden instalar independentiemente de la tuberı́a, pero es necesario esperar que se haya instalado la tuberı́a para instalar el suelo. Los sistemas de gestión de proyectos usan la ordenación topologica para determinar el orden en que se ejecutarán las actividades de un proyecto. ii) Sistemas de procesamiento a flujo de datos. Estos sistemas se representan como grafos en que los vértices son procesos que consumen y producen datos y los arcos con canales a través de los cuales los datos producidos por un proceso se comunican a otros procesos. Para ejecutar un sistema de flujo de datos es necesario ordenar los procesos de forma tal que un proceso se ejecute sólo después que se han ejecutado los procesos que producen sus ingresos. La ordenación topológica proporciona un orden de ejecución que satisface estos vı́nculos. grafos 69 Para derivar un algoritmo de ordenación topológica, empezamos con una observación. grafo (4.95) muestra el resultado de una ejecución de dfs en el grafo (4.95). WVUT PQRS 17/18 HH HH H$ HIJK ONML 2/13 6> .... v v v v v .... vvvvv .... PQRS HIJK ONML WVUT 1/14 .... 19/22 .... .... .... .... WVUT PQRS HIJK / ONML 15/16 3/10 El (4.95) WVUT +3 PQRS 11/12 GG GG G# HIJK ONML WVUT +3 PQRS 20/21 7? 6/7 JJ w w JJ w ww JJ wwwww $ HIJK ONML 5/8 tt 6> t t t t tttttt HIJK +3 ONML 4/9 Concentrémonos simplemente en los tiempos de finalización f [u] de los vértices. Si ordenamos los vértices por valores decrecientes de f [u], empezando con el vértice J, que tiene f = 18, y terminando con el vértice K con f = 7, conseguimos la ordenación (4.94). Por otro lado, se puede verificar que la ordenación (4.93) deriva de otra ejecución de DFS, una que empieza con el vértice B. Esta observación nos sugiere el siguiente algoritmo para la ordenación topológica de un grafo sin ciclos. i) se ejecute DFS con la modificación siguiente: i.a) al final de la visita de un vértice (es decir, justo después de poner el estado del vértice igual a P ), insertar el vértice al principio de una lista enlazada; i.b) al final del algoritmo, devolver la lista; el orden en que los vértices aparecen en la lista constituye una ordenación topológica del grafo. Es decir, el algoritmo completo de ordenación topológica es el siguiente: L = Topologica(G) 1. L: List; L = []; 2. foreach u in V[G] do 3. estado[u] ← N; 4. π[u] ← NIL; 5. od 6. t ← 0; 7. foreach u in V[G] do 8. if estado[u] = N then 9. Topo-rec(G, u, L); 10. fi 11. od 12. return L; Topo-rec(G, u, L) 1. estado[u] ← E; 2. t ← t + 1; 3. d[u] ← t; 4. foreach v in Ady[u] do cahiers d’informatique 70 5. 6. 7. 8. 9. 10. 11. 12. 13. if estado[v] = N then π[v] ← u; Topo-rec(v); fi od estado[u] ← P; t ← t+1; f[u] ← t; insert-at-head(L, u); El vértice u se pone en la lista al instante f [u] (al paso 13, tras fijar el valor f [u] al paso 12) y, ya que u se inserta al principio de la lista, cuando el algoritmo termina la lista se encuentra ordenada por valores decrecentes de f [u]. El tiempo de ejecución de la ordenación topológica es O(V + E), ya que el algoritmo DFS se ejecuta en un tiempo O(V + E), y la inserción de un elemento al principio de una lista enlazada se ejecuta en un tiempo O(1) (o sea, es necesario un tiempo O(V ) para insertar todos los vértices del grafo en la lista). La clave para demostrar la correción del algoritmo es el teorema de la sección precedente según el cual un grafo G es acı́clico si y sólo si el bosque DF no contiene ningún arco hacia atrás. Theorem 4.12.2. Sea G un grafo dirigido acı́clico. Tras la ejecución de Topologica(G) la lista L contiene una ordenación topológica del grafo. Demostración. Para demostrar el teorema es suficiente mostrar que, si (u, v) ∈ E, entonces f [v] < f [u]: esto garantiza que v será insertado en la lista antes que u y, como todos los vértices se insertan al principio de la lista, que u estará más cerca que v al principio de la lista L que se retorna, o sea, que u aparecerá antes que v en la ordenación. Consideremos el momento en que el algoritmo DFS (o la función Topologica, que se basa en DFS) explora el arco (u, v). En este momento, el estado de u es E. El vértice v, en este momento, no puede estar en estado E, si no v serı́a un antecesor de u, (u, v) serı́a un arco hacia atrás, y por tanto el grafo tendrı́a un ciclo (por el teorema 3.11.3 de página 63). Entonces v sólo se puede encontrar en estado N o P . Si v está en estado N , por el teorema del camino blanco será un descendente de u en el árbol DF, por tanto d[u] < d[v] < f [v] < f [u]. Si v está en estado P , entonces al instante en que se visita u, ya se ha terminado de visitar v, o sea f [v] < d[u] < f [u]. Las varias posibilidades de las posiciones reciprocas de Iu y Iv se muestran en la figura 4.12, donde se evidencian las combinaciones declaradas imposibles en la demostración del teorema (recordamos que Iv y Iu tienen que ser disjuntos o uno incluido en el otro, por las propiedades del algoritmo DFS). 4.13 Componentes fuertemente conexas En esta sección consideraremos otra aplicación muy importante del algoritmo DFS: la determinación de las componentes fuertemente conexas de un grafo. Esta determinación es interesante en si misma, para la solución de algunos problemas de relevancia práctica y como algoritmo para la construción del grafo de componentes de un grafo dado, una estructura que proporciona información importante sobre la estructura del grafo inicial. La determinación de las componentes fuertemente conexas se usa también como paso inicial grafos 71 Iu Iv Iv v → ··· → u → v ¡hay ciclo! No: - - (u, v) ∈ E No: Iv - Iv f [v] < f [u] - Figure 4.12: Las posibilidades de posiciones reciprocas de Iu y Iv antes la aplicación de otros algoritmos, ya que en algunos problemas la forma más eficiente de operar sobre un grafo consiste en determinar sus componente fuertemente conexas, resolver el problema separadamente en cada componente, y luego usar el grafo de componentes para integrar las soluciones encontradas. Empecemos con dar la definición formal de componentes fuertemente conexas: Definition 4.13.1. Una componente fuertemente conexa de un grafo G es un conjunto U ⊆ V [G] que goza de las propiedades siguientes: p0 p i) para cada pareja de vértices u, v ∈ U existen en el grafo los caminos u → v y v → u; ii) no existe nungún conjunto U 0 ∈ V [G] tal que U 0 cumple i) y U ⊂ U 0 . La condición i) es la condición de conexión: implica que desde cualquier vértice de una componente fuertemente conexa se puede llegar a cualquier otro vértice. La condición ii) es la condición de maximalidad: la componente fuertemente conexa que contiene un vértice u es el conjunto más grande entre los que contienen u y cumplen la condición i). Las condiciones i) y ii) juntas implican que un camino entre dos vértices de una componente fuertemente conexa U no sale de U : Lemma 4.13.1. Sea G un grafo, U ⊆ V [G] una componente fuertemente conexa, y u, v ∈ U . p = [u, y1 , . . . , yn , v] un camino entre u y v. Entonces para todo 1 ≤ i ≤ n, es yi ∈ U . Demostración. La demostración es por contradición. Sea p Sea w un vértice del camino u → v tal 0 p 00 p que w 6∈ U . El camino p se puede dividir en dos partes p0 y p00 tales que u → w → v. q Siendo u y v en la misma componente fuertemente conexa, existe un camino v → u, por tanto q p0 que v → u → w es un camino de v a w. Desde cualquier vértice de U se puede llegar hasta v y desde aquı́, a través del camino q · p0 , hasta w. Similarmente, desde w se puede llegar hasta u y, desde aquı́ a cualquier vértice de U . Entonces el conjunto U 0 = U ∪ {w} cumple la propiedad i) de la definición y, siendo w 6∈ U , es U ⊂ U 0 . Es decir U no cumple la propiedad ii), contradiciento la hipótesis que U fuese una componente fuertemente conexa. cahiers d’informatique 72 Ejemplo Ejemplo: El grafo de la figura 4.13 contiene tres componentes fuertemente conexas: una formada por los vértices a, b, c, e ; una formada por d, g, h; y una formada sólo por i. a ?>=< 89:; 1 j b c g h i * 89:; ?>=< ?>=< / 89:; 1 Fc 1 O FF FF FF F ?>=< 89:; ?>=< / 89:; e d u: 2 < 1O x u x u x u xx uu xxx uu u ?>=< ?>=< 89:; 89:; ?>=< / 89:; 3 2 o 2 Figure 4.13: Componentes fuertemente conexas de un grafo. Definition 4.13.2. El grafo de componentes de un grafo G = (V, E) es el grafo Ĝ = (V̂ , Ê) donde V̂ contiene un vértice para cada componente conexa de G, y (u, v) ∈ Ê si hay un arco entre cualquier vértice de la componente conexa u y cualquier vértice de la componente conexa v. El grafo de componentes del grafo de figura 4.13 se ilustra en figura 4.14. ()*+ /.-, 8 Id qqq IIII q q q II qq I // .-, ()*+ ()*+qq /.-, a, b, c, e d, g, h Figure 4.14: i El grafo de componentes del grafo precedente. Theorem 4.13.1. El grafo de componente de un cualquer grafo G es acı́clico. La demostración de este teorema se deja al lector (véase el ejercicio 9 de esta sección). El algoritmo siguiente usa dos llamadas a DFS para determinar las componentes fuertemente conexas de un grafo en tiempo O(V + E): i) invocar DFS(G) para calcular los tiempos finales f[u] para cada vértice u; ii) calcular GT ; iii) invocar DFS(GT ), pero en el bucle principal (el bucle donde se llama DFS-Visit) recorrer los vértices en orden decreciente de los valores f[u] calculados al paso 1; iv) mostrar los vértice de cada árbol del bosque obtenido en 3 como componentes conexas. grafos 73 a b (0 ONML HIJK HIJK / ONML 3/6 2/7 Le LL O LL LL LL L PQRS HIJK / ONML 10/15 4/5 d WVUT 4< r9 O q r q r qq rrr qqq rrrr qqq WVUT PQRS PQRS HIJK ONML +3 WVUT 11/14 12/13 9/16 o HIJK ONML 1/8 hp g h a c i r ONML HIJK 9/16 e b c WVUT PQRS WVUT 10/15 Lo 12/13 6. PQRS LLLLL KS LLLLLL LLLL L L !) HIJK ONML PQRS WVUT o 3/4 11/14 d q KS r q r q rr qq rrr qqq r q r q ry qx HIJK HIJK ONML HIJK ONML +3 ONML 7/8 2/5 o 1/6 g h e i Figure 4.15: En (a), el resultado de la aplicación del primer paso del algoritmo de determinación de las componentes fuertemente conexas al grafo de figura 4.13, y en (b) el resultado de la aplicacióndel tercer paso a la traspuesta del grafo. Los árboles DFS de (b) corresponden a las componentes fuertemente conexas del grafo. Ejemplo: Consideremos el grafo de la figura 4.13. La figura 4.13.a muestra el resultado de una ejecución de DFS en este grafo. En cada vértice u se han marcado los instantes d[u] y f [u] y, por claridad, se han marcado los arcos del árbol DFS que se ha construido. (Estos arcos y los instantes d[u] son irrelevantes para la determinación de las componentes fuertemente conexas, ya que la primera ejecución de DFS sirve sólo para determinar los valores f [u] y, a través de ellos, el orden en que los vértices se recorrerán en la segunda ejecución.) La figura 4.13.b representa la transpuesta del grafo de figura 4.13 en que se ejecuta el segundo DFS. El primer nodo que se examina es el nodo g, que tiene el valor f [g] = 16. Partiendo de este nodo se determina un árbol DF compuesto por los vértices g, h, d. Después de la determinación de este árbol, los vértices con f = 15 y f = 14 (o sea h y d) ya están en estado P , ası́ que el próximo árbol se empieza por el vértice i, que tiene f [i] = 13; este árbol se compone sólo del vértice i. El valor de f más alto es ahora f [a] = 8, por lo cual empezamos un árbol DF del vértice a. Este árbol incluye los vértices a, b, c, e. Ahora se ha analizado todo vértice, por lo cual el DFS termina. Cada uno de los tres árboles que resultan de la última ejecución de DFS contiene los vértices de una de las componentes fuertemente conexas del grafo de figura 4.13. 4.13.1 Desarrollo del algoritmo El algoritmo tiene una estructura muy sencilla, pero merece la pena invertir un poco de tiempo en desarrollarlo más concretamente, ya que tal desarrollo constituye un útil ejercicio de adaptación de los algoritmos elementales a situaciones más complejas. Empezaremos con un cambio en la estructura que almacena los grafos. En la sección 2.7, la representación por listas de adyacencia constaba de una cadena de estructuras de tipo vertex, y la estructura arista contenı́a el ı́ndice del vértice destino. Para este algoritmo, es conveniente representar el grafo como una lista de vértices. Con esta representación los vértices ya no tienen ı́ndices, por tanto será necesario cambiar la estructura donde se almacenan las aristas de forma tal que en lugar del ı́ndice del cahiers d’informatique 74 vértice destino contengan un puntero a dicho vértice. type graph is list of vertex; type vertex is begin data: nodedata; state: { N, E, P } ady: list of edge; next: →vertex; end type edge is begin dest: next: end →vertex; →edge; También asumiremos que, si V es una lista, el bucle foreach v in V do . . . od recorra la lista ordenadamente desde el primer elemento hasta el último. Para implementar el algoritmo DFS necesitamos una función DFS-Visita per, en este caso, no necesitamos que el DFS-Visita recoga todos los datos de la versión estándar. La primera llamada, de la cual nos estamos ocupando ahora, no necesita proporcionar ninguna información sobre el tiempo de descubrimiento d[u] ni sobre los punteros π[u]: por el momento necesitamos sólo conocer el orden de finalización de los vértices. Para esto, tampoco necesitamos conocer el tiempo de finalización f [u] ya que, como se ha visto en el algoritmo de ordenación topológica, es suficiente insertar los vértices en la cabecera de una lista mano a mano que se finalizan para conseguir el órden deseado6 . Un algoritmo DFS-Visita "minimalista" es el siguiente. Nótese que el resultado de este algoritmo es una lista de vértices, es decir, un grafo. DFS-V-CC(V : graph, u : vertex) 1. out, o1: graph; 2. state[u] ]lef tarrow E; 3. out ← []; 4. foreach v in u.ady do 5. o1 ← DFS-V-CC(V, v); 6. out ← append(o1, out); 7. od; 8. state[u] ← P; 6 Por supuesto, esto no quiere decir que en esta fase del algoritmo se consigue una ordenación topológica del grafo: los grafos que nos ocupan en esta sección pueden tener ciclos, por tanto la ordenación topológica puede no existir. grafos 9. 10. 75 out ← insert-at-head(u, out); return out; En la primera llamada a DFS, todas las listas creadas por llamadas a DFS-V-CC se juntan en una que, al final de la ejecucción, contendrá todos los vértices del grafo, ordenados por valores decrecientes del tiempo de finalización. DFS-CC-1(V: graph) 1. foreach u in V do 2. state[u] ← N; 3. od 4. g, l1: graph; 5. g ← []; 6. foreach u in V do 7. if state[u] = N then 8. l1 ← DFS-V-CC(V, u); 9. g ← append(l1, g); 10. fi 11. od 12. return g; Al algoritmo para calcular la transpuesta de un grafo en esta representación es muy sencillo, y se deja como ejercicio para el lector (véase el ejercicio 10 al final de esta sección). La única advertencia es que hay que tener cuidado para construir, en el grafo transpuesta, una lista de vértices ordenada como en el grafo original, ya que este orden nos sirve para la llamada al segundo DFS. La segunda llamada a DFS es un poco distinta de la primera. Los vértices se deben recorrer en órden de finalización, pero esto no es un problema, ya que la primera llamada a DFS ha dejado la lista en el orden que se necesita. El cambio más importante tiene que ver con la preparación del resultado. Esta llamada a DFS debe retornar las componentes conexas del grafo. El número de dichas componentes es desconocido en el momento de la llamada, por tanto la solución más racional es almacenarlas en una lista, una componente en cada elemento de la lista. Cada componente conexa se produce con una llamada a DFS-V-CC, que retorna la lista de vértices de un árbol DF (correspondientes, como se verá, a una componente conexa). Esto supone que cada elemento de la lista de componentes será una lista de vértices. Es decir, el resultado de esta llamada a DFS será una lista de listas. DFS-CC-2(V: graph) 1. res: list of list of vertex; 2. l: list of vertex; 3. l ← []; 1. foreach u in V do 2. state[u] ← N; 3. od 6. foreach u in V do 7. if state[u] = N then 8. l1 ← DFS-V-CC(V, u); 9. g ← append(l1, g); 10. fi cahiers d’informatique 76 11. 12. od return g; Nótese que las primera dos funciones retornan una lista de vértices, es decir, un grafo. La función DFS-CC-2 retorna una lista de listas. En este caso, las aristas no nos interesan: sólo nos interesan los grupos de vértices que se forman en cada sub-lista, ya que estas son las componentes conexas del grafo. El algoritmo final es muy sencillo: CC(V: graph) 1. V1, V2: graph; 2. C: list of list of vertex; 3. V1 ← DFS-CC-1(V); 4. V2 ← transpose(V1); 5. C ← DFS-CC-2(V2); 6. return C; El algoritmo DFS y el algorimo transpose tienen un tiempo de ejecución O(V + E), que es también el tiempo de ejecución de CC. 4.13.2 Corrección del algoritmo El algoritmo CC es una aplicación sencilla de la búsqueda en profundidad que, a primera vista, parece no tener ninguna relación con las componentes conexas de un grafo. Todo lo que hace es establecer un orden de los vértices (en la primer llamada a DFS), y utilizar este orden para conseguir árboles DF en la transpuesta del grafo. En esta sección demostraremos que, efectivamente, estas operaciones determinan las componentes fuertemente conexas del grafo. El teorema siguiente proporciona una primera conexión, todavı́a bastante débil, entre árboles DF y componentes conexas. Theorem 4.13.2. En cualquer ejecución de DFS, los vértices de la misma componente fuertemente conexa formarán parte del mismo árbol DF. Demostración. Sea U una componente fuertemente conexa del grafo, y sea r ∈ U el primer vértice de U que se visita durante la ejecución de DFS. Sea u ∈ U otro vértice arbitrario p de U . Por definición de componente fuertemente conexa hay un camino p : r → u y, por el lema 4.13.1, p se compone enteramente de vértices de U . Siendo r el primer vértice de U que se descubre, al tiempo d[r] todos los vértices de p se encuentran en estado N. por tanto, p es un camino blanco de r a u y, por el teorema del camino blanco, u será un descendiente de r en el árbol DF. El árbol DF contiene toda la componente fuertemente conexa de su raı́z, pero puede contener también otras componentes. En el grafo ?>=< 89:; s @ === == == == ?>=< 89:; 89:; ?>=< a f o ?>=< ?>=< /89:; /89:; c b =^ == == == == 89:; ?>=< d (4.96) grafos 77 la componente conexa de s se compone de los vértices {s, a, f }, pero el árbol DF con raı́z en s abarca todo el grafo, e incluye la componente de s y la de b ({b, c, d}). Nótese que este resultado depende de la elección de la fuente: tomando b come primera fuente se consiguen dos árboles DF, cada uno correspondiente a una componente fuertemente conexa del grafo. El teorema 4.13.2 demuestra que si el árbol DF contiene un vértice de una componente fuertemente conexa, entonces contiene toda la componente. Por tanto un corolario del teorema es: Corollary 4.13.1. Sea T el conjunto de vértices que constituye un árbol DF del grafo. S Entonces T = k Uk , donde cada Uk es una componente fuertemente conexa del grafo. La idea del algoritmo, expresada en manera muy informal, es la siguiente. Un árbol DF puede contener varias componentes fuertemente conexas porque hay "puentes" entre las componentes que el algoritmo puede cruzar. En el ejemplo precedente, el arco (s, b) es un puente entre las dos componentes fuertemente conexas del grafo. Estos puentes se pueden cruzar sólo en una dirección (si no, las dos componentes serı́an la misma), mientras que entre vértices de una misma componente hay conexiones en las dos direccipnes. Por tanto, si tomamos la transpuesta del grafo, los caminos internos a la misma componente quedan abiertos, mientras que los puentes se cierran: 89:; ?>=< 89:; ?>=< c b =o @ == == == == 89:; ?>=< d ?>=< 89:; s o ^=== == == == ?>=< 89:; ?>=< / 89:; a f (4.97) Esta es la razón por la cual, en el paso final del algoritmo, se buscan árboles DF en la traspuesta. Claramente, es necesario elegir oportunamente las raices del árbol. En esta traspuesta, si empezáramos con el vértice b, conseguirı́amos un árbol que abarca todo el grafo. Por otro lado, empezando con el vértice s conseguimos las componentes fuertemente conexas. El orden correcto de visita de las raices de los árboles DF se consigue con el primer paso del algoritmo, como se demostrará a continuación. * * * En el resto de esta sección, las cantidades d[u] y f [u] indicarán el tiempo de descubrimento y el tiempo de finalización del vértice u en la primera ejecución de DFS, y p la notación u → v indicará que p es un camino de u a v en el grafo G, y no en la traspuesta GT . Dado un vértice u, su progenitor (forefather) es el vértice alcanzable desde u que finaliza por último en la primera ejecución de DFS. Es decir, el vértice tal que u → φ(u) y f [φ(u)] = max f [w] w:u→w (4.98) Es posible que φ(u) sea u mismo, ya que u es alcanzable desde si mismo. Esta observación y la segunda de las condiciones (4.98) implican que, para todo u ∈ V [G], f [u] ≤ f [φ(u)] Lemma 4.13.2. Si u → v, entonces f [φ(v)] ≤ f [φ(u)]. (4.99) cahiers d’informatique 78 Demostración: Cada vértice alcanzable desde v es también alcanzable desde u. poniendo U = {w : u → w} W = {w : v → w} se cumple V ⊆ U . Por tanto, (4.100) Por la definición de progenitor, entonces f [φ(v)] = max f [φ(v)] w∈V (4.101) y f [φ(u)] = max f [φ(v)] w∈U = max max f [w], max f [w] w∈V w∈U −V = max f [φ(v)], max f [w] w∈U −V ≥ f [φ(v)] (4.102) Lemma 4.13.3. Para cada u ∈ V [G], se cumple φ(φ(u)) = φ(u). Demostración: Por definición de φ(u), u → φ(u) is por tanto, por el lema 4.13.2, f [φ(φ(u))] ≤ f [φ(u)]. Por la propiedad (4.99), también es f [φ(u)] ≤ f [φ(φ(u))], por tanto f [φ(φ(u))] = f [φ(u)]. Ya que vértices distintos no pueden tener el mismo tiempo de finalización, es φ(φ(u)) = φ(u) Para cada componente fuertemente conexa U hay un progenitor de todos los vértices de U , y que consideraremos como el "representante" de la componente. En el primer DFS, este vértice es el primer vértice de U que se descubre, y consecuentemente el último que se finaliza. En el segundo DFS, él que se ejecuta en GT , este vértice es la raı́z del árbol DF que coincide con U . Theorem 4.13.3. En un grafo G = (V, E), se ejecute el algoritmo DFS. Al final de la ejecución, para cada vértice u, φ(u) es un antepasado de u en un árbol DF. Demostración: Si φ(u) = u el teorema se cumple banalmente. Consideremos entonces φ(u) 6= u, y analicemos el estado de φ(u) en el momento d[u] en que se descubre u. i) Si estado[φ(u)] = P, entonces f [φ(u)] < d[u] < f [u], contradiciendo la inegualdad (4.99). ii) si estado[φ(u)] = N, hay que distinguir dos casos. p ii.a) Hay un camino blanco u → φ(u) (Recordemos que al tiempo d[u] es estado[u] = N y que, por definición, hay un camino u → φ(u).) En este caso, por el teorema del camino blanco, φ(u) es un descendiente de u en el árbol DF. Por tanto f [φ(u)] < f [u], contradiciendo la inegualdad (4.99). p ii.b) En el camino u → φ(u) hay por lo menos un vértice cuyo estado no es N. Sea w el último de estos vértices, es decir, sea el camino de u a φ(u) r q u → w → φ(u) (4.103) grafos 79 donde q es blanco. El estado de w en este momento no puede ser P porque, si no, durante su análisis se habrı́a cruzado el camino q y el estado de φ(u) no podrı́a ser N, como estamos hipotizando. Si estado[w] = E, hay un camino blanco de w a φ(u), y φ(u) será un decendiente de w en el árbol DF, lo que supone f [w] > f [φ(u)]. Pero hay un camino de u a w y por el lema 4.13.2, f [φ(w)] ≤ f [φ(u)], es decir f [w] > f [φ(u)] ≥ f [φ(w)] (4.104) contradiciendo la igualdad (4.99) para el vértice w. Por tanto, al tiempo d[u] tiene que ser estado[φ(u)]=E y, por el teorema del paréntesis, φ(u) será un antepasado de u en el árbol DF. Un corolario de este teorema nos proporciona otra relación entre árboles DF y componentes fuertemente conexas: Corollary 4.13.2. En cada ejecución de DFS en un grafo G, para cada vértice u, u y φ(u) pertenecen a la misma componente fuertemente conexa. Demostración: Hay un camino u → φ(u) por la definición de progenitor, y un camino φ(u) → u porque, por el teorema precedente, φ(u) es un antepasado de u en el árbol DF. El teorema siguiente nos proporciona una condición más fuerte. Theorem 4.13.4. En un grafo G, los vértices u y v se encuentran en la misma componente fuertemente conexa si y sólo si en cualquier ejecución de DFS los dos tienen el mismo progenitor. Demostración: Supongamos primero que los dos tengan el mismo progenitor φ. Por el corolario 4.13.2, u y v están en la misma componente fuertemente conexa de φ. Supongamos ahora que u y v estén en la misma componente fuertemente conexa del grafo. Entonces existen caminos u → v y y → u. Aplicando el lema 4.13.2 a los dos conseguimos f [φ(v)] ≤ f [φ(u)] f [φ(u)] ≤ f [φ(v)] (4.105) Por tanto f [φ(v)] = f [φ(u)] y, ya que vértices distintos no pueden finalizar al mismo tiempo, φ(u) = φ(v). Sea Tr en árbol DF con raı́z en r encontrado durante la segunda ejecución de DFS. Nótese que, a pesar de que ahora consideremos árboles encontrados durante la segunda ejecución de DFS en GT , las cantidades d[u], f [u], y el progenitor φ(u) siguen siendo referidas a la primera ejecución de DFS, la ejecución en el grafo G. Sea C(r) = {v ∈ V : φ(v) = r} (4.106) el conjunto de vértices que tienen r como progernitor. Por el teorema 4.13.4, cada C(r) es una componente fuertemente conexa del grafo. Para demostrar la corrección del algoritmo, nos queda demostrar que cada C(r) coincide con Tr . Lemma 4.13.4. Si C(r) 6= ∅, entonces r ∈ C(r). cahiers d’informatique 80 Demostración: Si C(r) 6= ∅, hay por lo menos un vértice v tal que v ∈ C(r), es decir, un vértice v tal que φ(v) = r. Por tanto ≡ ≡ ≡ ≡ r = φ(v) φ(r) = φ(φ(v)) (aplicación de φ) φ(r) = φ(v) (lema 4.13.3) φ(r) = r (definición del φ(v)) r ∈ C(r) (definición de C(r)) (4.107) Lemma 4.13.5. Durante cualquier ejecución de DFS, todo vértice u ∈ C(r) se introduce en Tr . Demostración: Por el teorema 4.13.2, todo vértice en la misma componente fuertemente conexa se pone en el mismo árbol DF, es decir, por el teorema 4.13.4, todos los vértices de C(r) se introducen en el mismo árbol DF. Ya que, por el lema ??, r ∈ C(r), y r es la raı́z de Tr , este árbol es Tr . El lema 4.13.5 proporciona una parte de la demostración de corrección: el algoritmo introduce toda la componente conexa C(r) en el árbol Tr . Queda por demostrar que sólo la componente C(r) se introduce en Tr , es decir, que si w ∈ V se introduce en Tr , entonces φ(w) = r. Esto se demostrará basándose en dos lemas, que establecen las condiciones para f [φ(w)]. Lemma 4.13.6. Si un vértice w se introduce en Tr , entonces f [φ(w)] ≥ f [r]. Demostración: Si w se introduce en Tr , entonces hay en GT un camino r → w y por tanto hay en G un camino w → r. Por tanto f [φ(w)] ≥ f [φ(r)] (lema 4.13.2) = f [r] (demostración del lema 4.13.4) (4.108) Lemma 4.13.7. Si un vértice w se introduce en Tr , entonces f [φ(w)] ≤ f [r]. Demostración: La demostración es por inducción sobre el número de árboles DF que se crean a lo largo del algoritmo. Base: Sea Tr el primer árbol que se crea. Por la ordenación de los vértices en el bucle principal de la segunda ejecución de DFS, f [r] es el valor máximo de todos los tiempos de finalización, por tanto f [φ(w)] ≤ f [r]. Inducción: suponga que el lema se cumple por los primeros n árboles, Tr1 , . . . , Trn , y que se inserta w en Trn+1 . Asumamos por contradicción que f [φ(w)] ≥ f [rn+1 ]. Entonces existe r0 tal que f [φ(w)] = f [r0 ] y por tanto w ∈ C(r0 ). Por la ordenación del bucle principal de DFS, si f [r0 ] > f [rn+1 ], entonces en el momento en que se están introduciendo vértices en el árbol Trn+1 , el árbol Tr0 ya se ha analizado, es decir, Tr0 es uno de los árboles Tr1 , . . . , Trn . Ya que w ∈ C(r0 ) y el árbol Tr0 ya se ha construido, por el lema 4.13.5 w ya se ha introducido en Tr0 , y por tanto no se puede introducir en Trn+1 . Con estos lemas, la demostración de la corrección del algoritmo es inmediata. grafos 81 Theorem 4.13.5. Tras ejecutar el algoritmo CC en un grafo G = (V, E), para cada vértice u ∈ V se cumple que u ∈ Tr si y sólo si u ∈ C(r), y cada C(r) es una componente fuertemente conexa del grafo. Demostración: C(r) es una componente fuertemente conexa del grafo por el teorema 4.13.4. Si u ∈ C(r), entonces u se introduce en Tr por el lema 4.13.5. Por otro lado, si u se introduce en Tr , los lemas 4.13.6 y 4.13.7 demuestran que f [φ(u)] = f [r] y, es decir, por la unicidad de los tiempos de ejecución, que φ(u) = r. Por tanto, que u ∈ C(r). cahiers d’informatique 82 4.13.3 Ejercicios 1. Ordenen topológicamente los grafos siguientes. Asuman que las listas de adyacencias estén ordenadas alfabéticamente y, en el bucle principal del algoritmo DFS, que se visiten los vértices en orden alfabético (o sea, el primer árbol DFS siempre tendrá como raı́z el vértice a). ()*+ /.-, a b ()*+ //.-, /.-, ()*+ /.-, o ()*+ ()*+ 8/.-, d e f a b ()*+ /.-, /.-,H ()*+ / z z HH HH zz zz zz H# z} zz z } ()*+ ()*+D g /.-, c /.-, DD vv DD v D! v{ vv ()*+? /.-, ()*+ /.-, ?? ? d f ??? ? ()*+ /.-, ()*+ //.-, O c ()*+ /.-, b /.-, ()*+ ()*+ /.-, /.-,> ()*+ >> >> > ()*+ /.-, ()*+ //.-, ()*+ //.-, a c e ()*+ /.-, a b // .-, ()*+ d /.-, /()*+ /.-, /()*+ c a b ()*+ /.-, ()*+o /.-, o z= O Hc HH z= O HH zz zz z z z H zz ()*+ ()*+Dza g /.-, c /.-, ; O v DD v DD v v D vv ()*+ /.-, ()*+_? /.-, A ?? ?? f ?? d ? ()*+ /.-, d ()*+ /.-, b /.-, /()*+ ()*+ //.-, c ()*+ //.-, a a d b ()*+ /.-, e c /.-, /()*+ B f e 2. Repitan el ejercicio precedente ejecutando el bucle principal de DFS en orden alfabético inverso (o sea, de forma tal que el primer árbol BFS tenga como raı́z el último vértice en orden alfabético). 3. La ordenación topológica de un grafo no es única. Se pueden encontrar varias ordenaciones de un mismo grafo. Entre todos los grafos con n vértices, ¿cuál es el grafo que permite el mayor número de ordenaciones topológicas diferentes? ¿Cuál es el grafo que permite el mı́nimo número? ¿Cuántas ordenaciones diferentes permiten los grafos citados? Para el caso n = 5 dibujen estos dos grafos. 4. Demuestren que un grafo acı́clico con n vértices siempre contiene n componentes fuertemente conexas. 5. Demuestren que un grafo es acı́clico si, y sólo si, es posible enumerar sus vértices de forma tal que su matrı́z de adyacencia es triangular con ceros en la diagonal. grafos 83 6. El grafo siguiente no es acı́clico. ¿Qué pasa si se ejecuta el algoritmo de ordenación topológica en el grafo? ¿El algoritmo termina o produce un resultado? Si el algoritmo produce un resultato, muéstrelo. t ()*+ /.-, /.-, ()*+_>OO /.-, /.-, ()*+ / /()*+ >>OOO oooo7 ??? O >>ooOoOO ? oo> OOO' ?? ()*+ /.-, ()*+ ()*+oo /.-, /.-, ()*+ /.-, \ \ p z u v s q w (4.109) 7. Determinen las componentes fuertemente conexas del grafo del ejercicio precedente. 8. Determinen las componentes fuertemente conexas de los grafos siguientes y dibujen el grafo de componentes correspondiente. a /.-, ()*+ O b // .-, ()*+ /.-, /()*+ /.-,o ()*+ /.-,o ()*+ /.-, ()*+ /.-, ()*+ d e f b a ()*+ /.-, a /.-, ()*+ _ a /.-, ()*+ _ c c b /.-, ()*+ b ()*+ /.-, O /.-, ()*+ d a /.-,> ()*+ >> >> > // .-, ()*+ c b /.-,> /.-, ()*+ ()*+ / >> >> > ()*+ /.-, ()*+ //.-, d a ()*+ /.-, [ /.-, ()*+ d e b /.-, ()*+ C [ c b ()*+> /.-, /.-, ()*+ ()*+o //.-, O >> O >>> ? >> >> > > ()*+ /.-, ()*+ /.-, // .-, ()*+ e &()*+ //.-, /.-, ()*+ /()*+ / .-, /.-,o ()*+ /.-, ()*+ e a ()*+ //.-, g f c f c ()*+ /.-, C d ()*+ /.-, ? a b .-, //()*+ ? /.-, ()*+ h /.-, ()*+ / O c ()*+ //.-, c b ()*+ /.-, ()*+ /.-, ()*+ /.-, / yy yy O y y y y yy yy /.-,y| // .-, ()*+ ()*+y| /.-, /()*+ a d e∗f 9. Demuestren que el grafo de componentes de cualquier grafo es acı́clico. 10. Escriban el algoritmo transpose para la representación del grafo con lista de vértices, de forma tal que con la llamada V1 ← transpose(V), los vértices en los grafos V y V1 tengan el mismo orden. cahiers d’informatique 84 V. PROBLEMAS DE MÍNIMO EN GRAFOS PONDERADOS 5.14 Caminos de coste mı́nimo En esta sección trataremos un problema muy común y con importantes aplicaciones: la determinación del camino de coste mı́nimo (o, simplemente, del camino mı́nimo ) entre dos, o más, vértices de un grafo. En las secciones precedentes ya hemos encontrado una versión de este problema: el algoritmo BFS, como hemos visto, encuentra el camino con el menor número de arcos de la fuente s a cualquier vértice alcanzable desde ella. Es decir, encuentra el camino de coste mı́nimo si el coste de un camino se define como el número de arcos que lo compone. En esta sección consideraremos una generalización importante de este problema, que consiste en asignar a cada arco del grafo un peso o coste. Al fin de darse cuenta de la importancia del peso en las aplicaciones, consideremos el ejemplo (2.42) de la red de una compa~ nı́a aerea: JFK vv 3932 vv v vv vv LAX SSS SSSS SSSS SS 10181 SSSS SS 5766 MAD H HH HH959 HH 1263 HH LHR 780 CDG (5.110) 831 FCO 856 BCN Si consideramos los caminos más cortos simplemente en términos de número de arcos, ası́ como se hacı́a con el algoritmo BFS, resulta que da lo mismo ir de Madrid (MAD) a Roma (FCO) pasando por Parı́s (CDG) y Barcelona (BCN) que pasando por Nueva York (JFK) y Los Angeles (LAX): ambos recorridos se componen de tres arcos. Al contrario, si tomamos en cuenta los pesos de las aristas del grafo y los interpretamos como el coste de recorrer la arista, vemos que pasar por Parı́s nos "cuesta" 2.646Km, mientras pasar por Los Angeles nos "cuesta" 19.879Km. Los pesos pueden suponer una diferencia importante en problemas de mı́nimo. grafos 5.15 85 Caminos en grafos ponderados Los grafos ponderados, permiten modelizar algunas situaciones que no se pueden modelizar adecuadamente con un grafo simple como, por ejemplo, el grafo de aeropuertos del ejemplo precedente. La formalización del concepto de grafo ponderado se encuentra en la siguiente definición. Definition 5.15.1. Un grafo ponderado es un par (G, w), donde G es un grafo y w : E[G] → R es una función que asocia a cada arco (u, v) ∈ E un peso w(u, v). Esta definición asocia un peso w a un arco (u, v) pero, para los problemas de esta sección, nos interesa más bien asociar pesos a caminos, o sea, a conjuntos de arcos contı́guos. Para este fin, necesitamos una convención que nos diga como se componen los pesos de conjuntos de arcos. Esta operación de composición debe ser conmutativa (el coste es igual, independientemente del orden en que se recorren los arcos) y asociativa (recorrer e1 seguido por e2 y e3 es igual a recorrer e1 y e2 seguido por e3 ). La manera más sencilla de componer pesos de forma conmutativa y asociativa es sumando: el peso del camino p = [v1 , . . . , vn ] se define como w(p) = n−1 X w(vi , vi+1 ) (5.111) i=1 Esta forma de composición es útil en muchos problemas prácticos. Funciona bien, por ejemplo, en el caso de la lı́nea aerea: la longitud de una ruta compuesta por dos tramos es igual a la suma de las longitudes de los dos tramos. En otros problemas se necesitan otras formas de composición. Por ejemplo, si los pesos representan probabilidades, la manera más natural de componerlos es por multiplicación: w(p) = n−1 Y w(vi , vi+1 ) (5.112) i=1 Los algoritmos de esta sección asumirán siempre que los pesos se componen por suma. De toda manera, si el problema impone encontrar un camino mı́nimo con composición de coste por multiplicación, es muy fácil modificar la función de coste para utilizar los algoritmos que aquı́ presentamos. Dados los pesos w(vi , vj ) consideremos los pesos modificados ŵ(vi , vj ) = log w(vi , vj ). Por la nota propiedad de los logaritmos, logab = loga + logb, hay logw(p) = log n−1 Y w(vi , vi+1 ) (5.113) i=1 = n−1 X logw(vi , vi+1 ) i=1 = n−1 X ŵ(vi , vi+1 ) i=1 Por tanto, remplazando los pesos dados por sus logaritmos, los algoritmos de esta sección (con composición por suma) determinarán el camino que minimiza logw(p). Por la monotonı́a de la función logaritmo, el mismo camino minimiza w(p). cahiers d’informatique 86 Análogamente, si el problema nos pide encontrar el camino de coste máximo entre dos vértices (como es el caso, por ejemplo, cuando el peso de un arco representa una ganancia), será suficiente remplazar cada w(vi , vj ) con −w(vi , vj )7 . El coste del camino mı́nimo de u a v se define como p min{w(p) : u −→ v} si existe un camino entre u y v δ(u, v) = ∞ en otro caso (5.114) Un camino mı́nimo entre u y v es cualquier camino p de u a v con w(p) = δ(u, v). El problema del camino mı́nimo se presenta de varias formas. En esta sección nos ocuparemos sobre todo del problema del camino más corto con única salida : dado un grafo G y un vértice de salida (o fuente ) s, se quiere encontrar el camino mı́nimo desde s hasta cualquier vértice v ∈ V . Otras variantes del camino mı́nimo se pueden resolver usando los algoritmos que estudiaremos aquı́: único destino: dado un grafo G y un vértice destino u, determinar el camino mı́nimo desde cualquier vértice v de G a u; el problema es equivalente a resolver el problema de la única salida en GT ; único par: dados dos vértices, encontrar el camino mı́nimo entre estos vértices; el problema se resuelve de forma óptima con el algoritmo de única salida; todos los pares: encontrar el camino mı́nimo entre todo par de vértices; el problema se puede resolver aplicando el algoritmo de única salida a todos los vértices, aún si hay formas más eficientes de hacerlo. 5.15.1 Ciclos de coste negativo Hay que hacer una distinción muy importante en los problemas de camino mı́nimo según si la función de peso w es siempre no negativa o si el grafo puede contener arcos de coste negativo. Si los arcos de coste negativo no dan lugar a ningún ciclo de coste total negativo, entonces el problema del camino mı́nimo queda bien definido y, para cada vértice u del grafo, es posible definir el coste del camino mı́nimo δ(s, u) y darle un valor finito. Consideremos, por ejemplo, el grafo de figura 5.16. El grafo contiene dos arcos de coste negativo (w(e, b) = −2 y w(c, d) = −5), pero el coste del único ciclo es positivo (w[b, c, e, b] = 2). Existen una infinidad de caminos entre, por ejemplo, a y d: [a, b, c, d], [a, b, c, e, b, c, d], [a, b, c, e, b, c, e, b, c, d], etc., pero cada vez que se recorre el ciclo el coste del camino aumenta de 2, ası́ que el camino mı́nimo es el que nunca recorre el ciclo: [a, b, c, d], con coste w([a, b, c, d]) = −2. Por otro lado, si el grafo contiene un ciclo de coste negativo, tal como el grafo de figura 5.17, el camino mı́nimo entre dos vértices puede no existir. Para cada camino 7 A primera vista, parece que esta sustitución nos pueda causar problema porque puede introducir ciclos de coste negativo y por tanto, como veremos en la sección siguiente, transformar en indefinido un problema que, originariamente, era definito. En realidad no es ası́. Es fácil darse cuenta que un problema de camino máximo en un grafo con ciclos de coste positivos es indeterminado exactamente como un problema de mı́nimo en un grafo con coste negativo: siempre se puede aumentar la ganancia recorriendo una vez más el ciclo. Ası́ que si, después de la sustitución, nos aparece un ciclo de coste negativo en el grafo, el problema inicial era mal puesto. Por otro lado, veremos que el algoritmo de Dijkstra acepta sólo grafos con función de coste no negativa y por tanto con la sustitución propuesta no serı́a posible su aplicación hasta en problemas bien puestos. En este caso se pueden utilizar otra funciones: cada sustitución ŵ(vi , vj ) = f (w(vi , vj )) con f decreciente transformará un problema de máximo en uno de mı́nimo. Se puede poner, por ejemplo, ŵ(vi , vj ) = 1/w(vi , vj ). grafos 87 a 89:; ?>=< a b 1 c 2 /89:; ?>=< ?>=< /89:; c b <^ << << 2 −2 << 89:; ?>=< d d −5 ?>=< / 89:; e e Figure 5.16: negativo. Un grafo con pesos de coste negativo pero sin ciclos de coste total 89:; ?>=< a Figure 5.17: 1 1 / 89:; 2 /89:; ?>=< ?>=< ?>=< /89:; c e b =^ == == −1 −2 = 89:; ?>=< d Un grafo con un ciclo de coste negativo. entre s y u que roce o contenga el ciclo, es posible encontrar un camino de coste menor dando una vuelta más al ciclo. En el caso del grafo de figura 5.17, indiquemos con [a, b, c, (e, b, c)n , d] el camino entre a y d que se consigue dando n vueltas al ciclo. El coste de este camino es w([a, b, c, (e, b, c)n , d]) = 4 − n (5.115) En este caso es δ(a, d) = −∞, y el camino no es definido. El algoritmo de Dijkstra, que se analizará en la próxima sección, asume que todos los pesos del grafo son positivos, ası́ que el problema de los ciclos negativos no se presenta. El algoritmo de Bellman-Ford acepta grafos con pesos negativos, y está en grado de detectar la presencia de ciclos negativos. 5.15.2 Árboles de caminos mı́nimos En muchos problemas nos interesa una representación del camino más que el mero coste del camino mı́nimo entre la fuente y un vértice cualquera. Puede ser interesante saber que en una red de metro se puede ir de la estación A a la estación B recorriendo, en el caso mejor, 15Km, pero mucho más nos interesa saber como hacerlo, o sea, por que estaciones hay que pasar para recorrer este camino mı́nimo. Los caminos mı́nimos entre la fuente y los demás vértices del grafo G = (V, E) se guardan usando la misma técnica usada en el algoritmo BFS: a cada vértice v ∈ V se le asocia un predecesor π[v]. Los caminos mı́nimos entre la fuente8 y los demás vértices del grafo constituyen el (sub-)grafo de caminos Gπ = (Vπ , Eπ ), donde Vπ = {s} ∪ {v ∈ V |π[v] 6= Nil} (5.116) Eπ = {(π[v], v)|v ∈ (Vπ − {s}) } (5.117) y 8 Recordamos que nos estamos ocupando del problema de encontrar caminos mı́nimos con única salida. cahiers d’informatique 88 ?>=< 89:; ;C a 2 ~~ ~ ~ ~~~~ ?>=< 89:; s> 3 >> >> 5 89:; ?>=< b 89:; ?>=< ;C a 2 ~~ ~ ~ ~~~~ 89:; ?>=< s >> 3 >>>> >># 5 89:; ?>=< b Figure 5.18: Un grafo puede contener varios árboles de caminos mı́nimos: mismo grafo, los dos árboles (a) y (b) son de caminos mı́nimos. aquı́ para el En el caso del algoritmo BFS se ha demostrado que el grafo Gπ era un árbol y que el camino desde la fuente s hasta cualquier vértice v ∈ Vπ correspondı́a al camino más corto, en términos de número de arcos, entre s y v en el grafo. Los algoritmos de esta sección construyen un grafo Gπ que es también un árbol (el árbol de caminos mı́nimos ) que contiene, pero, los caminos de coste mı́nimo entre s y cualquier vértice v ∈ Vπ (es decir, hasta cualquier vértice alcanzable desde s). Los caminos de coste mı́nimo no son necesariamente únicos y, consecuentemente, un grafo puede contener varios árboles de caminos mı́nimos. Un ejemplo se ilustra en la figura 5.15.2 El algoritmo BFS es un algoritmo de camino mı́nimos con única salida para grafos en que w es una constante (todos los arcos tienen el mismo peso). 5.15.3 Estructura de los caminos mı́nimos Los algoritmos de búsqueda del camino mı́nimo utilizan una propiedad importante: los trozos de un camino mı́nimo son ellos mismos caminos mı́nimos entre los vértices que unen. Esta propiedad se formaliza en el lema siguiente: Lemma 5.15.1. Sea G = (V, E) un grafo con función peso w : E → R, sea p = [v1 , . . . , vk ] el camino mı́nimo entre v1 y vk y, para cada i, j, con 1 ≤ i ≤ j ≤ k, sea pij = [vj , . . . , vj ] el sub-camino de p entre los vértices vi y vj . Entonces pij es un camino mı́nimo entre vi y vj . p1i pij pjk Demostración: El camino p se puede descomponer como v1 −→ vi −→ vi −→ vk , con coste w(p) = w(p1i ) + w(pij ) + w(pjk ). Supongamos que existe un camino p0 entre vi y vj con coste p1i p0 pjk w(p0 ) < w(pij ). Entonces v1 −→ vi −→ vi −→ vk es un camino entre v1 y vk con coste w0 = w(p1i ) + w(p0 ) + w(pjk ) < w(p), contradiciendo la hipótesis que p era un camino mı́nimo entre v1 y vk . Los dos lemas siguientes demuestran propiedades de la función δ de distancia mı́nima entre los vértices de un grafo y una fuente dada. Se trata de generalizaciones a grafos ponderados de propiedades similares que se han demostrados durante el análisis de la busqueda en profundidad. Lemma 5.15.2. Sea G = (V, E) un grafo con función peso w : E → R. p0 Supongamos que el camino mı́nimo p entre s y v se puede descomponer en s −→ u → v, con u ∈ V y (u, v) ∈ E. Entonces el peso del camino más corto entre s y v es δ(s, v) = δ(s, u) + w(u, v). grafos 89 Demostración: Por el lema precedente, p0 (que es un sub-camino de p), es el camino mı́nimo entre s y u, entonces δ(s, v) = w(p) = w(p0 ) + w(u, v) = δ(s, u) + w(u, v) (5.118) Lemma 5.15.3. Sea G = (V, E) un grafo ponderado dirigido con función peso w : E → R y fuente s. Entonces, para cada arco (u, v) ∈ E se cumple δ(s, v) ≤ δ(s, u) + w(u, v). Demostración: El camino mı́nimo entre s y v tiene un coste δ(s, v), y este es menor o igual al coste de cualquier otro camino entre s y v. En particular, es menor o igual al coste p0 del camino s −→ u → v, es decir δ(s, u) + w(u, v). 5.16 Relajación Los algoritmos de determinación de caminos mı́nimos usan una técnica llamada relajación, y mantienen, para cada vértice v, dos atributos: el predecesor π[v] de v en el camino que se está encontrando, y una estimación d[v] de la distancia del camino mı́nimo entre v y la fuente. Como en el caso de la busqueda BFS (que, recordémolos, es un algoritmo de caminos mı́nimos para grafos ponderados con función de peso constante), a lo largo de la ejecución de un algoritmo, d siempre será una estimación por exceso de la distancia mı́nima, o sea, se cumplirá siempre d[v] ≥ δ(s, v). En el algoritmo BFS, el valor d[v] se inicializaba a +∞ para cada vértice v y durante la ejecucción del algoritmo, se cambiaba sólo una vez, justo antes de insertar el vértice v en la cola, asignándole el valor final δ(s, v). En los algoritmos de esta sección las cosas son más complicadas: el valor d[v] se cambiará (relajará) varias veces durante la ejecución, decrementándolo cada vez, y demostraremos que, al final de la ejecución de cada algoritmo de esta sección, se cumplirá d[v] = δ(s, v). Las estimaciones del camino más corto se inicializan como en el caso del BFS: Inicializa(G, s) 1. foreach v ’cmdin V[G] do 2. d[v] ← ∞; 3. π[v] ← NIL; 4. od; 5. d[s] ← 0 El proceso de relajación de un arco (u, v) consiste en verificar si se puede mejorar la estimación actual de d[v] pasando por u y, consecuentemente, cambiando d[v] y π[v]. El proceso de relajación puede reducir el valor d[v] si pasando por u se obtiene un camino de coste menor que la estimación actual, pero no puede en ningún caso aumentar el valor d[v], una propiedad que se usará muchas veces en los teoremas de esta sección. El código siguiente relaja el arco (u, v) de un grafo con función de peso w: Relaja(u, v, w) 1. if d[v] > d[u] + w(u, v) then cahiers d’informatique 90 2. 3. 4. d[v] ← d[u] + w(u, v); π[v] ← u; fi La figura 5.19 ilustra dos ejemplos de aplicación de la función de relajación: en el primer caso el camino por u mejora la estimación actual d[v], mientras que en el segundo no; consecuentemente en el segundo caso la estimación d[v] no cambia. d[u]=5 ul d[u]=5 ul Figure 5.19: segundo no. 2 @ @ 2 - vld[v]=9 d[u]=5 ul - vld[v]=7 d[u]=5 ul Dos ejemplos de relajación: 2 @ @ 2 - vld[v]=6 - vld[v]=6 en el primero se cambia el valor d[v], en el La corrección de los algoritmos de búsqueda del camino más corto depende de las propiedades de la función de relajación, propiedades que se establecerán en los lemas siguientes. Observamos que se trata de propiedades que se cumplen para cualquier tipo de ejecución de Inicializa seguido por una o más ejecuciones de Relaja, independientemente del hecho que estas llamada sean parte o menos de una búsqueda de camino más corto. Tanto el algoritmo de Dijkstra como el de Bellman-Ford se componen de una llamada a Inicializa seguida por varias llamadas a Relaja. La diferencia entre los dos consiste en la secuencia de arcos que se relajan y en el criterio de terminación utilizado. Lemma 5.16.1. Sea G = (V, E) un grafo dirigido ponderado con función peso w : E → R, y sea (u, v) ∈ E. Entonces, después de relajar (u, v) se cumple d[v] ≤ d[u] + w(u, v). Demostración: Si antes de llamar Relaja(u, v, w) ya es d[v] ≤ d[u] + w(u, v), la función no cambia d[v], por tanto la propiedad sigue cumpliendose después de la llamada. Si d[v] > d[u] + w(u, v), entonces la función pone d[v] = d[u] + w(u, v). Lemma 5.16.2. Sea G = (V, E) un grafo dirigido ponderado con función peso w : E → R, s ∈ V el vértice de salida y sea el grafo inicializado por Inicializa(G, s). Entonces, después de un numero arbitrario de llamadas a Relaja, con cualesquiera arcos de E, para cada v ∈ V se cumple d[v] ≥ δ(s, v). Si después de alguna llamada se cumple d[v] = δ(s, v), entonces d[v] no variará en ninguna de las llamadas siguientes. Demostración: Después de la inicialización la desigualdad se cumple banalmente: d[s] = δ(s, s) ≥ 0 (δ(s, s) = −∞ si s se encuentra en un ciclo negativo, y 0 en otro caso), y d[v] = ∞ ≥ δ(s, v) para cualquier vértice v 6= s. La demostración de la validez general del lema es por contradicción. Supongamos que el lema no se cumpla, y sea v el primer vértice para el cual una relajación del arco (u, v) causa d[v] < δ(s, v). Entonces, después de la relajación, se cumple d[u] + w(u, v) = d[v] < δ(s, v) ≤ δ(s, u) + w(u, v) (porque en este paso se ha relajado d[v]) (por la hipótesis) (por el lema 5.15.3) (5.119) grafos 91 o sea, d[u] < δ(s, u). Pero, ya que relajar (u, v) no cambia en ningún caso d[u], esta relación se cumplı́a antes de relajar el arco (u, v), contradiciendo la hipótesis que v fuese el primer vértice para el cual el lema no valı́a. Entonces debe ser, d[v] ≥ δ(s, v) para cada vértice v ∈ V a lo largo de cualquier número de llamadas a Relaja. Si en algún momento se cumple d[v] = δ(s, v), el valor de d[v] no puede decrementar, en cuanto acabamos de demostrar que d[v] ≥ δ(s, v). Por otro lado, la función Relaja solo puede decrementar el valor de d[v], entonces d[v] ni siquiera puede aumentar. Este lema es importante en cuanto nos garantiza que no es posible "estropear" una solución llamando demasiadas veces a Relaja. Relaja es, en este sentido, una función segura : si ya hemos encontrado el camino mı́nimo para llegar a un vértice u y seguimos relajando los arcos que entran en u, el resultado no cambia. El lema siguiente se refiere al uso de Inicializa y Relaja en un algoritmo de búsqueda del camino mı́nimo, y es de importancia fundamental para demostrar la corrección de los algoritmos de esta sección. Lemma 5.16.3. Sea G = (V, E) un grafo dirigido ponderado con función peso w : E → R, sea s ∈ V el vértice de salida y sea el grafo inicializado por Inicializa(G, s). Supongamos p que s −→ u → v sea el camino mı́nimo para dos vértices u, v ∈ V , y que se ejecute una serie de pasos de relajaciń que incluya Relaja(u, v, w). Si d[u] = δ(s, u) antes de la llamada a Relaja(u, v, w), entonces d[v] = δ(s, v) en cualquier momento después de la llamada. Demostración: Por el lema 5.16.2, si d[u] = δ(s, u) en cualquier momento de la serie de la relajación de (u, v), entonces la igualdad se cumple en cualquier momento sucesivo. En particular, después de relajar (u, v), se cumple d[v] ≤ d[u] + w(u, v) = δ(s, u) + w(u, v) = δ(s, v) (por el lema 5.16.2) (por la hipótesis de este lema) (por el corolario 5.15.2 y porque el camino es un camino mı́nimo) (5.120) Pero por el lema 5.16.2, se cumple d[v] ≥ δ(s, v), ası́ que debe ser d[v] = δ(s, v). Al final de todo algoritmo de esta sección, los valores d[u] serán iguales a la distancia del camino mı́nimo entre la fuente s y el vértice u. Además, los punteros π[u] definirán el grafo Gπ = (Vπ , Eπ ) definido en 5.116 y 5.117. Los lemas siguientes caracterizan este grafo. Lemma 5.16.4. Sea G = (V, E) un grafo ponderado con función peso w : E → R sin ciclos de coste negativo y s ∈ V . Tras llamar a la función Inicializa(G,s) y un número arbitrario n ≥ 0 de veces la función Relaja(u, v, w) con cualesquiera vértices u y v del grafo, para cada vértice v ∈ Vπ , el grafo de caminos Gπ contiene un camino de s a v. (n) (n) (n) Demostración: Sea Gπ = (Vπ , Eπ ) el grafo que se consigue tras n llamadas a Relaja. (n) Demostraremos por inducción sobre n que el lema se cumple por cada Gπ . (0) Si n = 0, Gπ contiene sólo la fuente s, y el lema se cumple banalmente. Sea el lema (n) cierto para Gπ , y supongamos que la próxima llamada a Relaja relaje el arco (u, v). Hay que distinguir dos casos: i) antes de la ejecucción se cumplı́a d[v] ≤ d[u] + w(u, v); en este caso, la llamada a Relaja (n+1) (n) no cambia nada, Gπ = Gπ , y el lema sigue cumlpiéndose. cahiers d’informatique 92 ii) antes de la ejecución se cumplı́a d[v] > d[u] + w(u, v); el único cambio concerne v, por (n+1) tanto será suficiente demostrar que en Gπ hay un camino de s a v. Si d[v] > d[u] = w(u, v), entonces antes de la llamada era d[u] < ∞ y, en el momento en que se habı́a asignado el valor d[u], también se habı́a puesto d[u] 6= Nil (a menos que no fuese u = s). Supongamos que esto haya occurrido en la llamada número k a Relaja (k) (k ≤ n). El vértice se ha insertado en Vπ en esta llamada y, como π[u] no puede (n) (n) volver nunca a ser Nil, es también u ∈ Vπ . (Si era u = s, claramente u ∈ Vπ , ya (n) que s está en todo Vπ por definición.) Por la hipótesis inductiva, hay un camino p (n) (n) s → u que, encontrándose en Gπ , está formado por vértices de Vπ . Siendo (n+1) (n) (n+1) (n+1) Vπ =π ∪{v}, p se encuentra también en Gπ y, siendo π[v] = u, (u, v) ∈ Eπ . p v (n+1) Por tanto s → u → es un camino de s a v en Gπ . Theorem 5.16.1. Sea G = (V, E) un grafo ponderado con función peso w : E → R sin ciclos de coste negativo y s ∈ V . Tras llamar a la función Inicializa(G,s) y un número arbitrario n ≥ 0 de veces la función Relaja(u, v, w) con cualesquiera vértices u y v del grafo, el grafo Gπ es un árbol con raı́z s. Demostración: Por el lema 5.16.4 hay un camino de s a cualquier vértice de Gπ , por tanto, si se ignora el sentido de las aristas de Gπ , se consigue un grafo no dirigido conexo (para ir de un vértice u a cualquier vértice v se puede ir de u a s en sentido contrario al sentido de las aristas y luego de s a v). Por definición del grafo Gπ , es |Eπ | = |Vπ | − 1. 27), Gπ es un árbol. Por tanto, por el teorema 2.6.2 (página Theorem 5.16.2. Sea G = (V, E) un grafo ponderado con función peso w : E → R sin ciclos de coste negativo y s ∈ V . Tras llamar a la función Inicializa(G,s) y se llame la función Relaja(u, v, w) un número de veces tal que, al final, para cada vértice del grafo sea d[u] = δ(s, u). Entonces Vπ contiene todos los vértices alcanzables desde s y, para cada u ∈ Vπ el camino (único) de s a u en Gπ es un camino de coste mı́nimo de s a u en el grafo G. Demostración: Demostremos primero que Vπ es el conjunto de vértices alcanzables desde s. La distancia δ(s, v) es finita si y sólo si v es alcanzable y, por tanto, d[v] es finito si y sólo si v es alcanzable. Pero para cada v 6= s se asigna a d[v] un valor finito en la misma llamada a Relaja en que se pone π[v] 6= Nil, es decir, cuando se pone v en Vπ . Por tanto, v ∈ Vπ si y sólo si v es alcanzable desde s. p Dado ahora v ∈ Vπ , demostraremos que el (único) camino s → v en Gπ es un camino mı́nimo en el grafo. Sea p = [v0 , . . . , vk ], con v0 = s y vk = v. Para cada i con 1 ≤ i ≤ k se cumple d[vi ] = δ(s, vi ) (hipótesis del teorema). También, para cada vi es π[vi ] = vi−1 . Entonces, la última vez que se cambió vi , se puso π[vi ] ← vi−1 d[vi ] ← d[vi−1 ] + w(vi−1 , vi ) (5.121) Desde entonces, vi no se ha vuelto a cambiar y, si se ha cambiado d[vi ], su valor sólo se puede haber reducido (una llamada a Relaja nunca incrementa el valor d), ası́ que será: d[vi ] ≥ d[vi−1 ] + w(vi−1 , vi ), (5.122) grafos 93 es decir w(vi−1 , vi ) ≤ δ(s, vi ) − δ(s, vi−1 ) (5.123) Sumando para todos los vértices del camino conseguimos w(p) Pk = w(vi−1 , vi ) Pi=1 k ≤ (δ(s, vi ) − δ(s, vi−1 ))(por la (5.123)) i=1 = δ(s, vk ) − δ(s, v0 ) (suma telescópica) = δ(s, vk ) (porque v0 = s) (5.124) Es decir, w(p) ≤ δ(s, vk ). Pero siendo δ(s, vk ) la distancia de camino mı́nimo de s a vk es también w(p) ≥ δ(s, vk ) y por tanto w(p) = δ(s, vk ), o sea, w(p) es un camino mı́nimo. La definición del grafo Gπ asume implı́citamente que π[s] = Nil. Si no fuera ası́, no tendrı́amos ninguna justificación para eliminar s del conjunto de vértices que se utilizan en la definición de Eπ , y ya no se podrı́a deducir que |Eπ | = |Vπ | − 1, una propiedad que necesitamos para demostrar que Gπ es un árbol. El teorema siguiente justifica esta decisión demostrando que π[s] 6= Nil sólo cuando el grafo contiene ciclos de coste negativo. Theorem 5.16.3. Sea G = (V, E) un grafo ponderado con función peso w : E → R sin ciclos de coste negativo y s ∈ V . Tras llamar a la función Inicializa(G,s) y un número arbitrario n ≥ 0 de veces la función Relaja(u, v, w) con cualesquiera vértices u y v del grafo, Si en el curso de estas llamadas se pone en algun momento π[s] 6= Nil, entonces hay en el grafo un ciclo de coste total negativo. Demostración: Empecemos demostrando que el grafo tiene un ciclo y que, en el momento en que se pone π[s] 6= Nil, este ciclo es un ciclo del grafo Gπ . Sea (vk , s) la arista relajando la cual se pone por primera vez π[s] 6= Nil. En este momento, todavı́a no se ha cambiado el vértice s que, por tanto, sigue teniendo los valores que le habı́a dado Inicializa. Durante esta llamada, la condición del if de la linea 1 de Relaja se cumple. Por tanto es 0 = d[s] > d[vk ] + w(vk , s) (5.125) condición que implica d[vk ] < ∞. Por el lema 5.16.2 es siempre d[vk ] ≥ δ(s, vk ) y, por p tanto, δ(s, vk ) < ∞. Es decir, vk es alcanzable desde s a través de un camino s → vk . A~ nadiendo a este camino la arista (vk , s) se consigue un ciclo en el grafo. En el momento en que se puso d[vk ] < ∞, también se puso π[vk ] 6= Nil y desde este momento, p es vk ∈ Vπ . Por el lema 5.16.4 hay un camino s → vk en Gπ . Después de relajar el arco p s (vk , s) se pone π[s] = vk y por tanto (vk , s) ∈ Eπ . EL ciclo s → vk → es entonces un ciclo en Gπ . Queda por demostrar que este ciclo tiene un coste negativo. Por tal fin, se escribe el ciclo como p = [v0 , . . . , vk , vk+1 ], con v0 = vk+1 = s. En el momento en que se relaja el arco (vk , s) se cumple la (5.125), que podemos escribir como d[vk+1 ] > d[vk ] + w(vk , vk+1 ) (5.126) Para los demás vi , se cumple π[vi ] = vi−1 y por tanto la última vez que se cambió π[vi ] ← vi−1 d[vi ] ← d[vi−1 ] + w(vi−1 , vi ) (5.127) cahiers d’informatique 94 Desde entonces, vi no se ha vuelto a cambiar y, si se ha cambiado d[vi ], su valor sólo se puede haber resucido (una llamada a Relaja nunca incrementa el valor d), ası́ que será: d[vi ] ≥ d[vi−1 ] + w(vi−1 , vi ), (5.128) Sumando la (5.126) y todas las (5.128) se consigue k+1 X i=1 d[vi ] > k+1 X d[vi−1 ] + i=1 k+1 X w(vi−1 , vi ) (5.129) i=1 pero, siendo v0 = vk+1 , es k+1 X d[vi ] = i=1 k+1 X d[vi−1 ] (5.130) i=1 y por tanto 0> k+1 X w(vi−1 , vi ). (5.131) i=1 Es decir, el camino p es un ciclo de coste total negativo. 5.17 El algoritmo de Dijkstra El algoritmo de Dijkstra determina el camino mı́nimo entre un vértice de salida y los demás vértices de un grafo dirigido ponderado con función de peso no negativa. Gracias a esta limitación a priori, para este algoritmo no tendremos que preocuparnos de la presencia de ciclos con coste negativo. El algoritmo mantiene un conjunto S de vértices de que ya se ha determinado el camino mı́nimo; es decir, tal que para todo v ∈ S, se cumpla d[v] = δ(s, v). En cada iteración, el algoritmo selecciona el vértice v ∈ V − S con el valor mı́nimo de d[v], relaja todos los arcos que salen de v, e introduce v en S. En la implementación del algoritmo se usa una cola de prioridad Q para guardar los vértices de V − S. Dijkstra(G, w, s) 1. Inicializa(G, s); 2. S ← ∅; 3. Q ← V[G]; 4. while not empty(Q) do 5. u ← Q.extract min; 6. S ← S ∪ {u}; 7. foreach v in u.ady do 8. Relax(u, v, w); 9. od; 10. od; El algoritmo relaja los arcos como ilustrado en figura 5.20. La lı́nea 1 inicializa el valor de distancia de todo vértice, excepto la fuente, a +∞, y las lı́neas 2 y 3 inicializan el conjunto se vértices analizados (que en este momento está vacı́o), y la introducen todos los vértices en la cola de prioridad Q (que, en todo momento, contiene el conjunto V − S de vértices no analizados). A cada iteración del bucle, el vértice de V − S con la estimación de distancia menor se extrae de Q (en la primera iteracción el grafos 95 vértice extraı́do será, por supuesto, s, dado que todos los demás vértices tienen una estimación igual a ∞), y lo inserta en S. Las lı́neas 7 y 8 relajan todos los vértices que están en el entorno del vértice extracto de Q. s t u 8 89:; 89:; /) ?>=< / ?>=< ∞ :v ∞ v: U v v 1 vvv 3 vvv 1 2 5 vv vv 2 vvv 4 + vvv 9 ?>=< 89:; ?>=< 89:; 89:; /5 ?>=< ∞ k 1 ∞ ∞ ?>=< 89:; 0 w s 12 4 x 15 [s, t, u, w, x, y] t 12 y u )+3 @ABC 8 89:; GFED / ?>=< 3 @ 8 ; 11T x y y x y y x y 3 x 1 yy 1 2 5 xx 2 yyyy yyyyyyy4 xxxx . & 9 @ABC GFED 89:; ?>=< 89:; 6 2 j 1 ?>=< 19/ 17 ?>=< 89:; 0 w 4 15 x [x, u, y] y s t u 8 ?>=< GFED /%- @ABC +3 89:; 12 ; 4 x v; T x v x v 3 vv 1 xx 2 5 1 x v 2 vv xxxx 4 v v 9 + ?>=< 89:; ?>=< 89:; ?>=< /5 89:; ∞ ∞ 1 2 j ?>=< 89:; 0 12 4 15 w x [w, t, u, x, y] t 12 s w u s 4 15 w x [u, y] t 12 s y t u %-/ GFED 8 ?>=< @ABC / 89:; 3 @ 8 ; 12T x y y x y y x y 3 x 1 yy 2 5 1 xx 2 yyyy yyyyyyy4 xxxx . & 9 89:; ?>=< ?>=< G@ABC FED 6 2 j 1 89:; 19/ 17 89:; ?>=< 0 y (/ 89:; 8 ?>=< ?>=< / 89:; 3 7? 9 T @ 8 x y x y x y x y x y 1 yy 3 xx 1 2 5 xxxx 2 yyyy yyyyyyy4 xxxxxxx . & 9 @ABC GFED ?>=< 89:; ?>=< 2 j 1 89:; 6 5+3 15 89:; ?>=< 0 s 12 4 15 x [t, x, u, y] t 12 y u (/ 89:; 8 ?>=< ?>=< / 89:; 3 7? 9 T @ 8 x y x y x y x y x y 3 xx 1 yy 1 2 5 xxxx 2 yyyy yyyyyyy4 xxxxxxx . & 9 G@ABC FED ?>=< 89:; ?>=< 6 2 j 1 89:; 5/ 11 89:; ?>=< 0 w 4 15 x [y] y u (/ 89:; 8 ?>=< ?>=< / 89:; 7? 9 T 8@ 3 x y x y x y x y x y 1 yy 3 xx 2 5 1 xxxx 2 yyyy yyyyyyy4 xxxxxxx . & 9 ?>=< @ABC GFED 89:; ?>=< 2 j 1 89:; 6 5/ 11 89:; ?>=< 0 w 4 x [] 15 y Figure 5.20: Ejecución del algoritmo de Dijkstra. Los vértices indicados con un cı́rculo no se han todavı́a analizados, y se encuentran en la cola Q; los vértices indicados con un doble cı́rculo ya se han analizados, y se encuentran en el conjunto S; el vértice cuadrado es el vértice que se elegirá en el próximo ciclo a la lı́nea 4. Los valores en los cı́rculos son la estimación de la distancia del camino más corto. Tiempo de ejecución En la lı́nea 3, todos los vértices se insertan en la cola; en cada iteración del bucle 4--10 se extrae un vértice, y nunca se vuelve a insertar ningún vértice, Por tanto, el bucle "while" se ejecuta |V | veces. La inicialización toma un tiempo O(V ). Si la cola de prioridad de implementa con un heap, la función extract min se ejecuta en un tiempo O(log V ), por tanto el tiempo de ejecución del bucle, excepto por la llamada a Relaja, es O(V log V ). La función Relaja se llama una vez para cada arco. Si en la función se cambia el valor de d[v], es necesario ejecutar la función heapify para reconstituir el heap (se vea el cahiers d’informatique 96 capı́tulo II), operación que toma un tiempo O(log V ). En el caso peor, heapify se ejecuta cada vez que se ejecuta Relaja, o sea una vez para cada arco del grafo, por un tiempo total de O(E log V ). El tiempo total de ejecución del algoritmo es entonces O((V + E) log V ). Es posible mejorar este tiempo usando una estructura de heap de Fibonacci para implementar la cola de prioridad. En esta estructura, reajustar el heap después de la relajación toma un tiempo medio O(1), ası́ que el tiempo de ejecución del algoritmo es O(V log V + E). Corrección En cada iteración el algoritmo de Dijkstra elige el vértice de Q con el valor mı́nimo de d, lo saca de la cola, lo analiza, y lo inserta en el conjunto S: una vez que un vértice se ha elegido, la decisión no se cambia, y el vértice queda en S. Una estrategia de este tipo se llama estrategia avara (greedy strategy). Estas estrategias llegan muy rápidamente a una solución en los problemas de optimización en que se pueden utilizar, porque sólo tienen que tomar una decisión para cada elemento de la solución, sin necesidad de volver a analizar y quizás a cambiar decisiones ya tomadas. Desafortunatamente, no todos los problemas de mı́nimo admiten ser solucionados con una estrategia avara (de hecho, muy pocos lo admiten); por otro lado, el problema de camino mı́nimo sin pesos negativo si lo hace, como podremos corroborar demostrando la corrección del algoritmo de Dijkstra. La clave para demostrar que el algoritmo encuentra una solución óptima es mostrar que, cada vez que se inserta un vértice u en al conjunto S, se cumple d[u] = δ(s, u). Theorem 5.17.1. Tras ejecutar el algoritmo de Dijkstra en un grafo G = (V, E) con funcion peso w : E → R+ ∪ {0} y salida s ∈ V , se cumple d[v] = δ(s, v) para todo v ∈ V . Demostración: La demostración se efectuará por inducción sobre el número de vértices que se salen de la cola de prioridad Q. Después de la inicialización, se cumple d[s] = 0 y v 6= s ⇒ d[v] = ∞. Entonces la primera vez que se ejecuta la instrucción 5, el vértice que sale de la cola es s y, para este vértice, d[s] = 0 = δ(s, s). Supongamos ahora que el teorema se cumple para los primeros k vértices que se extraen de la cola, y sea v el vértice número k + 1 que se extrae. Si v no es accesible, se cumple d[v] ≥ δ(s, v) = ∞ (por el lema 5.16.2), o sea d[v] = ∞ = δ(s, v). Si v es accesible, consideremos un camino de coste mı́nimo de s a v. Sea y el primer vértice de este camino que todavı́a se encuentra en la cola, y x el vértice que lo precede (se vea la figura 5.21). p p0 Escribamos el caminos cómo s −→ x → y −→ v. Demostremos que d[y] = δ(s, y). Ya que x ha salido de la cola antes de v, para la hipótesis p de inducción se cumple d[x] = δ(s, x). Por el lema 5.15.1, s −→ x → y es un camino de coste mı́nimo de s a y y, en el ciclo en que se sacó x de la cola, en paso 8, se llamó Relaja(x, y, w) luego, por el lema 5.16.2 se cumple d[y] = δ(s, y). Entonces tenemos las desigualdades siguientes: d[y] = δ(s, y) ≤ δ(s, v) ≤ d[v] (como se ha demostrado arriba) (porque los pesos no son negativos y el camino hasta v es más largo) (por el lema 5.16.1) (5.132) grafos 97 Por el otro lado, v sale de la cola antes de y, ya que v es el próximo elemento que sale, entonces d[v] ≤ d[y]. Las dos desigualdades proporcionan d[v] = d[y] = δ(s, v). Después que d[v] ha alcanzado el valor δ(s, v) no cambia por el lema 5.16.2. - A s AA Q - x - y v 6 A AA Figure 5.21: Ilustración de la dimostración de la corrección del algoritmo de Dijkstra. Corollary 5.17.1. El sub-grafo Gπ = (Vπ , Eπ ), donde Vπ = {s} ∪ {v ∈ V |π[v] 6= Nil}, (5.133) Eπ = {(π[v], v)|v ∈ Vπ − {s}} (5.134) y π[v] es el valor asignado por el algoritmo de Dijkstra define un árbol formado por caminos de coste mı́nimo. Demostración: La demostración se efectua por inducción sobre el número de vértices que constituyen un camino. Si este número es cero, el camino es de s a s, y el teorema se cumple banalmente. Supongamos que el teorema se cumpla para caminos de k arcos, y sea v un vértice cuyo camino contiene k + 1 arcos. El valor π[v] se asigna en el momento en que se asigna d[v], y d[v] tiene el valor d[v] = δ(s, v) = d[π[v]] + w(π[v], v) = δ(s, π[v]) + w(π[v], v), donde d[π[v]] = δ(π[v], v) por la hipótesis de inducción, ya que el camino hasta π[v] contiene k arcos. Entonces el camino hasta π[v] es una camino de coste mı́nimo, luego el camino hasta v también es de coste mı́nimo. 5.18 El algoritmo de Bellman-Ford A diferencia del algoritmo de Dijkstra, el algoritmo de Bellman-Ford se aplica al caso general en que un grafo ponderado contenga pesos positivos y negativos, o sea en que el dominio de la función w : E → R no es restringido a números no negativos. Esta mayor generalidad, claramente, se paga: el tiempo de ejecución del algoritmo de Bellman-Ford es mayor (asintóticamente) que él del algoritmo de Dijkstra. Como ya el algoritmo de Dijkstra, el algoritmo de Bellman-Ford usa la técnica de la relajación, disminuyendo la estimación de camino más corto entre la fuente y cada vértice v ∈ V hasta estabilizarse cuando todas las estimaciones han llegado al valor δ(s, v). El algoritmo retorna FALSE si el grafo contiene ciclos negativos accesible desde el vértice de salida. Bellman-Ford(G, w, s) 1. Inicializa(G, s); cahiers d’informatique 98 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. for i=1 to |V|-1 do foreach (u,v) in E do Relaja(u, v, w); od od foreach (u,v) in E do if d[v] > d[u] + w(u, v) then return FALSE; fi; od; return TRUE; (hay un ciclo negativo) El algoritmo ejecuta |V | − 1 veces un bucle (lı́nea 2) en que se relaja una vez cada arco del grafo (lı́neas 3-5). Un ejemplo de ejecución del algoritmo se encuentra en figura 5.22. Observamos que no hay un orden definido en que se relajan los arcos (ası́ como pasaba en el algoritmo de Dijkstra): el algoritmo se basa en la propiedad que relajando cada arco |V | − 1 veces siempre se consigue encontrar el camino más corto para cada vértice, a menos que haya ciclos con coste negativo, en cual caso el ciclo se detecta con el test de lı́nea 8. w * 89:; ?>=< 89:; ?>=< 89:; / ?>=< ∞ Aj −2 ∞ ∞A U AA AA }> O } AA AA }} AA9 AA}} AA 8 }A 1 7 −3 }} AA −4 −1 AA AA } A } A A }} * ?>=< ?>=< 89:; 89:; 89:; / ?>=< 7 ∞ j ∞ ∞ z v 5 6 9 −3 u z y x v w ) 89:; 6 89:; ?>=< 89:; / ?>=< 5 3 ==em −2 ?>=< 0= U == ==== <D O == ==== ==9 ==== == 8 == 1 7 −1 == −3 =====−4 = == ==" %- ?>=< ?>=< 89:; 89:; >=< / ?89:; 7 8 2 1 i 9 5 u −3 x y z v w * 6 89:; ?>=< ?>=< 89:; ?>=< +3 89:; 0 >> 6 @i −2 ? ∞O U >>>> @@ ~ ~ > @@ ~~ >>> @@~~ >>>>9 ~@ >>> 8 7 1 > −3 −1 ~~ @@@−4 >>>> ~ @ ~ >># ~~ @ * 89:; 89:; ?>=< 89:; ?>=< ?>=< / 7 ∞ ∞ 1 i 5 9 u z −3 y w x v ) 89:; 6 ?>=< 89:; ?>=< ?>=< / 89:; 3 ??em 0= −2 ;C 5O U == ???? == ???? ==9 ???? == 8 1 ??????−4 7 −3 −1 == ???? == # % 89:; ?>=< GFED ?>=< 89:; / @ABC 7 −1 8 1 i 9 5 −3 u x y z v w ) 6 ?>=< 89:; ?>=< ?>=< +3 89:; 0= 6 ==i −2 89:; <D 5O U == ==== == ==== ==9 ==== == 8 == 7 1 −1 == −3 =====−4 = = ==" = %- 89:; ?>=< 89:; ?>=< ?>=< / 89:; 7 8 2 1 i 9 5 u z −3 y w x v ) 89:; 6 ?>=< ?>=< 89:; ?>=< / 89:; 3 ??em 0= −2 ;C 5O U == ???? == ???? ==9 ???? == 8 1 ??????−4 7 −3 −1 == ???? == # % 89:; ?>=< @ABC 89:; ?>=< / GFED 7 −1 8 1 i 9 5 −3 u x Figure 5.22: Una ejecución del algoritmo Bellman-Ford. El vértice de salida es z; los valores de d son reportados dentro de los cı́rculos que representan los vértices. Se asume que, a cada paso, los arcos se relajan en orden lexicográfico. La primera figura muestra la situación después de la inicialización, las siguentes muestran la situación después de cada ejecución del bucle. En las últimas dos la situación no ha cambiado: el algoritmo se ha estabilizado y desde este momento ninguna llamada a Relaja cambiará la situación. y grafos 99 Tiempo de ejecución La inicialización del algoritmo toma un tiempo O(V ). Cada una de las |V | − 1 ejecuciones del bucle 2--6 toma un tiempo O(E) (cada una consta en |E| ejecuciones de Relaja), y el bucle de las lı́neas 7--12 toma un tiempo O(E). Por tanto, el tiempo de ejecución del algoritmo es O(V E). Estas observaciones subrayan que la posibilidad de funcionar con grafos de coste negativo se paga en tiempo de ejecución: el algoritmo Bellman-Ford es más lento que el algoritmo de Dijkstra que, recordamos, tiene un tiempo de ejecución asintótico de O((V + E) log V ). El tiempo asintótico de ejecución del algoritmo no se puede mejorar, pero se puede mejorar el tiempo efectivo de ejecución, por lo menos en la mayorı́a de los casos. El algoritmo standard de Bellman-Ford relaja |V | veces todos los arcos, incluso los arcos que ya se han estabilizado, o sea, los arcos por los que ya se ha encontrado un camino mı́nimo. Se puede mejorar la velocidad del algoritmo si se visitan en cada iteración sólo los vértices por los cuales en la iteracción precedente se ha cambiado la estimación de distancia. Esta versión del algoritmo usa una cola para guardar los vértices que se van a visitar. Bellman-Ford-Fast(G, w, s) 1. Inicializa(G, s); 2. enqueue(s, Q); 3. enquqeued[s] ← true; 4. while Q 6= ∅ do 5. u ← extract(Q); 6. enqueued[u] ← false; 7. foreach v in u.ady do 8. if d[v] > d[u] + w(u, v); then 9. d[v] ← d[u] + w(u, v); 10. π[v] ← u; 11. if not enqueued[v] then 12. enqueue(v, Q); 13. enqueued[v] ← true; 14. fi 15. fi 16. od 17. od Este algoritmo no funciona con ciclos negativos: si hay un ciclo negativo, la estimación del coste de los vértices que lo componen seguirá bajando y los vértices se seguirán insertando en la cola. Por tanto, el algoritmo no termina. Se puede modificar el algoritmo, marcando el número de veces que un vértice se introduce en la cola, de manera tal que se detecten ciclos de coste negativo. Esta modificación, y la propiedad en que se basa, son el objeto de los ejercicios 9 y 10 de esta sección. Corrección Para demostrar la corrección del algoritmo de Bellman-Ford (no optimizado), empezamos demostrando que, si no hay ciclos negativos, el algoritmo determina el camino más corto para todo vértice accesible desde el vértice de salida. Lemma 5.18.1. Sea G = (V, E) un grafo dirigido ponderado con vértice de salida s y función cahiers d’informatique 100 de peso w : E → R, sin ciclos de coste negativo accesibles desde s. Entonces, al final de la ejecución de Bellman-Ford, se cumple d[v] = δ(s, v) para todo vértice v accesible desde s. Demostración: Sea v un vértice accesible desde s, y sea p = [v0 , . . . , vk ] un camino más corto de s a v, con v0 = s, vk = v. Ya que no hay ciclos negativos, el camino p es simple (no contiene ciclos), y por tanto k ≤ |V | − 1. Demostramos por inducción que, para i = 0, . . . , k, tras la iteración número i del algoritmo se cumple d[vi ] = δ(s, vi ) y que esta igualdad se mantiene durante todas las iteraciones siguientes. Recordamos que cada iteración consiste sólo en llamadas a la función Relaja, por tanto los lemas 5.15.3--5.16.2 se cumplen. Para i = 0, después de la inicialización, se cumple d[s] = 0 = δ(s, s) y, para el lema 5.16.1, esta igualdad no cambia después. Supongamos ahora que tras i − 1 iteraciones, se cumpla d[vi−1 ] = δ(s, vi−1 ). En la iteración número i se relaja el arco (vi−1 , vi ) y por tanto, por el lema 5.16.2, se cumple d[vi ] = δ(s, vi ), y por el lema 5.15.3 la igualdad se mantiene en las iteraciones siguientes. Como k < |V | − 1, el algoritmo ejecutará por lo menos k iteraciones, ası́ que al final se cumplirá d[v] = δ(s, v). Este lema contiene la idea fundamental del algoritmo. El lema 5.16.2 nos garantiza que, si p = [v0 , . . . , vk ] es un camino más corto y, en un momento dado de la ejecución, ya hemos estimado correctamente p hasta el vértice vi , una llamada a Relaja(vi , vi+1 , w) nos proporcionará una estimación correcta hasta vi+1 . Entonces, llamando k veces la función Relaja con los arcos correctos, al final tendremos la estimación correcta del camino. Si no hay ciclos negativos, ningún camino contiene más de |V | − 1 arcos, por tanto relajando |V | − 1 veces todos los arcos del grafo se encontrarán todos los caminos. Para terminar la demostración de la corrección de Bellman-Ford, solo queda demostrar que si no hay ciclos negativos el algoritmo retorna TRUE y si los hay el algoritmo retorna FALSE. Estos resultados, ası́ como el resultado precedente, forman el teorema siguiente: Theorem 5.18.1. Sea G = (V, E) un grafo ponderado con salida s ∈ V y peso w : E → R; se ejecute Bellman-Ford en este grafo. Entonces, si el grafo no contiene ciclos negativos, el algoritmo retorna TRUE y al final de cumple d[v] = δ(s, v) para todo vértice v. Si el grafo contiene un ciclo negativo accesible desde s, el algoritmo retorna FALSE. Demostración: Supongamos que el grafo no contenga ciclos negativos. Demostramos que d[v] = δ(s, v) para todo v. Si v es accesible desde s, entonces el lema 5.18.1 demuestra el resultado. Si v no es accesible desde s, el lema 5.16.2 garantiza que d[v] ≥ δ(s, v) = ∞, o sea d[v] = ∞ = δ(s, v). Todavı́a debemos demostrar que el algoritmo retorna TRUE. Al final del bucle a las lı́neas 2--6 se cumple, para cada arco (u, v) ∈ E d[v] = δ(s, v) ≤ δ(s, u) + w(u, v) = d[u] + w(u, v) (por el lema 5.16.1) (5.135) por tanto ningún arco cumple la propiedad del if de lı́nea 8, y la instruccción return FALSE no se ejecuta. Se ejecuta entonces la lı́nea 12, y el algoritmo retorna TRUE. Supongamos ahora que el grafo contenga un ciclo de coste negativo c = [v0 , . . . , vk ] con v0 = vk accesible desde s. Entonces k X w(vi−1 , vi ) < 0 (5.136) i=1 grafos 101 Si Bellman-Ford retornara TRUE, el if de la linea 8 no se cumplirı́a para ningún arco o sea, para los arcos del ciclo, se cumplirı́a d[vi ] ≤ d[vi−1 ] + w(vi−1 , vi ), i = 1, . . . , k. Sumando estas desigualdades a lo largo del ciclo c se obtiene k X i=1 d[vi ] ≤ k X d[vi−1 ] + i=1 k X w(vi−1 , vi ). (5.137) i=1 Cada vértice de c aparece exactamente una vez en cada una de las primeras dos sumas (recordamos que v0 = vk ), ası́ que k X d[vi ] = i=1 entonces 0≤ k X d[vi−1 ], (5.138) i=1 k X w(vi−1 , vi ), (5.139) i=1 contradiciendo la hipótesis que el ciclo fuese de coste negativo. retorna FALSE. Por tanto el algoritmo cahiers d’informatique 102 5.19 Ejercicios 1. Ejecuten el algoritmo de Dijkstra en los grafos siguientes, empezando por el vértice marcado con una s. /.-,o 7 /.-, ()*+ ()*+ { O `AAA 7 }} O { AA }} {{ A} 1 {{ 2 { }} AAA4 2 { } A } { 2 ()*+{} /.-, ()*+ ()*+ //.-,~} 1 /.-, / u ?>=< 89:; s U 3 6 1 /.-,`A 7 ()*+ ()*+ // .-,o O AA }> } AA }} A} 2 }} AAA 5 } A } ()*+}o 7 3 /.-, /.-, ()*+ 1 ()*+ /.-, 89:; ?>=< / sC { O CC { CC {{2 CC 4 4 {{ { C 2 {{ 8 CC! { } ()*+ ()*+ /.-, / //5 .-, 2 6 5 2 ()*+Mf /.-,; 2 //.-, ()*+ MMM2 O ; ;; MMM 2 ; 2 M ; ()*+ /.-, ;; 2 2 8D q q ; 2 q q ; q ; qq ()*+o 2 /.-, /.-, ()*+q 5 ()*+ /.-, 3 //.-, k()*+ kkk k k k kkk kkk 5 k k k ()*+ku k /.-, // .-, ()*+ // .-, ()*+ 1 7 >=< / ?89:; s 9 # 3 ()*+ /.-, /.-,8o ()*+ ()*+o /.-, I 8 2 3 _??? 1 88 ?? 88 89:; s 3 88 9 ?>=< 2 88 ????1 8 ? 6 ()*+o /.-, ()*+ /.-, ()*+ //.-, 17 )/.-, /.-,Bi ()*+ ()*+Bo ()*+ /.-, O BB 3 BB13 |= U | BB || BB B|B| BB 7 1 9 B BB 2 ||| BBBB 7 5 B! || B! ()*+ /.-, /.-, ()*+o 89:; ?>=< s 5 O |O 15 | O Z | | | 2 7 | | || 4 3 || | | || 3 || ||1 }||| } | ()*+ /.-, ()*+ ()*+ //.-, //.-, 5 8 8 2 1 5 4 2 1 1 4 3 ()*+ /.-, /.-,O ()*+ ()*+ //.-, / 5Z D 33 OOO2 5 O 5 33 OO 55 OOO 33 525 OO 1 3 33 OOOO 55 7 OO' 1 ()*+ /.-, ?>=< / 89:; s 4 * ()*+ /.-, /.-,Hj ()*+ 3 O O HH u u HH uu HH u H# zuuu 2 1 ()*+j /.-, 1 2 x 8 5 xxx 7 x xx * 89:; ()*+x| o /.-, ?>=< s 2 1 5 2 //.-, ()*+ 89:; ?>=< s? ?? ??4 1 3 ?? ? ()*+ /.-, ()*+ //.-, 5 9 2. Dibujen un grafo y una fuente s con la existe un árbol de caminos mı́nimos con caminos mı́nimos con fuente s que no lo este tipo que contiene tres vértices. siguiente propiedad: para cada arco (u, v) ∈ E fuente s que contiene (u, v) y otro árbol de contiene. (Sugerencia: existe un grafo de El grafo contiene arcos de coste negativo.) 3. ¿Dónde falla la demostración de la corrección del algoritmo de Dijkstra si el grafo contiene arcos de coste negativo? 4. Den un ejemplo de grafo con arcos de coste negativo por el cual el algoritmo de Dijkstra da una respuesta equivocada. 5. Escriban el algoritmo de Dijkstra para un grafo representado como matrı́z de adyacencia. ¿Cuál es el tiempo de ejecución de esta versión? 6. Ejecuten el algoritmo de Bellman-Ford en los grafos del ejercicio 1. 7. Ejecuten el algoritmo de Bellman-Ford en los grafos siguiente comprobando, al final del algoritmo, si el grafo contiene ciclos de coste negativo grafos 103 4 4 ()*+ /.-, /.-,aBo ()*+ ()*+ /.-, / BB 5 |D = aBBB | | | BB || 1 BB || B B | 1 | |B |B 3 4 || BBB 3 ||| BBB | B }| | 1 B || 3 ()*+ /.-, /.-, ()*+ /.-, / /()*+ 2 ()*+ ()*+ −4 //.-, /.-,aBo −4 /.-, ()*+ BB−3 ||D = aBBB | | BB || BB || B|B| 6 5 B||B −1 4 B | 2 | BB || BBB | | B }| | 1 B | 2 ()*+| /.-, ()*+ ()*+ //.-, //.-, −2 1 7 $/.-, 3 ()*+ ()*+ /.-, /.-,d ()*+ / 00 −2 g 00 4 00 1 3 00 −2 −1 &/ ?89:; ()*+ /.-, >=< s 3 tt /.-,t ()*+ O ()*+Ke /.-, ()*+ /.-, KK t t KK tt 2 K ytt −5 ()*+ /.-, −7 −1 /.-, ()*+ 9 KK 5 1 ttt KK KK t t t % 1 ()*+ /.-, /.-, ()*+fMo O X11 MMM 11 MMM1 MMM −1 −1 11 MMM 11−2 M /.-, ()*+Ke ()*+ /.-, 1 KK 11 tt KK 1 t t −1 K ytt −1 ()*+ /.-, /.-, ()*+ 9 KK 3 KK KK % ()*+ /.-, 9 ttt /.-, ()*+ O d 1 & $/.-, 89:; ?>=< ()*+>o 1 /.-, ()*+?d s 3 1 ? > O 8 ? > ? > > ? > ? >>−3 1 ?? 8 3 1 5 >> ? > −2 ??$ −2 ()*+o /.-, ()*+d /.-, ()*+ /.-, ()*+o /.-, 0 3 ()*+ ()*+ −4 //.-, /.-,aBo −4 /.-, ()*+ BB−3 ||D = aBBB | | BB || BB || B|B| 3 5 B|B| −1 4 B | 2 | BB || BBB | | | −1 B }| | −2 B ()*+| /.-, ()*+ ()*+ //.-, //.-, −2 2 /.-, ()*+ 9 KK 5 KK KK t t % 1 ()*+ /.-, ()*+tfMo /.-, XO 11 MMM 11 MMM1 MMM −1 −1 11 MMM 111 M ()*+Ke /.-, ()*+ /.-, 1 KK 11 t KK 1 tt t −1 K ytt −1 ()*+ /.-, 1 ttt 1 4 4 8. El diámetro de un grafo es la máxima distancia, en número de arcos, que se puede encontrar entre cualquier par de vértices. Por ejemplo, el grafo siguiente tiene un diámetro igual a 2. ()*+? /.-, ()*+ /.-, ?? ?? ?? ()*+ /.-, ()*+ /.-, Dado un grafo G con un diametro D < V , ¿que simple modificación se puede hacer al algoritmo Bellman-Ford (versión no optimizada) para que ejecute más rapidamente? ¿Cuál es el tiempo de ejecución de la versión modificada? 9. En el algoritmo de Bellman-Ford optimizado, demuestren que si un vértice se introduce más de |V | veces en la cola, el grafo contiene un ciclo de coste negativo. 10. Utilizando la propiedad del ejercicio precedente, modificar el algoritmo Bellman-Ford optimizado de manera tal que detecte la presencia de ciclos de coste negativo. cahiers d’informatique 104 5.20 Árboles abarcadores mı́nimos En muchos problemas prácticos se necesita conectar una serie de elementos con el coste de conexión más bajo posible. Consideremos por ejemplo un circuito eléctrico con n puntos que deben ser conectados usando la menor cantidad posible de alambre. El problema se puede modelar como un grafo no dirigido ponderado, con pesos positivos G = (V, E), donde V es el conjunto de puntos que se deben conectar, E es el conjunto de conexiones posibles, y el coste de un arco w(u, v) ≥ 0 representa la cantidad de alambre necesaria para realizar una conexión directa entre u y v. El problema que nos ponemos es conectar todos los puntos con el menor coste posible, o sea, determinar un sub-grafo conexo G0 = (V, T ), con T ⊆ E que contenga todos los vértices de V y tal que el coste total de los arcos de T sea mı́nimo. Es bastante fácil darse cuenta que en ningún caso el grafo G0 contendrá ciclos. Si el grafo contuviera un ciclo, serı́a posible eliminar un arco del ciclo, consiguiendo un grafo G00 que seguirı́a conteniendo todos los vértices de V y que, conteniendo un arco menos y siendo los pesos positivos, tendrı́a un coste menor. Por ejemplo, en el grafo a la izquierda se puede eliminar un arco, consiguiendo otro grafo abarcador de coste menor: 1 ()*+ /.-, /.-,? ()*+ ?? ??1 1 ?? ? ()*+ /.-, coste = 3 1 /.-,? ()*+ ()*+ /.-, ?? ??1 1 ?? ? ()*+ /.-, coste = 2 Ya que G es no dirigido, G0 tampoco será dirigido, por tanto G0 es un árbol, llamado el árbol abarcador mı́nimo (o de coste mı́nimo) de G. Observamos que aquı́ "mı́nimo" se refiere al coste, y no al número de arcos, ya que cada árbol abarcador contiene |V | − 1 arcos (si contuviera menos, el sub-grafo G0 no podrı́a ser conexo). Formalmente, el problema se pone de la forma siguiente: Definition 5.20.1. Dado un grafo G = (V, E), ponderado, conexo, y no dirigido, con función peso w : E → R no negativa, un árbol abarcador mı́nimo de G es un sub-grafo acı́clico conexo G0 = (V, T ), T ⊆ E que minimiza X w(T ) = w(u, v) (5.140) (u,v)∈T Como ya hemos visto, el sub-grafo abarcador de coste mı́nimo es siempre acı́clico, o sea, es un árbol. La figura 5.20 es un ejemplo de grafo donde se ha marcado un árbol abarcador mı́nimo. Nótese que el árbol abarcador mı́nimo no es único: en el árbol de figura 5.20 se puede eliminar el arco (c, g) y a~ nadir el arco (f, g) consiguiendo un árbol abarcador con el mismo coste. El algoritmo que veremos en esta sección para la búsqueda del árbol abarcador mı́nimo pertenece a una clase de algoritmos conocidos como algoritmos avaros (greedy algorithms), de los cuales ya hemos visto un ejemplo. Estos algoritmos eligen, en cualquier momento de su ejecución, el elemento (el arco, en este caso) que da la mejor ventaja inmediada, sin tener en cuenta una visión global del algoritmo. Una tal estrategia, en general, no encuentra la solución óptima de un problema: a veces una solución que no parece tan buena en un momento resulta mejor a largo plazo. Pero, en el caso del árbol abarcador mı́nimo demostraremos que la estrategia avara consigue la solución óptima. grafos 105 a c b 9 1 ()*+F /.-, ()*+ /.-, ()*+ /.-, 66 FFFFFFF 4 66 FFFFF d 6 FFFF 8 6 F 66 2 8 4 3 ()*+ /.-, r 666 r 7 rr 6 2 r 6 rr ()*+ /.-, ()*+ /.-, ()*+r /.-, 4 e 4 f g Figure 5.23: Un ejemplo de árbol abarcador mı́nimo para un grafo conexo no dirigido. El árbol no es único: se puede quitar el arco (c, g) y remplazarlo con el arco (f, g) sin cambiar el coste. El algoritmo de Dijkstra para caminos mı́nimos, que hemos analizado en la sección 5.17, se basa en una estrategia similar: en cualquier momento elige el vértice de camino estimado mı́nimo entre los que todavı́a no se han elegido. El algoritmo de Dijkstra presenta muchas similitudes con el algoritmo de Primm, que será presentado en esta sección y, de hecho, determina un tipo de árbol abarcador mı́nimo: si el grafo G es dirigido, y se busca un árbol abarcador mı́nimo con raı́z en un vértice dado (el vértice de salida), el algoritmo de Dijkstra encuentra el árbol abarcador mı́nimo. 5.20.1 Acrecimiento de un árbol El algoritmo de esta sección funciona por acrecimiento: el árbol abarcador se construye un arco a la vez, insertando cada arco en un conjunto A que el algoritmo mantiene y seleccionando cada vez el arco más conveniente entre los que cumplen un determinado invariante, (una propiedad que se cumple a lo largo de la ejecución del algoritmo). En el caso presente, el invariante es: Dado el conjunto de arcos A (el conjunto de arcos seleccionados en un momento dado de la ejecución), existe un árbol abarcador mı́nimo T tal que A ⊆ T . En cada paso, el arco (u, v) que se a~ nade al conjunto A se elige entre los que mantienen el invariante, o sea, entre los arcos tales que A ∪ {(u, v)} es también un sub-conjunto de un árbol abarcador mı́nimo. Un arco que mantiene el invariante se llama seguro. Claramente, si seguimos a~ nadiendo arcos seguros hasta llegar a un conjunto de |V | − 1 arcos, obtendremos un árbol abarcador mı́nimo. El algoritmo general para árboles abarcadores mı́nimos es entonces el siguiente: AAM generico(G) 1. A ← ∅; 2. while |A| < |V| − 1 do 3. busca un arco (u, v) seguro para A; 4. A ← A ∪ {(u, v)}; 5. od 6. return (V, A); El problema es encontrar, en cada iteración del bucle de las lı́neas 2--5, un arco seguro para A. Un arco seguro existe siempre: antes de la ejecución de la lı́nea 3, el cahiers d’informatique 106 C C C1 2 a b c C C @ 6 C 2 3 4 1 4 5 @ C@ ? C @ ? R C d e f 3 C 2 CC Figure 5.24: Ejemplo de cruce invariante se cumple, es decir, existe un árbol abarcador T tal que A ⊆ T ; si |T | = |A|, entonces A es un árbol abarcador mı́nimo, si no existe por lo menos un arco en T − A, y este arco es seguro para A. El problema entonces es: ¿cómo encontrar arcos seguros? necesitamos empezar con unas cuantas definiciones. Antes de resolver este problema Dado un grafo G = (V, E), un corte de G es una partición de V en dos conjuntos (S, V − S). Un arco (u, v) cruza el corte si uno de sus vértices se encuentra en S y el otro en V − S. Dado un conjunto de arcos A, un corte (S, V − S) respeta A si ningún arco de A cruza el corte. Un arco ligero de un corte es un arco de coste mı́nimo entre los que cruzan el corte. En general, decimos que (u, v) es un arco ligero para una dada propiedad si es un arco de peso mı́nimo entre los que cumplen la propiedad. Un arco ligero, generalmente, no es único, ya que pueden existir varios arcos con el mismo peso. Por ejemplo, la lı́nea espesa de figura 5.20.1 representa un cruce con S = {a, d, f } V − S = {b, c, e} (y también representa el cruce complementar con S = {b, c, e} y V − S = {a, d, f }: el dibujo es ambuguo). El cruce respeta conjuntos de arcos como A = {(b, e), (e, c)} A = {(a, d)} A = {} También respeta el conjunto A = {(a, d), (b, e), (b, c), (e, c)} que es el conjunto máximo respetado por el corte: si el corte respeta B, entonces B ⊆ A. (5.141) para cada otro conjunto de arcos B, La clave para encontrar arcos seguros la proporciona el teorema siguiente. Theorem 5.20.1. Sea G = (V, E) un grafo dirigido conexo con función peso w : E → R y A un sub-conjunto de E, tal que existe un árbol abarcador mı́nimo (V, T ) tal que A ⊂ T . Sea (S, V − S) un corte que respeta A, y (u, v) un arco ligero que cruza (S, V − S). Entonces el arco (u, v) es seguro para A. Demostración: Si (u, v) ∈ T , entonces claramente (u, v) es seguro para A. Supongamos que (u, v) 6∈ T y vamos a construir un árbol abarcador mı́nimo T 0 tal que A ∪ {(u, v)} ⊆ T 0 . Si dicho árbol existe entonces (u, v) es claramente seguro para A. Siendo T un árbol abarcador, y (u, v) 6∈ T , hay en T un camino de u a v que no incluye (u, v). Entonces, como se ve en figura 5.20.1, el arco (u, v) forma con el camino p un ciclo. Los vértices u y v están en lados opuestos del corte, por tanto hay en p por lo menos un arco (x, y) que cruza grafos 107 S V-S m xm ym m m m p m ? um vm Figure 5.25: Construcción del árbol T 0 . Los arcos que se muestran son los del árbol T , y los arcos marcados doble pertenecen a A (claramente, aquı́ no se muestran todos los arcos del grafo). El arco (u, v) es un arco ligero que cruza el corte, y el arco (x, y) es un arco en el camino (único) entre u y v en T . Un árbol abarcador mı́nimo que contenga (u, v) se construye sacando (x, y) de T y a~ nadiendo (u, v). el corte, y (x, y) 6∈ A, ya que, por hipótesis, el corte respeta A. El arco (x, y) se encuentra en el único camino en T entre u y v, entonces, sacando (x, y) divide T en dos partes, y a~ nadiendo el arco (u, v) lo recompone en un nuevo árbol abarcador T 0 = T − {(x, y)} ∪ {(u, v)}. T 0 es entonces un árbol abarcador, y A ∪ {(u, v)} ⊆ T 0 , ya que A ⊆ T y (x, y) 6∈ A. Hay que demostrar que T 0 es mı́nimo. Como (u, v) es un arco ligero que cruza el corte, y (x, y) también cruza el corte, se cumple w(u, v) ≤ w(x, y), entonces w(T 0 ) = w(T ) − w(x, y) + w(u, v) ≤ w(T ). (5.142) Pero T era un árbol abarcador mı́nimo, ası́ que w(T ) ≤ w(T 0 ); por tanto w(T ) = w(T 0 ), y T 0 es también un árbol abarcador mı́nimo. 5.21 El algoritmo de Primm El algoritmo de Primm se basa en el algoritmo genérico de acrecimiento del árbol abarcador de un grafo G y construye un árbol abarcador mı́nimo dada una raı́z arbitraria r ∈ V [G] un arco a la vez. En cada paso el algoritmo mantiene un conjunto S de vértices ya a~ nadidos y encuentra un arco ligero que cruza el corte (S, V − S). Por construcción el corte respeta el conjunto de vértices ya encontrados y, por el teorema precedente, si (u, v) es el arco escogido y v 6∈ S, entonces S ∪ {v} es un sub-árbol de un árbol abarcador mı́nimo. Si seguimos a~ nadiendo arcos hasta que sea S = V [G] conseguimos el árbol que buscamos. La figura 5.21 ilustra el funcionamiento del algoritmo. El algoritmo de Primm sigue una estrategia avara; en cada iteración el árbol se aumenta usando el arco de coste mı́nimo disponible en momento, sin ocuparnos del coste de los demás arcos. La clave para la implementación eficaz del algoritmo es una búsqueda rápida del arco de coste mı́nimo entre los que cruzan el corte. El algoritmo guarda los vértices que todavı́a no se han a~ nadido al árbol en una cola de prioridad Q ordenada por un valor key[v]. Para cada v ∈ V [G], key[v] es el peso mı́nimo de cualquier arco que une v al árbol cahiers d’informatique 108 1 ()*+J /.-, /.-,; 9 ?>=< ()*+ 89:; ()*+ /.-, r ;; JJJ4 JJ ;; J ;; 4 ()*+ /.-, 3 2 r 8 2 ;;8 r r ;; r rr ()*+ /.-, ()*+r 7 /.-, ()*+ /.-, 1 /.-,; 9 89:; ()*+ ()*+ /.-, J ?>=< ()*+ /.-, r ;; JJJ4 JJ ;; J ;; 4 ()*+ /.-, 3 2 r 8 2 ;;8 r r ;; r rr ()*+ /.-, ()*+r 7 /.-, ()*+ /.-, 1 /.-,; 9 89:; ()*+ ()*+ /.-, J ?>=< ()*+ /.-, r ;; JJJ4 JJ ;; J ;; 4 ()*+ /.-, 3 2 r 8 2 ;;8 r r ;; r rr ()*+ /.-, ()*+r 7 /.-, ()*+ /.-, 1 ()*+ /.-, J /.-,; 9 ?>=< ()*+ 89:; ()*+ /.-, r JJ 4 ;; JJ ;; JJ ; ()*+ /.-, ; 4 3 2 8 2 ;;8 rr r ;; r rr ()*+ /.-, ()*+ /.-, ()*+ /.-, r 7 1 ()*+ /.-, J /.-,; 9 89:; ()*+ ?>=< ()*+ /.-, r JJ 4 ;; JJ ;; JJ ; ()*+ /.-, ; 4 3 2 8 2 ;;8 rr r ;; r rr ()*+ /.-, ()*+ /.-, ()*+ /.-, r 7 1 ()*+ /.-, J /.-, ()*+ ; 9 89:; ?>=< ()*+ /.-, r JJ 4 ;; JJ ;; JJ ; ()*+ /.-, ; 4 3 2 8 2 ;;8 rr r ;; r rr ()*+ /.-, ()*+ /.-, ()*+ /.-, r 7 4 4 (1) 4 4 (4) 4 4 (2) 4 4 1 /.-, ()*+ ; 9 ?>=< 89:; ()*+ /.-, ()*+ /.-, JJ r JJJJJ4 ;; JJJJ ;; JJJ ;; ()*+ /.-, 3 4 2 8 2 ;;8 rr r ;; r rr ()*+ /.-, ()*+ /.-, ()*+ /.-, r 7 (5) 4 4 4 (3) 4 4 (6) 4 (7) Figure 5.26: Ejemplo de aplicación del algoritmo de Primm. Notese que el arco (c, d) era una opción posible en el paso 4 (una opción que, en este ejemplo, no se elige), pero no en el paso 5, ya que a este punto el arco mı́nimo que cruza el corte es el arco (e, a), the coste 3. grafos 109 que se está construyendo, o key[v] = ∞ si no hay arco. de v en el árbol. El campo π[v] contiene el "padre" El conjunto A que aparece en el algoritmo genérico---el conjunto de arcos que ya se han a~ nadido al árbol---no se almacena de forma explı́cita en el algoritmo. El conjunto es A = {(v, π[v])|v ∈ V − {r} − Q} (5.143) y, por una ejecución dada del algoritmo con una raı́z dada, depende, en cualquier momento de la ejecución, sólo del contenido de la cola Q y de los punteros π[v] que ya se han asignado ası́ que almacenando estos elementos también se almacena (implı́citamente) A. Al final del algoritmo la cola Q se encuentra vacı́a, ası́ que el árbol abarcador mı́nimo queda definido por los punteros π[v] como T = {(v, π[v])|v ∈ V − {r}} (5.144) El algoritmo es el siguiente: Primm(G, w, r) 1. Q ← V[G]; 2. foreach u in Q do 3. key[u] ← ∞; 4. od; 5. key[r] ← 0; 6. π[r] ← Nil; 7. while Q 6= ∅ do 8. u ← extract min(Q); 9. foreach v in u.ady do 10. if v in Q and w(u, v) < key[v] then 11. π[v] ← u; 12. key[v] ← w(u, v); 13. fi 14. od 15. od Para determinar el tiempo de ejecución del algoritmo, condideremos que cola Q se implemente con un Heap. En este caso, la inicialización de la cola supone un build heap, con un tiempo O(V ). La cola se inicializa con todos los vértices, y en cada iteración del bucle 7--15 se saca un vértice, ası́ que el bucle se ejecutará O(V ) veces. La función extract heap tiene un tiempo de ejecución O(log V ), por tanto el tiempo de ejecución del bucle 7--15, excepto la ejecución del bucle 9--14, es O(V log V ). Como ya hemos visto en otras ocasiones, el bucle 9--14 se ejecuta un total de O(E) veces (la suma de los tama~ nos de las listas de adyacencias de todos los vértices). Averiguar si v ∈ Q se puede hacer en un tiempo O(1), guardando un flag asociado a v que se inicializa a true y se pone false en el momento en que v se saca de la cola. La operación al paso 12 supone la ejecución de un decrease key en el heap, ya que ahora la posición de v puede haber cambiado. Esta operación se puede copletar en un tiempo O(log V ) y por tanto el tiempo total de ejecución del bucle 9--14 es O(E log V ). Entonces, el tiempo total de ejecución del algoritmo es O((V + E) log V ). El tiempo se puede mejorar implementando la cola con una estructura de heap de Fibonacci. En este caso el tiempo de ejecución medio de decrease key es O(1), y el tiempo de ejecución del algoritmo es O(E + V log V ). cahiers d’informatique 110 5.22 Ejercicios 1. Ejecuten el algoritmo de Prim en los grafos siguientes ()*+ ()*+? 5 /.-, /.-, ??? 8 ?? 3 4 4 4 ? ??7? ()*+ 1 /.-, /.-, ()*+ 2 /.-, ()*+ ()*+ /.-, 1 ()*+ /.-, ()*+ /.-, 3 1 3 ()*+ /.-, ()*+ /.-, 3 5 ()*+ /.-, ()*+? /.-, ??3? ()*+ 1 1 /.-, ()*+? 4 /.-, 9 ? ?? ()*+ 5 13 /.-, /.-, ()*+ 1 ()*+ /.-, ()*+ /.-, 7 ()*+ /.-, ()*+OO /.-, ()*+ /.-, OOO OOO 5 O 3 2 3 7 OOOOO OO4OO ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, ()*+ /.-, 1 3 4 ()*+ /.-, ()*+ /.-, /.-,? ()*+ ?? ???4 ? ?? 1 1 4 ??2 ? ?? ()*+ /.-, ()*+ 2 /.-, /.-, ()*+ 3 3 /.-,? ()*+ ()*+? /.-, ()*+ /.-, ??2 ??1 ? ? ()*+? ()*+ 3 /.-, /.-, 1 2 ?? ???1 ? ? 2 ()*+ 3 /.-, ()*+ /.-, ()*+ /.-, 1 4 ()*+ /.-, 2 ()*+ /.-, ()*+ /.-, ()*+ /.-, yy y y ()*+yE 4 /.-, 9 EE13 y y EE yy5 y ()*+ /.-, ()*+ /.-, ()*+ /.-, 7 ()*+ /.-, /.-,O 3 ()*+ ()*+ /.-, OOO 5 ??5 o3oo OOO 6 ?o?oo ()*+ ()*+? OoOooOoOoO /.-, /.-, 2 OOO???4 2 4 oo?o? oo ? OOO? ()*+oo /.-, ()*+ 7 ()*+ /.-, 7 /.-, 1 1 2. Escriban una versión del algoritmo de Prim que funciona con grafo representados por matriz de adyacencia. 3. grafos 111 VI. APENDIX. EL PRINCIPIO DE INDUCCIÓN. El principio de inducción matemática es una propiedad del campo de los números naturales que nos proporciona una técnica muy útil para demostrar teoremas. El principio fue enunciado por primera vez en su totalidad por el matemático Giuseppe Peano como parte de su sistema de axiomatización de las matemáticas. La importancia del principio reside en ser a la base del procedimiento de demostración por inducción, que se usa mucho en varios campos de las matemáticas y de la informática cada vez que es posible expresar una familia de problemas de una manera que dependa de un parámetro entero. 6.23 Propiedades hereditarias El principio establece las condiciones bajo las cuales una cierta propiedad es válida en el campo de los números naturales. Una propiedad, en general, se expresa como un predicado lógico P (n). Por ejemplo: i) P1 (n) = n es par; ii) P2 (n) = n es mayor que 5; iii) P3 (n) = n es un número primo; iv) P4 (n) = n es un múltiplo de dos o el sucesor de un múltiplo de dos; y v) P5 (n) = n es un múltiplo de tres, el sucesor de un múltiplo de tres, o el sucesor del sucesor de un múltiplo de tres. Estas propiedades son proposiciones lógicas y, como tales, se pueden expresar usando el lenguaje de la lógica simbólica: i) P1 (n) = ∃m ∈ N : n = 2m; ii) P2 (n) = n > 5; cahiers d’informatique 112 iii) P3 (n) = ∀m, p ∈ N : n = mp ⇒ (m = 1 ∨ m = n); iv) P4 (n) = ∃m ∈ N : n = 2m ∨ n = 2m + 1; v) P5 (n) = ∃m ∈ N : n = 3m ∨ n = 3m + 1 ∨ n = 3m + 2; Algunas de estas proposiciones gozan de la propiedad siguiente: Definition 6.23.1. Sea P (n) una proposición que expresa una propiedad de los números naturales. La proposición P es hereditaria si, cada vez que un numero n goza de P , el numero n + 1 también goza de P . En fomulas: ∀n P (n) ⇒ P (n + 1) (6.145) De las propiedades P1 , . . . , P5 es fácil de mostrar que las P2 , P4 , P5 son hereditarias, mientras que las P1 y la P3 no lo son: i) Supongamos P1 (n), o sea que hay un m de tal modo que n = 2m. Tomamos ahora el número n + 1. Para este número vale n + 1 = 2m + 1 y, claramente, no existe ningún m0 para el que 2m + 1 = 2m0 . (Esto corresponde a la propiedad en la que si n es par n + 1 es impar.) ii) Supongamos n > 5, entonces n + 1 > n > 5 ası́ que la propiedad se cumple para n + 1 y por lo tanto es hereditaria. iii) En este caso utilizaremos un contra-ejemplo. Supongamos n = 7; sabemos que 7 es un número primo, entonces P3 (7) es cierto. Por supuesto, n + 1 = 7 + 1 = 8 y 8 es un múltiplo de 2 y como tal no es primo. Existe entonces un caso en que P3 (n) es cierto, pero P (n + 1) no lo es. Ya que la definción de propiedad hereditaria requiere que (6.145) se cumpla para cada n, se concluye que P3 no es hereditaria. iv) Supongamos P4 (n), o sea, que existe un m tal que n = 2m o n = 2m + 1. Aquı́ hay que analizar dos casos. Si n = 2m, entonces n + 1 = 2m + 1, y P4 (n + 1) es cierto. Si n = 2m + 1, entonces n + 1 = 2m + 2 = 2(m + 1) = 2m0 , donde m0 = 2m + 1. La propiedad P4 (n + 1) se cumple en este caso también, por lo cual se concluye que P4 es hereditaria. v) P5 también es hereditaria, como se puede mostrar de manera análoga al caso de P4 . Una vez definida esta propiedad, la formulación del principio de inducción matemática (también llamado principio de inducción completa ) es muy simple: Principio de inducción matemática matemática: Supongamos P una propiedad de los números naturales. hereditaria, entonces cada entero goza de P . Si 0 goza de P y P es En fórmulas: (P (0) ∧ (∀m ∈ N : P (m) ⇒ P (n + 1))) ⇒ ∀n ∈ N : P (n) (6.146) grafos 6.24 113 Aplicación a teoremas matemáticos El principio de inducción matemática es un instrumento matemático muy útil porque permite evitar una demostración completa de que una propiedad es cierta para cada entero: será suficiente demostrar que la propiedad vale para 0 y que es hereditaria. Para demostrar que la propiedad es hereditaria se asume que n la cumple y, basándose en esto, se deriva que n + 1 también la cumple. Pongamos, por ejemplo, que se quiera demostrar el teorema siguiente: Theorem 6.24.1. Cada entero es par o impar; o sea, para cada entero n existe un entero m tal que n se puede expresar como n = 2m (en tal caso n es par) o como n = 2m + 1 (en tal caso n es impar). Demostrar este teorema con el principio de inducción matemática es muy fácil: que demostrar los dos hechos siguientes: sólo habrá i) 0 cumple con la propiedad. Ésto es muy fácil de demostrar, en cuanto es posible escribir 0 como 0 = 2 · 0 (lo cual nos dice que 0 es par). ii) La propiedad es hereditaria. Ésto ya se ha demostrado en el punto iv) arriba. ¿Qué pasa si P (0) no se cumple? Hay propiedades, como la P2 de arriba, que son hereditarias, pero que empiezan a ser cumplidas a partir de un número que no es 0: en el caso de la P2 , por ejemplo, la propiedad se cumple a partir de n = 6. En esto caso también se puede aplicar el principio de inducción de la siguiente forma modificada: Principio de inducción matemática (modificado) (modificado):9 Sea P una propiedad de los números naturales. Si un número k ∈ N goza de P y P es hereditaria, entonces cada entero mayor o igual a k goza de P . En formulas: (P (k) ∧ (∀m ∈ N : P (m) ⇒ P (n + 1))) ⇒ ∀n ≥ k : P (n) (6.147) Gracias a este principio, es muy fácil demostrar que P2 se cumple para cada número n ≥ 6. Es importante notar que el principio de inducción matemática nos permite afirmar que la propiedad P es válida para cada número finito. Esta observación es particularmente importante en el caso de las aplicaciones informáticas, en las cuales el principio no se aplica directamente a los números, sino a alguna estructura parametrizada por números. Consideremos, por ejemplo, el teorema siguiente: Theorem 6.24.2. Cada conjunto finito de números posee un máximo. Demostración. Este teorema se puede demostrar de varias maneras pero, en este caso, se usará el principio de inducción. La inducción se hará sobre el número de elementos del conjunto. Es decir, la propiedad que se considererá es la siguiente: P (n) = cada conjunto de n números posee un máximo. En el caso n = 1, la propiedad es inmediata: es el máximo. el único número que aparece en el conjunto 9 En realidad esta formulación del principio es un teorema, en cuanto se puede derivar del principio (6.146). Aquı́, sin embargo, se seguirá el uso común, y se llamará principio. cahiers d’informatique 114 Sea ahora P (n) válida; queremos demostrar P (n + 1). Un conjunto de n + 1 números se puede escribir como {u1 , . . . , un+1 }. Quitamos el número un+1 del conjunto. Nos queda el conjunto {u1 , . . . , un } que, estando compuesto de n elementos, cumple la propiedad y posee un máximo, siendo µ(n) este máximo. Ahora es posible definir un+1 si un+1 > µ(n) µ(n + 1) = (6.148) µ(n) si no Claramente µ(n + 1) es el máximo del conjunto {u1 , . . . , un+1 }, y el teorema queda demostrado. Como ya se ha mencionado, el teorema permite demostrar, en este caso, que cada conjunto finito posee un máximo. En el caso de conjuntos infinitos la propiedad no vale: por ejemplo, el conjunto N de todos los enteros no posee máximo. En informática, el teorema se aplica mucho para demostrar la correción de algoritmos. En muchos casos el número n sobre el que se hace inducción es el tama~ no de la estructura a la cual se aplica el algoritmo: se demuestra que el algoritmo es correcto si se aplica a una estructura de tama~ no 1 (lo que, en muchos casos, es muy fácil de demostrar) y luego se demuestra que, si el algoritmo funciona correctamente con todas las estructura de tama~ no n, entonces funciona también con las de tama~ no n + 1. 6.25 Otras variantes A veces la aplicación del principio de inducción, ası́ como se ha enunciado en la sección precedente es imposible porque no se puede pasar del caso n al caso n + 1, aún si se ha demostrado para n. Consideremos, por ejemplo, el algoritmo merge sort : merge-sort(A, p, r) if p < r then q ← b(p + r)/2c; merge-sort(A, p, q); merge-sort(A, q + 1, r); merge(A, p, q, r); fi Se desea determinar el tiempo asintótico de ejecución del algoritmo. Sea T (n) el tiempo de ejecución de merge sort con un array de tama~ no n. Se sabe que el tiempo de ejecución de la función merge es n, pero el tiempo de ejecución total depende también del tiempo de ejecución de merge sort mismo, que se llama recursivamente dos veces con array de tama~ no n/2. Se puede entonces escribir: T (n) = 2T (n/2) + n (6.149) Se quiere demonstrar que, dada esta ecuación, el tiempo de ejecución es T (n) = O(n log n) o sea, que existe un valor c tal que, para cada n, T (n) ≤ cn log n, con una demostración que utilice el método de inducción. Para n = 1 la propiedad no vale (c · 1 · log 1 = 0 para cada c), pero esto no es tan importante: siendo una propiedad asintótica, nos interesan valores grandes de n. Empezamos con n = 2, para el cual se necesita T (2) < c · 2 · log 2. Es posible escoger c de tamańo tal para que la propiedad se cumpla. Supongamos entonces que la propiedad se cumple para n − 1, y demostramos que T (n) = 2T (n/2) + n ≤ cn log n (6.150) grafos 115 Aqui hay un problema: aparece n/2 hemos supuesto que la propiedad vale para n − 1 pero en la ecuación La intuición nos dice que la cosa deberı́a funcionar también: si se asume que la propiedad se cumple para n − 1, quiere decir que, de alguna manera, se asume demonstrada para todos los n más peque~ nos. De hecho, esta constatación se encuentra en una nueva formulación del principio de inducción10 : Definition 6.25.1. Una propiedad P es hereditaria en sentido general si el hecho de que P valga para todos los números 1, . . . , n − 1 implica que P vale para n. Es decir: ∀n : P (0) ∧ P (1) ∧ · · · ∧ P (n − 1) ⇒ P (n) (6.151) Se pueden hacer aquı́ las mismas constataciones de antes si la propiedad empieza a verificarse para un número que no es cero. Esta formulación del principio resuelve el problema del tiempo de ejecución de merge-sort. Supongamos que T (k) ≤ ck log k se cumple para k = 2, . . . , n − 1. Ya que n/2 ≤ n − 1, la propiedad se cumple para n/2, ası́ que (6.150) se puede escribir como T (n) 6.26 = ≤ = ≤ ≤ 2T (n/2) + n ≤ cn log n 2c n2 log n2 + n cn log n − cn log 2 + n cn log n − cn + n cn log n (definición de T (n)) (hipótesis inductiva) (propiedad del logaritmo) (porque log 2 ≤ 1) (se asume c > 1) (6.152) Peligros Algo que siempre es necesario recordar cuando se haga la demostración de que una propiedad es hereditaria: en la demostracción que, si P (n) se cumple, entonces P (n + 1) también se cumple, el número n tiene que ser absolutamente cualquiera. Es decir, no se puede usar ninguna propiedad que algunos n pueden cumplir y otros no. Un ejemplo de los riesgos a que uno se expone sin la debida atención se da en el teorema de los espa~ noles (Santini, 2006): Theorem 6.26.1. En un conjunto de personas, si una es espa~ nola, entonces todas son espa~ nolas. Demostración. El teorema se demuestra con el método de inducción. Para n = 1 el teorema es cierto: en un grupo de una persona, si esta persona es espa~ nola, todas lo son. Se suponga, entonces, que el teorema sea verificado para conjuntos de n personas, y vamos a demostrarlo para conjuntos de n + 1 personas. Consideramos el conjunto {p1 , p2 , . . . , pn+1 }. Para demostrar el teorema, es suficiente suponer que una persona es espa~ nola y derivar, de este hecho, que todas lo son. Supongamos, entonces, que en este conjunto haya un espa~ nol y, sin pérdida de generalidad, tomamos un espa~ nol que no sea p1 . Ahora quitamos a p1 del grupo, obteniendo el conjunto {p2 , . . . , pn+1 }. Este conjunto contiene n personas, entonces, para este conjunto, el teorema es cierto: como una persona es espa~ nola (y se hizo la hipótesis de que uno de los espa~ noles no era p1 ), todas lo son. Ahora, quitamos a pn+1 del grupo y ponemos otra vez a p1 , obteniendo 10 Esta forma del principio también es en realidad un teorema que se puede derivar de la forma original 116 cahiers d’informatique {p1 , p2 , . . . , pn }. Este grupo también contiene n personas y ya hemos demostrado que {p2 , . . . , pn } son espa~ nolas, entonces hay en el grupo {p1 , p2 , . . . , pn } por lo menos un espa~ ol y, siendo el grupo de n personas, se aplica la hipótesis de inducción, y se concluye que todos son espa~ noles. Se ha acabado de demostrar que, asumiendo que una persona del grupo es espa~ nola, todas las {p1 , p2 , . . . , pn+1 ) lo son. El teorema es válido. ¿Dónde está el problema? Supongamos que n + 1 = 2. Se asume que una persona en el grupo {p1 , p2 } es espa~ nola. Quitamos p1 del grupo, y mos quedamos con {p2 }. Si p2 es nuestro espa~ nol, entonces todos en el grupo lo son, porque el teorema es válido para n = 1. Ahora ponemos p1 en el grupo, y sacamos p2 . El grupo ahora es {p1 }, y... ¡la persona que se habı́a supuesto espa~ ola ha desaparecido del grupo ! Es decir, cuando se supone que haya un espa~ nol en el grupo, es necesario que esta persona se quede en el grupo ya sea cuando se quita a p1 o cuando se quita a pn+1 . Es decir, es necesario que el grupo {p2 , . . . , pn } que aparece en el teorema no esté vacı́o. Pero esto solo se puede dar si n + 1 > 2: sin pensarlo hemos hecho la hipótesis de que n > 1. Nuestra inducción no puede demostrar que el teorema es valido para n = 2 y, no pudiendo pasar el "obstaculo 2", la inducción se rompe. Solo hemos demostrado el teorema para n = 1, o sea, hemos demostrado que si en un grupo de una persona esta persona es espa~ nola, todos lo son; una propiedad que, aún si se ha demostrado con la certitud absoluta de las matemáticas, carece bastante de interés.