grafos en codigos completo

89
GRAFOS 1. Definiciones básicas: Un grafo es la representación por medio de conjuntos de relaciones arbitrarias entre objetos. Existen dos tipos de grafos según la relación entre los objetos sea unívoca o biunívoca. Los primeros forman los grafos dirigidos o dígrafos y los segundos los grafos no dirigidos o simplemente grafos. En la mayor parte de los algoritmos que serán nuestro objeto de estudio se hace referencia a la termología básica que se propone a continuación. Dicha terminología; por desgracia, no es estándar y puede llegar a variar en los distintos textos que existen en la materia. Cuando exista ambigüedad se harán las aclaraciones según sea necesario. Un grafo dirigido o dígrafo consiste de un conjunto de vértices V y un conjunto de arcos A. Los vértices se denominan nodos o puntos; los arcos también se conocen como aristas o líneas dirigidas que representan que entre un par de vértices existe una relación unívoca aRb pero no bRa. De modo que los arcos se representan comúnmente por medio de pares

Upload: bernardo-javier-blanco-segovia

Post on 01-Jul-2015

4.950 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: grafos en codigos completo

GRAFOS

1. Definiciones básicas:

Un grafo es la representación por medio de conjuntos de relaciones arbitrarias entre objetos. Existen dos tipos de grafos según la relación entre los objetos sea unívoca o biunívoca. Los primeros forman los grafos dirigidos o dígrafos y los segundos los grafos no dirigidos o simplemente grafos. En la mayor parte de los algoritmos que serán nuestro objeto de estudio se hace referencia a la termología básica que se propone a continuación. Dicha terminología; por desgracia, no es estándar y puede llegar a variar en los distintos textos que existen en la materia. Cuando exista ambigüedad se harán las aclaraciones según sea necesario.

Un grafo dirigido o dígrafo consiste de un conjunto de vértices V y un conjunto de arcos A. Los vértices se denominan nodos o puntos; los arcos también se conocen como aristas o líneas dirigidas que representan que entre un par de vértices existe una relación unívoca aRb pero no bRa. De modo que los arcos se representan comúnmente por medio de pares ordenados (a,b), donde se dice que a es la cabeza y b la cola del arco y a menudo se representa también por medio de una flecha, tal como se muestra en la figura 1.

Figura 1 Grafo dirigido donde , y tal que

. En dicho grafo se entiende que y en muchos casos solo existe uno de los pares de vértices.

a b

Page 2: grafos en codigos completo

Un vértice que solo tiene arcos saliendo de él se denomina fuente y un vértice que solo tiene arcos dirigidos hacia él se denomina sumidero. Dicha nomenclatura es importante cuando los dígrafos se usan para resolver problemas de flujos.

Un grafo no dirigido, o grafo, al igual que un dígrafo consiste de un conjunto de vértices V y un conjunto de arcos A. La diferencia consiste en que la existencia de aRb presupone que bRa también existe y además que son iguales. De este modo es indistinto hablar del arco (a,b) o (b,a), tampoco tiene sentido hablar de la cabeza o la cola del arco. Los grafos representan como lo indica la figura 2, donde los círculos representan los vértices y las líneas representan los arcos.

Figura 2 Grafo no dirigido

donde , y tal que . En dicho grafo se entiende que y además

, donde ambos pares de vértices representan el mismo arco.

Existen además grafos en donde los arcos tienen asociado algún valor en cuyo caso hablamos de grafos ponderados y ahora se representan los arcos como tripletas. Sigue existiendo la información de los vértices unidos por dicho arco además de la información del peso de dicho arco. Así pues el arco se representa como donde son el origen y destino y es el peso respectivamente.

Un nodo b se dice que es adyacente al nodo a si existe el arco (a, b), tómese en cuenta que para un grafo no dirigido necesariamente a es también adyacente a b. Esto no ocurre en los

a b

Page 3: grafos en codigos completo

grafos dirigidos donde la existencia de (a, b) no implica que (b, a) también existe. Este concepto es de particular importancia dado que los grafos suelen representarse en la computadora por medio de listas o matrices de adyacencias.

Un arco (a,b) incide en el nodo b, de igual modo en grafo no dirigido dicho arco también incide en el nodo a debido a que también existe (b, a). El número de arcos que inciden en un nodo le otorga el grado a dicho nodo. El nodo con mayor grado en el grafo le indica el grado de dicho grafo. También se acostumbra representar a un grafo por medio de listas o matrices de incidencias.

Existen otras definiciones que son útiles para explicar el funcionamiento de un algoritmo en particular, se definirán los conceptos en su momento.

Page 4: grafos en codigos completo

2. Métodos de representación en computadora

Tal como se adelanto en el apartado anterior, existen varias formas de representar un grafo en la computadora y cada una tiene sus ventajas y desventajas. Mostraremos las más comunes y la forma de implementarlas.

La primera forma es por medio de una matriz de adyacencias, con este método se tiene una matriz de tamaño nxn, donde n es el numero de vértices o nodos en el grafo. Una forma simple de ver la información guardada en dicha matriz es que los renglones de las mismas representan el origen y las columnas el destino de cada arista o arco en el grafo. Si el grafo es no ponderado se acostumbra poner un cero en el (renglón i, columna j) de la matriz cuando no existe dicho arco y un uno cuando dicho arco existe en el grafo. En el caso de grafos ponderados, se acostumbra poner una bandera (normalmente el valor de infinito) en las posiciones donde no existe un arco y el peso correspondiente en las posiciones donde si existe.

1 2 3 4 51 0 1 0 0 12 1 0 1 1 13 0 1 0 1 04 0 1 1 0 15 1 1 0 1 0

Page 5: grafos en codigos completo

Figura 3 Grafo no ponderado y su matriz de adyacencia

Debe notarse que para un grafo no dirigido la matriz de adyacencia es simétrica y que la diagonal principal contiene ceros. Esto puede llegar a aprovecharse para ahorrar tiempo en algunos algoritmos. La representación por medio de matriz se prefiere para algoritmos donde el numero de arcos es grande en proporción al numero de vértices. Si sucediera lo contrario se prefiere la representación por medio de listas de adyacencia.

Figura 4 Lista de adyacencia para el grafo de la figura 3

Las estructuras de datos para las dos formas de representación anteriores pueden modelarse en C como sigue:

char grafo[MAX_VERT][MAX_VERT], visitado[MAX_VERT];

1 2

5 4

3

2 5 /

1 5 3 4 /

2 4 /

3 /2 5

4 1 2 /

Page 6: grafos en codigos completo

void inserta(char i, char j){grafo[i][j] = grafo[j][i] = 1;

}

void limpia_grafo(){int i, j;

for(i = 0; i < nvert; i++){visitado[i] = 0;for( j = i; j < nvert; j++)

grafo[i][j] = grafo[j][i] = 0;}

}

Listado 1 Representación por matriz de adyacencia

Para encontrar los adyacentes al vértice i se tendría que construir un ciclo que evaluara en el renglón i aquellas columnas que tienen un uno. Como en el siguiente fragmento de código, donde se quieren meter los adyacentes no visitados a una pila.

for(i = 0; i < nvert; i++){if(!visitado[i] && grafo[j][i]){

pila.push(i);visitado[i] = 1;

}}

Listado 2 Encontrar adyacentes al vértice jEn las implementaciones de algoritmos se darán más detalles

acerca del manejo de las estructuras de datos. Por ahora revisemos la versión por medio de listas de adyacencia.

#include <vector>#include <list>

vector< list<int> > grafo(MAX_VERT);char visitado[MAX_VERT];

void inserta_arista(int i, int j){grafo[i].push_back(j);grafo[j].push_back(i);

}

void limpia_grafo(){int i;

for(i = 0; i < nvert; i++){grafo[i].clear();visitado[i] = 0;

}

Page 7: grafos en codigos completo

}

list<int>::iterator aux, fin;

aux = grafo[j].begin();fin = grafo[j].end();

while(aux != fin){if(!visitado[*aux]){

pila.push(*aux);visitado[*aux] = 1;

}aux++;

}

Listado 3 Versión por listas de adyacencias

En ambos casos se ha supuesto un grafo no dirigido y no ponderado. En el caso de un grafo dirigido basta con eliminar la doble inserción y no considerar la existencia de (j, i) para cada (i, j). La implementación para grafos ponderados por medio de matrices se presenta a continuación:

#define INFINITO MAXINT

char grafo[MAX_VERT][MAX_VERT], visitado[MAX_VERT];

void inserta_arista_ponderada(int i, int j, int w){grafo[i][j] = w;

}

void limpia_grafo(){int i, j;

for(i = 0; i < nvert; i++){visitado[i] = 0;grafo[i][i] = 0;for( j = i+1; j < nvert; j++)

grafo[i][j] = grafo[j][i] = INFINITO;}

}

int suma_pesos(int x, int y){if( x == INFINITO || y == INFINITO) return INFINITO;else return x + y;

}

Listado 4 Grafos ponderados por medio de matrices

Page 8: grafos en codigos completo

Adicionalmente se muestra una función para sumar pesos que permite solucionar el problema de sumar aristas con valor de infinito. Lo cual es muy común en algoritmos con grafos ponderados.

Ahora podemos revisar la versión con listas de adyacencias. Podemos notar que es necesario utilizar un par que guarde el nodo destino además del peso. Aquí se define el primer miembro como el destino y el segundo como el peso.

#include <vector>#include <list>

vector< list< pair<int , int> > > grafo(MAX_VERT);char visitado[MAX_VERT];

void inserta_arista_ponderada(int i, int j){pair ady;

ady.first = j;ady.second = wgrafo[i].push_back(ady);

}

Listado 5 Grafos ponderados con listas de adyacenciaEn muchos casos es necesario ordenar las aristas de un grafo

ponderado de acuerdo a su peso. Ante tal situación es apropiado definir una estructura que contenga la información de las aristas y luego insertarlas en una cola de prioridad. En otras ocasiones, se desea formar un subconjunto de aristas que cumplen con una cierta propiedad como cuando se obtienen los árboles de expansión de los recorridos de un grafo o se encuentran los árboles de expansión mínima.

typedef pair< int , int > ARISTApriority_queue< int, ARISTA> cola;

Listado 6 Definición de tipos para grafos ponderados

Muchas veces conviene multiplicar el peso por -1 para convertir la pila de prioridad descendente de la STL en una cola de prioridad ascendente que se necesita para algoritmos como dijkstra, prim o kruskal. Existen otras estructuras de datos que son útiles para

Page 9: grafos en codigos completo

construir algoritmos sobre grafos, entre ellas están las de conjuntos disjuntos que se discutirán más adelante.

Page 10: grafos en codigos completo

3. Algoritmos básicos de búsqueda

Existen dos técnicas básicas para recorrer los vértices de un grafo, la búsqueda por profundidad (DFS) y la búsqueda por anchura (BFS). La búsqueda por profundidad se usa cuando queremos probar si una solución entre varias posibles cumple con ciertos requisitos como sucede en el problema del camino que debe recorrer un caballo para pasar por las 64 casillas del tablero. La búsqueda por anchura se usa para aquellos algoritmos en donde resulta critico elegir el mejor camino posible en cada momento como sucede en dijkstra.

A continuación se muestra el algoritmo de la búsqueda por anchura en un grafo representado por medio de listas de adyacencias. En dicho algoritmo se usa una cola para almacenar los nodos adyacentes al actual y guardarlos para continuar con la búsqueda. El siguiente listado contiene la implementación del recorrido por anchura para un grafo completamente conectado (existe al menos un camino entre cualquier par de vértices en el grafo) y para un grafo que no lo esta.

//algoritmo para grafo completamente conectadovoid BFS(int v){//v es el nodo de inicio del recorridolist<int> cola;//cola de adyacenteslist<int>::iterator nodo_actual, aux, fin; visitado[v] = 1;//marcamos como visitado el nodo de inicio cola.push_back(v);//metemos inicio a la cola while(!cola.empty()){ nodo_actual = cola.front();//sacar nodo de la cola cola.pop_front(); aux = grafo[nodo_actual].begin();//posicionar iteradores para //lista de ady fin = grafo[nodo_actual].end(); while(aux != fin){//recorrer todos los nodos ady a nodo actual if(!visitado[*aux]){//añadir a la cola solo los no visitados visitado[*aux] = 1;//marcarlos como visitados cola.push_back(*aux);//añadirlos a la cola //aqui podriamos añadir codigo para hacer algo mientras //recorremos el grafo } aux++;//avanzar al siguiente adyacente del nodo actual } }}

Page 11: grafos en codigos completo

//algoritmo para grafo que no esta completamente conectadovoid BFS2(){int i; for(i = 0; i < nvert; i++) if(!visitado[i]) BFS(i);}

Listado 7 BFS o recorrido por anchura

Para que el código anterior funcione, se deben declarar de manera global el grafo y el arreglo de visitados. El arreglo de visitados debe contener ceros antes de iniciar el recorrido en el grafo. Dentro del ciclo que añade los vértices recién visitados a la cola puede añadirse código para hacer algo mientras se recorre el grafo, un ejemplo de esto último lo pueden encontrar en mi solución del problema 10009, donde se usa para asignar un nivel a cada ciudad y luego implementar un algoritmo Adhoc para encontrar un camino entre dos ciudades. El código lo pueden consultar en el editor de la página de entrenamiento de la UTM, navegando hasta la carpeta jorge/10009.

El algoritmo de la búsqueda por profundidad se puede hacer modificando el anterior en la parte que usa una cola y usar una pila. Otra forma de implementarla es usando recursividad, a continuación se muestran ambos enfoques así como la rutina para hacer la búsqueda en grafos que no están completamente conectados.

A continuación se presenta el listado con la implementación de la búsqueda por profundidad o DFS. También se ha añadido en la versión recursiva un contador que marca el orden en el que fueron visitados los nodos del grafo, dicho orden es muy útil al implementar otros algoritmos de grafos.

Page 12: grafos en codigos completo

void DFS(int v){//v es el nodo de inicio del recorridolist<int> pila;//pila de nodos adyacenteslist<int>::iterator nodo_actual, aux, fin; visitado[v] = 1;//marcar como visitado el nodo de inicio pila.push_back(v); while(!pila.empty()){//mientras no se vacie la pila de adyacentes nodo_actual = pila.back(); //aqui podriamos marcar el orden en que se visitaron pila.pop_back(); aux = grafo[nodo_actual].begin();//posicionar iteradores para //lista ady fin = grafo[nodo_actual].end(); while(aux != fin){//recorrer todos los ady al nodo actual if(!visitado[*aux]){//añadir a la pila solo los no visitados visitado[*aux] = 1; pila.push_back(*aux); //aqui podemos añadir código para hacer algo mientras //realizamos el recorrido } aux++;//avanzar al siguiente adyacente del nodo actual } }}

//esta seria la versión recursiva del algoritmo anterior en cuyo caso no se necesita la pilavoid DFS(int v){list<int>::iterator aux, fin;//iteradores para lista de ady visitado[v] = 1;//marcar como visitado //aqui se podria marcar el orden en que fueron visitados aux = grafo[v].begin();//posicionar los iteradores para lista de ady fin = grafo[v].end(); while(aux != fin){ if(!visitado[*aux]) DFS(*aux);//no se necesita marcar porque *aux se convierte en v aux++;//avanzar al siguiente adyacente de v }}

//esta es la version para grafos que no estan completamente conectadosvoid DFS2(){int i; for(i = 0; i < nvert; i++)//buscar un nuevo nodo de inicio que no ha sido visitado if(!visitado[i]) DFS(i);}

Listado 8 DFS o recorrido por profundidad

Es importante hacer notar que los recorridos por anchura son útiles en aquella aplicaciones en las queremos encontrar el camino más corto entre cualquier par de vértices y es por ello que forman la

Page 13: grafos en codigos completo

base de dichos algoritmos. El recorrido por profundidad sirve por otro lado para averiguar si un par de grafos están conectados.

A continuación se muestra un algoritmo que encuentra un camino a partir de los recorridos por profundidad y por anchura. Dichos algoritmos se basan en llevar un registro del padre de cada nodo en el recorrido, eso se puede conseguir agregando una línea que guarde en un arreglo de padres cuando se meten los vértices no visitados a la pila o cola de adyacentes. Como sabemos que cada nodo que se mete a la pila proviene del nodo_actual en el recorrido, basta con añadir una línea como padre[nodo_actual] = *aux; dentro del ciclo que añade los nodos no visitados a la pila o cola. Se muestra como ejemplo la solución del problema 10009, cuyo listado se incluye a continuación.#include<stdio.h>#include<list>#include<vector>

using namespace std;

int padre[26], visitado[26];//auxiliares para el recorridovector< list<int> > grafo(26);//almacenar el grafochar c1[256], c2[256];//ciudades a explorarint casos, aristas, consultas;

//funciones de soporte para manejar cada consulta y cada caso

//antes de cada caso se debe borrar el grafovoid borra_grafo(){//limpia las aristas del grafo para cada caso nuevoint i; for(i = 0; i < 26; i++) grafo[i].clear();//borrar cada lista de adyacencia}

//antes de cada recorrido se deben inicializar los padres y los visitadosvoid inicializa_busqueda(){//en cada consulta se inicializan los valoresint i; for(i = 0; i < 26; i++){ padre[i] = -1;//los padres contienen a -1 como bandera de no hay padre visitado[i] = 0;//todos los nodos se marcan como no visitados }}

Page 14: grafos en codigos completo

//funcion de búsqueda por anchura que almacena el padre de cada nodo durante el//recorrido

void BFS(int v){//recorre el grafo por anchura a partir de vlist<int> cola;//cola de adyacenteslist<int>::iterator aux, fin;//iteradores para recorrer adyacentes del //nodo actualint nact;//nodo actual visitado[v] = 1;//se marca nodo v como visitado cola.push_back(v);//se mete a la cola de adyacentes while(!cola.empty()){ nact = cola.front();//se obtiene primer elemento de la cola cola.pop_front();//se elimina dicho elemento aux = grafo[nact].begin();//se obtienen iteradores a lista de nodo actual fin = grafo[nact].end(); while(aux != fin){//mientras haya adyacentes al nodo actual if(!visitado[*aux]){//se toman los nodos no visitados visitado[*aux] = 1;//se marcan como visitados padre[*aux] = nact;//se almacena el padre del nodo recien visitado cola.push_back(*aux);//se mete dicho nodo a la cola } aux++;//tomar siguiente adyacente al nodo actual } }}

//funcion que encuentra el camino a partir del origen v usado en el recorrido//usando el arreglo de padres

void camino(int origen, int destino){//encuentra un camino de origen a destino//usando el arreglo de padres y un procedimiento recursivo if(origen == destino){//si se llego al caso base printf("%c", origen + 'A'); }else{ if(padre[destino] == -1){//si no existe un camino hacia el origen //desde el destino actual de llamada recursiva printf("no existe camino de %c a %c\n", origen + 'A', destino + 'A'); }else{ camino( origen, padre[destino]);//se toma como caso mas simple //de manera recursiva printf("%c", destino + 'A');//se imprimen en orden inverso a //partir del destino } }}

Page 15: grafos en codigos completo

//solucion del problema 10009 usando los algoritmos mencionados en el material

int main(){ int nodo1, nodo2; scanf("%d\n", &casos);//leer numero de casos

while(casos>0){scanf("%d %d\n", &aristas, &consultas);//leer numero de aristas y

//de consultasfflush(stdin);

borra_grafo();//limpiar el grafo antes de leer las aristaswhile(aristas > 0){//leer las aristas y almacenarlas en el grafo

scanf("%s %s\n", c1, c2);//leer arista como ciudad1 ciudad2fflush(stdin);nodo1 = c1[0] - 'A';//encontrar posicion en la lista denodo2 = c2[0] - 'A';//adyacentes en el grafografo[nodo1].push_back(nodo2);//mete la arista en grafo no

grafo[nodo2].push_back(nodo1);//dirigidoaristas--;//actualizar numero de aristas por leer

}

while(consultas > 0){//leer las consultasscanf("%s %s\n", c1, c2);//leer origen y destino de

//la consultafflush(stdin);nodo1 = c1[0] - 'A';//encontrar posiciones en la lista denodo2 = c2[0] - 'A';//adyacentes

inicializa_busqueda();//borra los arreglos antes de //iniciar la busqueda BFS(nodo1);//encuentra los caminos a partir de nodo1(origen)

camino( nodo1, nodo2);//encontrar el camino de la ciudad1 a //la ciudad2 printf("\n");

consultas--;//actualizar el numero de consultas por realizar}casos--;//actualizar el numero de casos por resolverif(casos>0)//para no imprimir el ultimo enter

printf("\n");}

return 0;}

Listado 9 Caminos a partir del recorrido en anchura

En el listado anterior se usa como base el hecho de que los recorridos por anchura o profundidad eligen un camino único para cada destino alcanzable a partir de un origen, dicho camino se puede reconstruir a partir de las aristas (padre[i], i) que se eligieron durante el recorrido. El camino es único porque los recorridos producen una estructura de árbol con las aristas seleccionadas. Los ciclos se eliminan por medio del arreglo de nodos previamente visitados, los cuales se descartan durante el recorrido. El problema 10009 también puede ser resuelto si se sustituye la búsqueda por anchura usando

Page 16: grafos en codigos completo

una búsqueda por profundidad porque las condiciones del problema hacen que los árboles resultantes sean idénticos.

Las búsquedas son una parte muy importante de los algoritmos sobre grafos y muchos de ellos son construidos a partir de ellos. Algunos permiten clasificar y encontrar propiedades interesantes de los grafos como los ciclos y los puntos de articulación. En el siguiente apartado se mostrarán algoritmos basados en alguna variación de los recorridos y se pedirá hacer referencia a esta sección con el fin de enfocarse en la técnica nueva sin explicar nuevamente el esquema general del recorrido usado.

Page 17: grafos en codigos completo

4. Teorema de los paréntesis y sus aplicaciones

Durante un recorrido en profundidad es posible almacenar el tiempo en que se visita un nodo por primera vez, cuando se han terminado de recorrer todos los nodos adyacentes a dicho nodo y los momentos en los que se vuelve a visitar. Con dicha información es posible hacer una clasificación de las aristas usadas para construir el recorrido en aristas del árbol de recorrido, aristas de retroceso, aristas de cruce y de avance. Las primeras son aquellas aristas (u, v) que se toman cuando se retira de la pila el nodo u y se detecta que el nodo v no ha sido visitado aún. Las aristas de retroceso son aquellas adyacentes al nodo que se saca de la pila y que ya han sido visitadas, lo que indica la presencia de un ciclo. Las aristas de cruce y de avance resultan de aquellos que no se encuentran completamente conectados cuando existe una arista de uno de los árboles del bosque de recorrido, hacia otro de los árboles previamente visitados. Este tipo de aristas resultan comúnmente en los grafos dirigidos.

El teorema de los paréntesis dice que los tiempos de inicio y finalización de la visita de un nodo y sus adyacentes forman una estructura de paréntesis perfectamente anidados. Esta estructura de paréntesis representa de alguna forma el árbol de recorrido, aunque también puede representar un bosque. Dicha información se usa para algoritmos como el de los componentes conexos y los puntos de articulación. Otra aplicación resulta de la ordenación topológica de los vértices en el grafo. Ambos algoritmos se presentan a continuación en forma de seudocódigo y luego se sugiere una forma de implementarlos.

El algoritmo del ordenamiento topológico resuelve el problema de encontrar el orden en que se deben llevar a cabo una serie de actividades cuando existen requisitos de actividades previas a realizar como puede ser la curricula de materias a estudiar en una universidad. Muchas actividades requieren de dicha planeación y por lo regular se aplican en el manejo de proyectos de diversa índole

Page 18: grafos en codigos completo

como pueden ser inversiones, manufactura y construcción. Simplemente el vestirse requiere que se siga una secuencia apropiada; por ejemplo, nadie se pone los calcetines después de ponerse los zapatos.

ORDENAMIENTO_TOPOLOGICO(G) DFS(G) y calcular f[v] para cada v en G Conforme se termina de visitar un vértice insertar al frente de

una lista Devolver la lista como resultado

Los tiempos de finalización y de inicio de visita de un vértice se encuentran llevando un contador que indique el orden en el que se hacen las llamadas y almacenando dicho contador en un arreglo. La idea se muestra a continuación a manera de seudocódigo.

DFS(G) Para cada nodo u en G

o color[u] = blancoo padre[u] = NULO

tiempo = 0 Para cada nodo u en G

o Si color[u] = blanco DFS-VISIT(u)

DFS-VISIT(u) color[u] = gris //se acaba de visitar el nodo u por primera vez tiempo = tiempo + 1 d[u] = tiempo Para cada nodo v que se adyacente a u //explorar aristas (u,v)

o si color[v] = blanco padre[v] = u DFS-VISIT(v)

color[u] = negro //se termino de visitar al nodo u y todos sus adyacentes

tiempo = tiempo + 1

Page 19: grafos en codigos completo

f[u] = tiempo

En los algoritmos anteriores se han usado ideas ya expuestas en el apartado anterior. Por lo que solo resta hacer algunas observaciones. La primera de ellas es que el arreglo color se usa de manera similar a visitados con el fin de detectar los nodos previamente visitados y ahora se han definido tres estados diferentes, sin visitar corresponde al color blanco, recién visitado corresponde al gris y negro indica que se ha terminado de explorar la rama del árbol que contiene a nodo en particular. La variable tiempo se usa para determinar el orden consecutivo en que se visitan los nodos y dicha información se almacena en el arreglo d. El arreglo padre se usa para identificar las aristas que forman parte del árbol de recorrido y para reconstruir los caminos a partir de la raiz de dichos árboles. Debe notarse que un nodo se marca como negro hasta que todos sus nodos adyacentes han terminado de recorrerse y tienen el estado de gris o negro. El arreglo f almacena el momento en que los nodos se marcaron como negros. La información de los recorridos tal como se muestran aquí fue tomada del capitulo 22 del libro de Cormen 2da edición. La implementación de los algoritmos anteriores, usando c y la STL, se muestra a continuación.#include<stdio.h>#include<vector>#include<list>

using namespace std;

vector< list<int> > grafo(10);//representacion del grafoint padre[10], color[10], d[10], f[10], tiempo;//variables de los algoritmos

#define BLANCO 0 //estados del nodo durante el recorrido#define GRIS 1#define NEGRO 2

#define NULO -1 //bandera para indicar que no se conoce al padre de un nodo

void limpia_grafo();void DFS();void DFS_VISIT(int);

//programa de prueba para los algoritmos DFS que aparecen en el cormen

Page 20: grafos en codigos completo

int main(){//variables para capturar el grafoint na, origen, destino, i; //capturar el numero de aristas en el grafo scanf("%d\n", &na); limpia_grafo(); while(na){ scanf("%d %d\n", &origen, &destino); grafo[origen].push_back(destino); na--; }

//se llama al procedimiento de busqueda DFS();

//se imprime el arreglo de descubrimientos printf("arreglo d\n"); for(i = 0; i < 6; i++) printf("d[%d] = %d, ", i, d[i]); printf("\n");

//se imprime el arreglo de finalizaciones printf("arreglo f\n"); for(i = 0; i < 6; i++) printf("f[%d] = %d, ", i, f[i]); printf("\n");

return 0;}

//limpiar el grafo antes de capturar los datosvoid limpia_grafo(){int i; for( i = 0; i < 6; i++) grafo[i].clear();}

//implementacion de los algoritmos tal como aparecen en el libro de cormen

void DFS(){int u;

//inicializar las variables antes del recorrido for( u = 0; u < 10; u++){ color[u] = BLANCO; padre[u] = NULO; } tiempo = 0;

//recorrido para grafos en general(no completamente conectados) for( u = 0; u < 6; u++) if( color[u] == BLANCO ) DFS_VISIT(u);}

//version recursiva del DFS que lleva cuenta de los tiempos de descubrimiento y//finalización, para la demostración del teorema de los parentesis

Page 21: grafos en codigos completo

void DFS_VISIT(int u){//iteradores para manejar la lista de adyacentes a ulist<int>::iterator v, fin; color[u] = GRIS; tiempo++; d[u] = tiempo; //iniciar con la visita de los adyacentes a u for(v = grafo[u].begin(); v != grafo[u].end(); v++){ if(color[*v] == BLANCO){ padre[*v] = u; DFS_VISIT(*v); } } color[u] = NEGRO; tiempo++; f[u] = tiempo;}

Listado 10 BFS como aparece en el cormen

El algoritmo anterior se probó con el grafo de la figura 5. Luego se muestra la salida del programa anterior donde se muestra el tiempo de descubrimiento y finalización de cada nodo en el grafo.

Figura 5 Grafo para probar listado 10

La salida del programa del listado 10 es la siguiente:

[jorge@localhost ~]$ ./dfs_cormen<dfs_cormen.inarreglo dd[0] = 1, d[1] = 2, d[2] = 9, d[3] = 4, d[4] = 3, d[5] = 10,arreglo ff[0] = 8, f[1] = 7, f[2] = 12, f[3] = 5, f[4] = 6, f[5] = 11,

Si ordenamos los tiempos de descubrimiento y finalización, escribiendo los números de nodos, tendremos la siguiente secuencia que ilustra el teorema de los paréntesis.

0 1 2

3 4 5

Page 22: grafos en codigos completo

(0 (1 (4 (3 3) 4) 1) 0) (2 (5 5) 2)Dicha secuencia indica que como el resultado del recorrido se

ha formado un bosque con 2 arboles, el primero contiene a los nodos 0, 1, 4 y 3, el segundo contiene a los nodos 2 y 5.

Ahora estamos en la posibilidad de mostrar la implementación del ordenamiento topológico, tal como aparece en seudocódigo mostrado anteriormente. Se muestran únicamente las modificaciones que es necesario hacer al DFS_VISIT, para almacenar la lista con el orden de finalización.//código modificado para almacenar el orden de finalización requerido para el //odenamiento topologicovoid DFS_VISIT(int u){//iteradores para manejar la lista de adyacentes a ulist<int>::iterator v, fin; color[u] = GRIS; tiempo++; d[u] = tiempo; //iniciar con la visita de los adyacentes a u for(v = grafo[u].begin(); v != grafo[u].end(); v++){ if(color[*v] == BLANCO){ padre[*v] = u; DFS_VISIT(*v); } } color[u] = NEGRO; tiempo++; f[u] = tiempo; orden.push_front(u);//insertar cada vértice en el orden en que finaliza}

void ORDENAMIENTO_TOPOLOGICO(){list<int>::iterator aux;//iterador para recorrer la lista con resultados

orden.clear();//borrar la lista de ordenamiento

dfs();//calcular los f[u] del grafo

//imprimir los resultadosfor( aux = orden.begin(); aux != orden.end(); aux++) printf(“%d, “, *aux);printf(“\n”);

}

Listado 11 Modificaciones a DFS para ordenamiento topológico

Para que el código anterior funcione, se necesita que la lista que almacenará el ordenamiento sea declarada de manera global. En el código anterior la variable tipo lista de enteros se llama orden.

Page 23: grafos en codigos completo

Cuando se prueba con el grafo de la figura 22.7 de la segunda edición del cormen, el resultado es el siguiente:

[jorge@localhost ~]$ ./ord_topo<ord_topo.in8, 6, 3, 4, 0, 1, 7, 2, 5,0 = undershorts, 1 = pants, 2 = belt, 3 = shirt, 4 = tie, 5 =

jacket, 6 = socks, 7 = shoes, 8 = watch

Aún cuando el resultado es diferente al que aparece en el libro, no se altera el orden correcto que se necesita para colocarse encima las prendas. La razón por la que se explica la diferencia es el orden en el que se enumeran los nodos y luego se visitan.

A continuación se lista el seudocódigo del algoritmo que encuentra los componentes fuertemente conectados del grafo. Por definición un componente fuertemente conectado de un grafo es un subgrafo C en el que para cada par de vértices u y v en C, existe un camino de u a v y de v a u. El algoritmo se basa en el lema que enuncia que los componentes fuertemente conectados de un grafo corresponden a los de su transpuesto. El grafo transpuesto T de un grafo G, es el mismo conjunto de vértices pero con las direcciones de las aristas en sentido contrario, es decir la arista (u, v) en G corresponde a (v, u) en T.

SCC(G)1. llamar DFS(G) para calcular los f[u] para cada u en G2. encontrar T3. llamar DFS(T), pero en el ciclo principal de DFS, los vértices

se exploran en orden decreciente del f[u] calculado en el paso 1

4. Sacar los vértices de cada árbol generado en el paso 3 como un componente fuertemente conexo por separado.

A continuación se muestran las modificaciones necesarias al DFS para implementar el algoritmo anterior. Debe notarse que el orden decreciente de los f[u] calculados en el paso 1, corresponden al ordenamiento topológico de los vértices en G. Por lo que el paso 1

Page 24: grafos en codigos completo

se puede sustituir por obtener el ordenamiento topológico de G. Y en el paso 3 diríamos que se recorre el grafo en profundidad usando el ordenamiento topológico calculado en el paso 1. También se incluye una implementación para encontrar el transpuesto de un grafo.//transpuesto de un grafo G con nv vertices, el resultado es el grafo T

void transpuesto(){list<int>::iterator aux;int i; //borrar el grafo T antes de comenzar for(i = 0; i < nv; i++) T[i].clear(); for(i = 0; i < nv; i++){ for(aux = G[i].begin(); aux != G[i].end(); aux++) T[*aux].push_back(i); } }

//ordenamiento topologico modificado para que no imprima el orden solo lo calculavoid ORDENAMIENTO_TOPOLOGICO(){list<int>::iterator aux;

orden.clear();//borrar la lista //calcular los f[u] con el DFS DFS();}

//encuentra los componentes fuertemente conectados sobre Tvoid SCC(){int u;list<int> aux;

//inicializar las variables antes del recorrido for( u = 0; u < 10; u++){ color[u] = BLANCO; padre[u] = NULO; } tiempo = 0;

//visitar T usando el orden topologico de G ORDENAMIENTO_TOPOLOGICO(); transpuesto(); for( aux = orden.begin(); aux != orden.end(); aux++) if( color[*aux] == BLANCO ){ DFS_VISIT2(*aux); printf("%d");//termino con un SCC } }

//busqueda sobre el grafo T e impresión de los elementos de cada SCCvoid DFS_VISIT2(int u){//iteradores para manejar la lista de adyacentes a ulist<int>::iterator v, fin; color[u] = GRIS; tiempo++; d[u] = tiempo;

Page 25: grafos en codigos completo

printf("%d, ", u);//imprime los elementos del SCC //iniciar con la visita de los adyacentes a u for(v = T[u].begin(); v != T[u].end(); v++){ if(color[*v] == BLANCO){ padre[*v] = u; DFS_VISIT(*v); } } color[u] = NEGRO; tiempo++; f[u] = tiempo;}

Listado 12 Calculo de los SCC

En el código anterior se deben declarar los grafos G y T como variables globales. Se deja como ejercicio hacer la implementación completa y probar con el grafo de la figura 22.10 del libro de cormen.

Adicionalmente al código anterior, es posible realizar una implementación que localice los puntos de articulación en un grafo a partir de su recorrido en profundidad y del orden en que se visitan sus nodos con la ayuda de la funcion LOW. Dicho algoritmo se describe en la figura 5.11 de Aho, Hopcroft, Ullman “The design and análisis of computer algorithms”. Se transcribe a continuación en el siguiente seudocódigo:

void SEARCHB(v){marcar v como visitado

dfs_number[v] = cont; cont++; LOW[v] = dfs_number[v]; para cada vertice w adyacente a v{ si no ha sido visitado w{ añadir (v, w) al arbol T; padre[w] = v; SEARCHB(w); si LOW[w] >= dfs_number[v] se encontro componente; LOW[v] = min(LOW[v], LOW[w]); }de otra forma{ si w no es el padre de v LOW[v] = min(LOW[v], dfs_number[w]); } }}

En la línea donde se encontró un componente, se puede vaciar la lista T de los vértices v que forman parte de un componente o imprimir los vértices v que corresponden a los puntos de articulación

Page 26: grafos en codigos completo

en el grafo, con excepción de la raíz del árbol de búsqueda. La implementación del algoritmo anterior se lista a continuación:

#include <stdio.h>#include <vector>#include <list>using namespace std;

vector< list<int> > grafo(30);int LOW[30], visitado[30], dfs_number[30], padre[30];int i, cont, n;int origen, destino;list< pair<int , int> > T;list<int> articulaciones;

void limpia_grafo(){ cont = 0; for(i = 0; i < 30; i++){ LOW[i] = visitado[i] = dfs_number[i] = padre[i] = 0; grafo[i].clear(); } T.clear(); articulaciones.clear();}

void searchb(int v){//implementación del algoritmo de busqueda de puntos list<int>::iterator aux, fin;//de articulaciónpair<int, int> arista; visitado[v] = 1; dfs_number[v] = cont; cont++; LOW[v] = dfs_number[v]; aux = grafo[v].begin(); fin = grafo[v].end(); arista.first = v; while(aux != fin){ if(!visitado[*aux]){ arista.second = *aux; T.push_back(arista); padre[*aux] = v; searchb(*aux); if(LOW[*aux] >= dfs_number[v])

articulaciones.push_back(v); LOW[v] = min( LOW[v], LOW[*aux]); }else{ if(*aux != padre[v]) LOW[v] = min( LOW[v], dfs_number[*aux]); } aux++; } }

int main(){list<int>::iterator aux, fin; limpia_grafo(); scanf("%d\n", &n);

Page 27: grafos en codigos completo

while(n>0){ scanf("%d %d\n", &origen, &destino); grafo[origen-1].push_back(destino-1); grafo[destino-1].push_back(origen-1); n--; } searchb(0); aux = articulaciones.begin(); fin = articulaciones.end(); printf("articulaciones\n"); while(aux != fin){ n = i + 1; printf("%d\n",*aux + 1); aux++; } return 0;}

Listado 12 Implementación de puntos de articulación

En el código anterior hace falta validar que la raíz del árbol de búsqueda en profundidad no siempre es un punto de articulación. La raíz siempre tiene un dfs_number igual a cero y es por ello que aparece como punto de articulación. Es de notarse que todos los miembros, a excepción del punto de articulación, de un componente tienen el mismo valor de LOW.

Page 28: grafos en codigos completo

5. Árboles de expansión mínima

En ocasiones se presenta el problema de elegir uno de varios árboles de expansión que cumplan con el requisito de que la suma total del peso de sus vértices sea la mínima posible. Este es un problema de optimización en donde se busca reducir el costo total de unir una serie de puntos en un grafo, por ejemplo puede desearse unir con caminos un conjunto de ciudades de tal forma que la longitud total de los caminos a construir sea el mínimo y que además permita que todas estén conectadas. Existen una serie de algoritmos basados en una técnica de programación ávida que cumplen con dicho requisito, nos enfocaremos particularmente en dos de ellos, el algoritmo de Kruskal y en el de Prim.

El algoritmo de Kruskal basa su funcionamiento en la elección de las aristas de menor peso que no forman ciclos, para poder elegir dichas aristas es necesario usar un método de almacenamiento que las ordene de menor a mayor peso. Dado que dicho método iniciar eligiendo cualquier arista que cumpla con el requisito de tener el menor peso y que no forme ciclos, es necesario mantener una serie de conjuntos disjuntos por lo que su implementación hace uso de la estructura UNION-FIND recomendada por el libro de cormen y que aparece dentro de su propio apartado dentro de la sección estructuras del temario. El seudocódigo del Kruskal se muestra a continuación:

MST-KRUSKAL(G,w)1. A es el conjunto vacío2. para cada vértice v en G

make-set(v)3. ordenar las aristas de menor a mayor peso4. para cada arista (u,v) en G, en tomadas en orden creciente

si find-set(u) es diferente de find-set(v)o A es la union de A con (u,v)o union(u,v)

5. Devolver A

Page 29: grafos en codigos completo

Las operaciones en negritas corresponden a la implementación de UNION-FIND. El resultado del algoritmo es el árbol de expansión representado por el conjunto de aristas incluidas en A. A continuación se muestra una implementación basada en la STL. Esta implementación se probó con el grafo que aparece en la figura 23.4 del libro de Cormen.

#include<stdio.h>#include<vector>#include<algorithm>

using namespace std;

//implementación de UNION-FIND#define MAX 1000 // ajustarlo apropiadamente (tamaño máximo del conjunto)int p[MAX], rank[MAX];

void make_set(int x) { p[x] = x; rank[x] = 0;}

void link(int x, int y) { if (rank[x] > rank[y]) p[y] = x; else { p[x] = y; if (rank[x] == rank[y]) rank[y] = rank[y] + 1; }}

int find_set(int x) { if (x != p[x]) p[x] = find_set(p[x]); return p[x];}

void union_set(int x, int y) { link(find_set(x), find_set(y));}

Page 30: grafos en codigos completo

//definiciones para usar en el algorimo de Kruskal#define ARISTA pair<int, int>#define ARISTA_PONDERADA pair<int, ARISTA>int nvert, narist;

//representación del grafo con un vector de aristasvector<ARISTA_PONDERADA> G(14), A(14);//14 aristas para la prueba

//algoritmo de kruskalvoid kruskal(){ARISTA a;int i, j;//contadores de aristas y verticesint u, v;//vertices for(v = 0; v < nvert; v++) make_set(v); sort(G.begin(), G.end()); for(i = 0, j = 0; i < narist; i++){ a = G[i].second; u = a.first; v = a.second; if(find_set(u) != find_set(v)){ A[j].first = G[i].first; A[j++].second = a; union_set(u,v); } } }

int main(){int i, n;//contadoresint u, v, w;//datos de las aristasARISTA a;//aristaARISTA_PONDERADA ap;//arista ponderada

//programa de prueba para el algoritmo de kruskal scanf("%d %d\n", &nvert, &narist);//leer numero de aristas y vertices n = narist;//iniciar los contadores

i = 0;

while(n){//ciclo para leer las aristas scanf("%d %d %d\n", &u, &v, &w); a.first = u; a.second = v; ap.first = w; ap.second = a; G[i++] = ap; n--;

}

for(i = 0; i < narist; i++)//ciclo para marcar las aristas A[i].first = -1; //se manda a llamar a kruskal

kruskal();

Page 31: grafos en codigos completo

//se imprimen los resultadosprintf("arbol resultante\n");

for(i = 0; i < narist; i++){ if(A[i].first != -1){ ap = A[i]; a = ap.second; u = a.first; v = a.second; w = ap.first; printf("(%d, %d, %d)\n", u, v, w); } } return 0;}

Listado 13 Implementación de Kruskal

A continuación se presenta el uso del algoritmo de Kruskal para la solución del problema 10397 Connect the Campus del juez en línea de la UVA. Aquí se usa kruskal para que sume las distancias entre los puntos y encuentre la distancia total. Se usa UNION-FIND para incluir los caminos ya construidos. Para evitar problemas con las comparaciones y errores de precisión, las distancias se almacenan como enteros y luego se calcula la raiz cuadrada. También se sobrecargo el operador de comparación para que hiciera correctamente las comparaciones de los pesos de las aristas. La solución aceptada es la siguiente:#include<stdio.h>#include<math.h>#include<vector>#include<algorithm>

using namespace std;

//implementación de UNION-FIND#define MAX 1000 // ajustarlo apropiadamente (tamaño máximo del conjunto)int p[MAX], rank[MAX];int nconj;

void make_set(int x) { p[x] = x; rank[x] = 0;}

void link(int x, int y)

Page 32: grafos en codigos completo

{ if (rank[x] > rank[y]) p[y] = x; else { p[x] = y; if (rank[x] == rank[y]) rank[y] = rank[y] + 1; }}

int find_set(int x) { if (x != p[x]) p[x] = find_set(p[x]); return p[x];}

void union_set(int x, int y) { link(find_set(x), find_set(y)); nconj--;//disminuye el numero de conjuntos con cada union}

//definiciones para usar en el algorimo de Kruskal#define ARISTA pair<int, int>#define ARISTA_PONDERADA pair< long, ARISTA>int nvert, narist;long double longitud;

//representación del grafo con un vector de aristasvector<ARISTA_PONDERADA> G;

//sobrecarga del operador menor para compararclass LessWeightedEdge{public: bool operator()(const ARISTA_PONDERADA &p, const ARISTA_PONDERADA &q) const{ return (p.first < q.first); }};

//algoritmo de kruskalvoid kruskal(){ARISTA a;int i;//contadores de aristas y verticesint u, v;//vertices //cuando se ordena lo hace con enteros sort(G.begin(), G.end(), LessWeightedEdge()); longitud = 0; //revisa todas las aristas o hasta que se forma un conjunto unico for(i = 0; (i < narist) && (nconj > 1); i++){ a = G[i].second; u = a.first; v = a.second; if(find_set(u) != find_set(v)){ //aqui si se calcula la raiz longitud = longitud + sqrtl(G[i].first); union_set(u,v); } } }//para evitar errores de precision se almacena antes de calcular la raiz

Page 33: grafos en codigos completo

double distancia(long x1, long y1, long x2, long y2){long dif1, dif2; dif1 = x1 – x2; dif2 = y1 – y2; return dif1*dif1 + dif2*dif2;}

int main(){vector< pair<int, int> > edificios;pair<int , int> edificio;int i, j, x, y, existentes;int x1, y1, x2, y2;int u, v;long w;ARISTA a;ARISTA_PONDERADA ap;

while(scanf("%d\n", &nvert) != EOF){//leer todos los casos //borrar el vector de edificios edificios.clear(); //leer la posicion de todos los edificios y almacenar en el vector for(i = 0; i < nvert; i++){ scanf("%d %d\n", &x, &y); edificio.first = x; edificio.second = y; edificios.push_back(edificio); } //borrar el grafo G.clear();

//generar el grafo for(i = 0, narist = 0; i < nvert; i++){ x1 = edificios[i].first; y1 = edificios[i].second; for(j = i+1; j < nvert; j++){ x2 = edificios[j].first; y2 = edificios[j].second; w = distancia( x1, y1, x2, y2); a.first = i; a.second = j; ap.first = w; ap.second = a; G.push_back(ap); narist++; } } //inicializar los conjuntos for(v = 0; v < nvert; v++) make_set(v); nconj = nvert; //incluir los caminos ya construidos scanf("%d\n",&existentes); while(existentes){ scanf("%d %d\n", &u, &v); //se valida antes de incluir el camino if(find_set(u-1) != find_set(v-1)) union_set( u-1, v-1); existentes--; }

//ejecutar kruskal para calcular la longitud

Page 34: grafos en codigos completo

kruskal(); //imprimir el resultado printf("%.2llf\n", longitud); } return 0;}

Listado 14 Solución del problema 10397

Ahora se presenta el algoritmo de Prim para encontrar el árbol de expansión mínima. Aquí la diferencia con el algoritmo anterior es que solo se mantienen dos conjuntos, el de los vértices incluidos en el árbol y el de los que no lo están. El procedimiento consiste en elegir la arista de menor peso que une un vértice en el conjunto del árbol con un vértice que no esta en el árbol. El seudocódigo se presenta a continuación:

PRIM(G, r)1. para cada vértice u en G

clave[u] = infinito padre[u] = NULO

2. clave[r] = 03. Meter los vértices u de G a una cola de prioridad Q con clave[u]4. Mientras no este vacía Q

Extraer un vértice de Q y llamarlo u Para cada vértice v que sea adyacente a u

o Si v esta en Q y el peso de (u,v) < clave[v] padre[v] = u clave[v] = w(u,v)

A continuación se propone una implementación basada en la cola de prioridad de la STL y usando un algoritmo similar al de la búsqueda por anchura. La variante es que se eligen primero aquellas aristas con un menor peso por medio de la cola de prioridad. Para evitar los ciclos, se revisa si el vértice a visitar ya fue incluido en el árbol y se marca como visitado. Para convertir la cola de prioridad de la STL en una cola ascendente (el menor en el tope) es necesario

Page 35: grafos en codigos completo

meter los pesos como números negativos. También es importante hacer notar que un nodo no se considera visitado hasta que es sacado de la cola. Una mejora simple que se puede hacer a la implementación es tener un contador que revise que todos los vértices fueron visitados y hacer que el ciclo principal termine antes. La implementación del algoritmo anterior se presenta a continuación:#include<stdio.h>#include<queue>#include<vector>#include<list>

using namespace std;

#define NVERT 9//se usa la figura 23.5 del cormen como prueba

//definicion de la arista ponderada aqui almacenamos peso, nodo destino#define ARISTA_PONDERADA pair< int, int>

#define INFINITO 300000000#define NULO -1

vector< list< ARISTA_PONDERADA> > G(NVERT);int padre[NVERT], clave[NVERT];int nvert, narist;

void prim(int r){priority_queue< ARISTA_PONDERADA> Q;ARISTA_PONDERADA ap;int u, v, visitado[NVERT];list<ARISTA_PONDERADA>::iterator aux;

//inicializar el algoritmo for(u = 0; u < NVERT; u++){ clave[u] = INFINITO; padre[u] = NULO; visitado[u] = 0; } clave[r] = 0; visitado[r] = 1; //inicializar la cola de prioridad ap.first = 0; ap.second = r; Q.push(ap); //ciclo principal del algoritmo while(!Q.empty()){ ap = Q.top();//sacamos el menor elemento de la cola Q.pop(); visitado[u] = 1; u = ap.second; for(aux = G[u].begin(); aux != G[u].end(); aux++){ v = (*aux).second; if(!visitado[v] && ((*aux).first < clave[v])){ padre[v] = u;//sirve para reconstruir el arbol clave[v] = (*aux).first;//el peso de la arista añadida ap.first = (*aux).first*(-1);

Page 36: grafos en codigos completo

ap.second = v; Q.push(ap); } } }}

int main(){int i, n;int u, v, w;ARISTA_PONDERADA ap;

scanf("%d %d\n", &nvert, &narist); n = narist; i = 0; //ciclo para insertar las aristas en el grafo while(n){ scanf("%d %d %d\n", &u, &v, &w); //el grafo es no dirigido por lo se insertan en dos direcciones ap.first = w; ap.second = v; G[u].push_back(ap);//insertar (u, v, w) ap.second = u; G[v].push_back(ap);//insertar (v, u, w); n--; } //se manda a llamar al metodo con la raiz en 0 prim(0); //se imprime el arbol resultante printf("arbol resultante\n"); for(i = 0; i < nvert; i++){ if((i != 0) && (clave[i] != INFINITO)){ u = padre[i]; v = i; w = clave[i]; printf("(%d, %d, %d)\n", u, v, w); } } return 0;}

Listado 15 Implementación de Prim

El problema 10397 también puede resolverse usando el algoritmo de Prim si se ponen a cero los pesos de los caminos ya construidos, esto se haría después del código que construye el grafo. Para ello sería necesario hacer búsquedas en la lista de adyacencias o usar una representación por medio de matrices de adyacencias. La matriz de adyacencias se justifica aquí debido a que el grafo es muy denso. La solución por este medio se deja como ejercicio. Es también claro que para encontrar el peso total del árbol de expansión mínima solo se tienen que sumar todas las entradas del arreglo de claves.

Page 37: grafos en codigos completo

6. Algoritmos para las rutas más cortas

En este apartado se revisarán los algoritmos de las rutas más cortas de Dijkstra y Floyd, por ser los más conocidos y útiles para resolver los problemas de la ACM. Otros algoritmos como el de Bellman-Ford y el de Warshall solo se mencionarán a manera de seudocódigo.

Todos los algoritmos de esta sección usan la desigualdad del triángulo, es decir, tratan de probar si peso(u,v) > peso(u,i) + peso(i,v). Como consecuencia del hecho anterior, los grafos que contienen ciclos negativos no pueden ser resueltos por dichos algoritmos al no encontrar una forma correcta de evaluar correctamente la desigualdad.

Los algoritmos de Bellman-Ford y de Dijkstra usan los siguientes algoritmos como inicialización y para determinar una mejor ruta. Dichos algoritmos se listan a continuación a manera de seudocódigo:

Inicialización(G, s)1. para cada vértice v en G

d[v] = INFINITO padre[v] = NULO

2. d[s] = 0

Relajamiento(u,v)1. si d[v] > d[u] + peso(u,v)

d[v] = d[u] + peso(u,v) padre[v] = u

A continuación se muestra el algoritmo de Bellman-Ford, este algoritmo tiene la particularidad de que es capaz de detectar si existen ciclos negativos en el grafo. Por consecuencia sigue

Page 38: grafos en codigos completo

funcionando a pesar de encontrar aristas con pesos negativos con la condición de que no existan los mencionados ciclos negativos.

BELLMAN-FORD(G,s)1. Inicializacion(G,s)2. para i = 1 hasta nvert – 1

para cada arista (u,v) en G relajamiento(u,v)3. para cada arista (u,v) en G

si d[u] > d[u] + peso(u,v) return FALSO4. return VERDADERO

El ciclo interior del paso 2 y el paso 3 pueden hacerse con ayuda de una lista de aristas. Al terminar de ejecutarse el algoritmo, las distancias más cortas serán almacenadas en el arreglo d y los caminos de s al resto de los vértices puede encontrarse por medio del algoritmo recursivo de caminos que se estudio en el apartado 3 “Algoritmos básicos de búsqueda” y que se transcribe a continuación a manera de seudocódigo:

Camino(u,v)1. si u = v imprime u2. en caso contrario si padre[v] = NULO

no existe camino de u a v3. si padre[v] es diferente de NULO

camino(u, padre[v]) imprime v

Debido a que en el paso 2 el algoritmo de Bellman-Ford es lento, su uso se restringe a aquellos casos en los que es importante identificar si existen ciclos negativos en el grafo. En caso de encontrar un ciclo negativo el algoritmo devuelve FALSO. La implementación del algoritmo de Bellman-Ford se deja como ejercicio.

Page 39: grafos en codigos completo

A continuación se presenta el seudocódigo de un algoritmo eficiente para trabajar con grafos dirigidos acíclicos o dag´s. Este algoritmo aprovecha que no existen ciclos en el grafo para proveer de un algoritmo de eficiencia lineal. Se hace uso del ordenamiento topológico como parte del preprocesamiento del grafo. A continuación se presenta el seudocódigo de las rutas más cortas en un dag.

Rutas-cortas-dags(G, s)1. Ordenar topológicamente G2. Inicialización(G,s)3. para cada vértice u, ordenado topológicamente

para cada vértice v que es adyacente a u Relajamiento(u,v)

Una aplicación importante del algoritmo anterior es para construir el análisis temporal de proyectos usando PERT. La ruta más larga ofrecida por el algoritmo anterior corresponde a la ruta crítica que trata de reducirse usando PERT.

Ahora es el momento de analizar con detalle uno de los algoritmos clásicos para encontrar las rutas más cortas. Se trata del algoritmo de Dijkstra, el cual por medio de una técnica ávida actualiza un vector de padres y de uno de distancias mínimas. Las rutas pueden encontrarse con el algoritmo recursivo de caminos que se describió en párrafos anteriores. A continuación se lista el seudocódigo del algoritmo de Dijkstra.

DIJKSTRA(G,s)1. Inicialización(G,s)2. S es el conjunto vacío3. Meter los vértices u a una cola de prioridad Q de acuerdo a d[u]4. mientras Q no este vacía

extraer el minimo de Q en u S = S union {u} para cada v adyacente a u relajamiento(u,v)

Page 40: grafos en codigos completo

La implementación del algoritmo de Dijkstra es muy similar a la del algoritmo de Prim. El conjunto S representa a los vértices ya visitados por el algoritmo y que por tanto no serán incluidos en la cola de prioridad. El procedimiento de relajamiento deberá sin embargo actualizar las distancias de todos los nodos adyacentes a u, sin importar si fueron o no visitados con anterioridad. El algoritmo de Dijkstra no funciona para grafos con aristas negativas sin importar si existen o no ciclos negativos. A continuación se presenta una implementación basada en las colas de prioridad de la STL, nótese que es casi idéntica a la implementación de Prim.#include<stdio.h>#include<queue>#include<vector>#include<list>

using namespace std;

#define NVERT 9

//definicion de la arista ponderada aquí almacenamos peso, nodo destino#define ARISTA_PONDERADA pair< int, int>

#define INFINITO 300000000#define NULO -1

vector< list< ARISTA_PONDERADA> > G(NVERT);int padre[NVERT], d[NVERT];int nvert, narist;

//implementación del algoritmo de dijkstravoid dijkstra(int s){//nodo de origen spriority_queue< ARISTA_PONDERADA> Q;ARISTA_PONDERADA ap;int u, v, visitado[NVERT];list<ARISTA_PONDERADA>::iterator aux;

//inicializar el algoritmo for(u = 0; u < NVERT; u++){ d[u] = INFINITO; padre[u] = NULO; visitado[u] = 0; } d[s] = 0; visitado[s] = 1; //inicializar la cola de prioridad ap.first = 0; ap.second = s; Q.push(ap);

Page 41: grafos en codigos completo

//ciclo principal del algoritmo while(!Q.empty()){ ap = Q.top();//sacamos el menor elemento de la cola Q.pop(); u = ap.second;//recuperamos vertice u visitado[u] = 1;//añadir u a visitados //tomar los vertices adyacentes a u para hacer el relajamiento for(aux = G[u].begin(); aux != G[u].end(); aux++){ v = (*aux).second; if( d[v] > (d[u] + (*aux).first) ){//relajamiento padre[v] = u;//sirve para reconstruir el arbol d[v] = d[u] + (*aux).first;//actualizar la ruta más corta //meter a la cola solo las distancias de vertices no visitados if(!visitado[v]){ ap.first = d[v]*(-1);//cambiamos a cola ascendente ap.second = v; Q.push(ap); }//fin de meter a la cola }//fin del relajamiento }//fin del for }//fin del while}//fin de dijkstra

//programa de prueba para el algoritmo de dijkstraint main(){int i, n;int u, v, w;ARISTA_PONDERADA ap;

scanf("%d %d\n", &nvert, &narist); n = narist; i = 0; //ciclo para insertar las aristas en el grafo while(n){ scanf("%d %d %d\n", &u, &v, &w); //el grafo es dirigido por lo se inserta solo en una dirección ap.first = w; ap.second = v; G[u].push_back(ap);//insertar (u, v, w) n--; } //se manda a llamar al metodo con la raiz en 0 dijkstra(0); //se imprime las aristas del arbol resultante printf("aristas del arbol resultante\n"); for(i = 0; i < nvert; i++){ if((i != 0) && (d[i] != INFINITO)){ u = padre[i]; v = i; printf("(%d, %d)\n", u, v); } } //se imprime el vector de distancias minimas a partir de 0 printf("Distancias minimas a partir del nodo 0\n"); for(i = 0; i < nvert; i++) printf("d[%d] = %d\n", i, d[i]); return 0;}

Listado 16 Implementación de Dijkstra

Page 42: grafos en codigos completo

El algoritmo anterior se probó con el grafo de la figura 24.6 del cormen. Una vez más se recuerda que por medio del algoritmo recursivo camino(u, v) pueden reconstruirse las rutas encontradas por Dijkstra, siempre que u sea el nodo de origen para el algoritmo.

Ahora se muestra la solución del problema 10171 “Meeting Prof. Miguel” del juez de la UVA, usando el algoritmo de Dijkstra. En este caso se usa para encontrar los vectores de las rutas más cortas en el grafo de las personas mayores y luego las rutas más cortas en el grafo de las personas menores. Al final se suman las distancias encontradas y se eligen aquellas posiciones que tienen al menor. Para poder usar el mismo código fue necesario usar parámetros para la función dijsktra, nótese que los grafos se pasan por referencia para no generar copias. El código de la solución aceptada se muestra a continuación.#include <stdio.h>#include <queue>#include <vector>#include <list>

using namespace std;

#define NVERT 26//todas las letras del abecedario

//definicion de la arista ponderada aqui almacenamos peso, nodo destino#define ARISTA_PONDERADA pair< int, int>

#define INFINITO 300000000#define NULO -1

//se almacenan los dos grafos, calles para menores y mayoresvector< list< ARISTA_PONDERADA> > GMayores(NVERT), GMenores(NVERT);int padre[NVERT], dMayores[NVERT], dMenores[NVERT];

//al dijkstra se le pasa el origen, el grafo y el vector de distancias//si fuera necesario se le pasaria el vector de padresvoid dijkstra(int s, vector< list<ARISTA_PONDERADA> > &G, int d[]){priority_queue< ARISTA_PONDERADA> Q;ARISTA_PONDERADA ap;int u, v, visitado[NVERT];list<ARISTA_PONDERADA>::iterator aux;

//inicializar el algoritmo for(u = 0; u < NVERT; u++){ d[u] = INFINITO; padre[u] = NULO; visitado[u] = 0; } d[s] = 0; visitado[s] = 1;

Page 43: grafos en codigos completo

//inicializar la cola de prioridad ap.first = 0; ap.second = s; Q.push(ap); //ciclo principal del algoritmo while(!Q.empty()){ ap = Q.top();//sacamos el menor elemento de la cola Q.pop(); u = ap.second; visitado[u] = 1;//añadir u a visitados //tomar los vertices de la cola los vertices no visitados for(aux = G[u].begin(); aux != G[u].end(); aux++){ v = (*aux).second; if( d[v] > (d[u] + (*aux).first) ){//relajamiento padre[v] = u;//sirve para reconstruir el arbol d[v] = d[u] + (*aux).first;//actualizar la ruta más corta //meter a la cola solo las distancias de vertices no visitados if(!visitado[v]){ ap.first = (*aux).first*(-1);//cambiamos a cola ascendente ap.second = v; Q.push(ap); } } } }}

int main(){ARISTA_PONDERADA ap;int num_calles, n;int minimos[NVERT];char usuario, dir, ciudad1, ciudad2, sha, mig;int peso;int min, i;

while(1){ //leer datos del caso scanf("%d\n", &num_calles); //termina con un cero if(!num_calles) break; //se limpia la información del caso anterior for(i = 0; i < 26; i++){ GMenores[i].clear(); GMayores[i].clear(); }

Page 44: grafos en codigos completo

//ciclo para leer las aristas n = num_calles; while(n>0){ //leer arista scanf("%c %c %c %c %d\n", &usuario, &dir, &ciudad1, &ciudad2, &peso); //validar caso especial if(ciudad1 == ciudad2) peso = 0; ap.first = peso; if(usuario == 'Y'){ ap.second = ciudad2 - 'A'; GMenores[ciudad1 - 'A'].push_back(ap); if(dir == 'B'){ ap.second = ciudad1 - 'A'; GMenores[ciudad2 - 'A'].push_back(ap); } }else{ ap.second = ciudad2 - 'A'; GMayores[ciudad1 - 'A'].push_back(ap); if(dir == 'B'){ ap.second = ciudad1 - 'A'; GMayores[ciudad2 - 'A'].push_back(ap); } } n--; } //leer consulta scanf("%c %c\n", &sha, &mig); dijkstra( sha - 'A', GMenores, dMayores); dijkstra( mig - 'A', GMayores, dMenores); min = dMayores[0] + dMenores[0]; minimos[0] = min; for(i = 1; i < 26; i++) if((dMayores[i] + dMenores[i]) <= min){ min = dMayores[i] + dMenores[i]; minimos[i] = min; }else{ minimos[i] = 15000; } if(min >= 15000) printf("You will never meet.\n"); else{ printf("%d", min); for( i = 0; i < 26; i++) if(minimos[i] == min) printf(" %c", i + 'A'); printf("\n"); } } return 0;}

Listado 17 Solución del problema 10171

Page 45: grafos en codigos completo

La solución del listado anterior puede mejorarse en tiempo si hacemos que el ciclo principal de Dijkstra termine cuando todos los nodos estén marcados como visitados, para ello basta con llevar un contador y encontrar la forma de contar el número de vértices. En el caso de este problema se podría llevar un arreglo de banderas que se ponga a 1 cada vez que un vértice se añade al grafo y luego se contarían los 1’s para saber cuantos vértices existen en el grafo. Las banderas tendrían que inicializarse a 0. El tiempo aún sin la mejora que se menciona es bastante bueno (0.002 segundos) por lo que en este caso no vale la pena, sin embargo puede ser útil al resolver un problema con casos más grandes.

Ahora se presentan los algoritmos que encuentran las distancias más cortas entre todos los pares de vértices. Aún cuando esto puede conseguirse con un ciclo que varíe el vértice de origen en Dijsktra, por lo general se usa el algoritmo de Floyd por tener una implementación más simple como se verá a continuación.

El algoritmo basa su funcionamiento en una técnica de programación dinámica que almacena al paso de cada iteración el mejor camino entre el que pasa por el nodo intermedio k y el que va directamente del nodo i al nodo j. Para determinar cual es el mejor camino se sigue usando la desigualdad del triángulo y para que el algoritmo funcione es necesario hacer una inicialización con los siguientes valores:

0 si i = j

Wij = el peso de la arista dirigida (i,j) si i j y (i,j) E

si i j y (i,j)

El algoritmo permite la presencia de aristas negativas siempre que no existan ciclos negativos como sucede con el algoritmo de

Page 46: grafos en codigos completo

Bellman-Ford. El método consiste en hacer iteraciones del proceso de relajación para cada arista en el grafo, tomando en cada paso un vértice intermedio diferente. La manera más simple de implementarlo es por medio de tres ciclos anidados que iteran sobre un grafo representado por una matriz de adyacencia. Al igual que los otros métodos presentados con anterioridad es posible reconstruir el camino más corto entre cualquier par de nodos por medio de un procedimiento recursivo similar al presentado anteriormente. Para reconstruir el camino es necesario tener una matriz de padres que se actualiza durante el proceso de relajación. El algoritmo de Floyd se presenta a continuación a manera de seudocódigo:

Floyd-Warshall1. n = numero de vértices2. para k = 1 to n3. para i = 1 to n4. para j = 1 to n

si w(i,j) > w(i,k) + w(k,j)o w(i,j) = w(i,k) + w(k,j)o padre(i,j) = k

Como puede apreciarse del seudocódigo la implementación es directa, a continuación se muestra la implementación del algoritmo para la inicialización, insertar una arista, encontrar las rutas más cortas y para recuperar el camino.//definiciones para el algoritmo#define INFINITO 10000000#define NULO -1

//matrices de pesos y de padresint W[NVERT][NVERT];int Padre[NVERT][NVERT];

//inicializar la matriz de adyacencia y de padresvoid inicializar(){int i, j; for(i = 0; i < NVERT; i++)

Page 47: grafos en codigos completo

for(j = 0; j < NVERT; j++){ Padre[i][j] = NULO; if(i == j) W[i][j] = 0; else W[i][j] = INFINITO; } }

//insertar una arista validando i = jvoid inserta_arista(int i, int j, int w){ if(i == j) W[i][j] = 0; else W[i][j] = w;}

//validacion para suma con infinitoint suma(int x, int y){ if( x == INFINITO || y == INFINITO) return INFINITO; else return x + y;}

//algoritmo que calcula las rutas más cortasvoid floyd(){int i, j, k; //ciclo principal de floyd for(k = 0; k < NVERT; k++) for(i = 0; i < NVERT; i++) for(j = 0; j < NVERT; j++) if( W[i][j] > suma( W[i][k], W[k][j]) ){ W[i][j]=suma( W[i][k], W[k][j]); Padre[i][j] = k; } }

//algoritmo para imprimir la ruta mas cortavoid camino(int origen, int destino){ if( origen == destino){//caso base printf("%d", origen); }else{ if( Padre[origen][destino] == NULO ){//no existe camino printf("No existe camino de %d a %d", origen, destino); }else{ camino(origen, Padre[origen][destino]);//llamada recursiva printf("%d", destino);//imprimir en orden origen, destino } } }

Listado 18 Implementación de Floyd

Como puede verse la implementación del algoritmo es bastante simple y es por ello que se prefiere sobre Dijkstra cuando el tamaño del problema es reducido (NVERT < 200). La razón para no usar siempre Floyd es que su eficiencia es cúbica en el numero de vértices como puede deducirse fácilmente de su implementación. El resultado de la ruta más corta entre i y j se encuentra en W[i][j] si es

Page 48: grafos en codigos completo

que existe dicha ruta, en caso contrario la ruta tendrá un valor de infinito. Algo similar reportaría el algoritmo para imprimir la ruta, al encontrar un padre con valor a nulo en su proceso. Es importante hacer notar que la parte de inicialización es clave para encontrar los resultados correctos y debe hacerse antes de ejecutar el algoritmo y de ingresar las aristas a la matriz. Otra observación importante para lograr una implementación exitosa es el hacer las sumas con infinito de manera correcta.

A continuación se presenta la solución del problema 10171 usando el algoritmo de Floyd. Aquí se usa Floyd dos veces, una sobre la matriz de las calles para mayores y otra para la de menores. La solución es muy similar a la presentada con el algoritmo de Dijkstra, aunque como es de esperarse el tiempo de ejecución es mayor.#include <stdio.h>

//matrices de pesosint grafo_mayores[30][30], grafo_menores[30][30];

//variables a usar en el algoritmoint num_calles, n;int minimos[30];char usuario, dir, ciudad1, ciudad2, sha, mig;int peso;int i, j , k; int min, pos_min;

int main(){

//ciclo principal del problemawhile(1){ //leer el numero de calles en el grafo

scanf("%d\n", &num_calles); if(!num_calles) break;//terminar con cero

//inicializar las matrices for(i = 0; i < 26; i++){ for(j = 0; j < 26; j++) grafo_mayores[i][j] = grafo_menores[i][j] = 15000; grafo_mayores[i][i] = grafo_menores[i][i] = 0; }

//leer las aristas e insertarlas en los grafos n = num_calles; while(n>0){ scanf("%c %c %c %c %d\n", &usuario, &dir, &ciudad1, &ciudad2, &peso);

if(ciudad1 == ciudad2) peso = 0;//validación i = j

Page 49: grafos en codigos completo

if(usuario == 'Y'){ grafo_menores[ciudad1-'A'][ciudad2-'A'] = peso; if(dir == 'B') grafo_menores[ciudad2-'A'][ciudad1-'A'] = peso; }else{ grafo_mayores[ciudad1-'A'][ciudad2-'A'] = peso; if(dir == 'B') grafo_mayores[ciudad2-'A'][ciudad1-'A'] = peso; } n--; }

//floyd sobre el grafo de mayores for(k = 0; k < 26; k++) for(i = 0; i < 26; i++) for(j = 0; j < 26; j++) if(grafo_mayores[i][j]>(grafo_mayores[i][k]+grafo_mayores[k][j])) grafo_mayores[i][j]=grafo_mayores[i][k]+grafo_mayores[k][j]; //floyd sobre el grafo de menores for(k = 0; k < 26; k++) for(i = 0; i < 26; i++) for(j = 0; j < 26; j++) if(grafo_menores[i][j]>(grafo_menores[i][k]+grafo_menores[k][j])) grafo_menores[i][j]=grafo_menores[i][k]+grafo_menores[k][j]; //leer consulta scanf("%c %c\n", &sha, &mig);

//encontrar el menor esfuerzo combinado min = grafo_mayores[mig-'A'][0] + grafo_menores[sha-'A'][0]; minimos[0] = min; for(i = 1; i < 26; i++) if((grafo_mayores[mig-'A'][i] + grafo_menores[sha-'A'][i]) <= min){ min = grafo_mayores[mig-'A'][i] + grafo_menores[sha-'A'][i]; minimos[i] = min; }else{

minimos[i] = 15000; }

//imprimir los resultados if(min >= 15000) printf("You will never meet.\n"); else{ printf("%d", min);

for( i = 0; i < 26; i++) if(minimos[i] == min) printf(" %c", i + 'A');

printf("\n"); } } return 0;}

Listado 19 Solución del problema 10171 con Floyd

El algoritmo de Floyd es muy versátil a pesar de ser muy simple y se usa para encontrar la solución de problemas donde es necesario encontrar el mejor camino basado en restricciones de

Page 50: grafos en codigos completo

carga máxima o de esfuerzo mínimo. En esos casos es necesario modificar el proceso de relajación para sustituirlo por uno que elija en cada iteración la mejor solución en función de las restricciones. A continuación se presentan los algoritmos llamados maxmin y minmax construidos sobre una modificación a Floyd.

Primero revisaremos el problema de encontrar la carga máxima que es posible transportar en una ruta determinada, cuando en cada segmento de la ruta existe una restricción del máximo que se puede transportar por dicho segmento. Analizando con calma el problema es fácil deducir que la máxima carga a transportar por el camino v1, v2, …, vn corresponde al mínimo( w(v1, v2), w(v2, v3), w(vn-1, vn)) y la carga máxima que se puede llevar del nodo v1 al nodo vn corresponde al máximo entre las distintas rutas que pueden formarse en el grafo. De este modo tenemos un problema del tipo maxmin, donde las aristas que no están conectadas en el grafo deberán inicializarse con cero indicando que por ellas se puede transportar una carga de cero. La implementación de maxmin sería como se muestra a continuación://definiciones para el algoritmo#define INFINITO 10000000#define NULO -1

int W[NVERT][NVERT];int Padre[NVERT][NVERT];

//inicializar la matriz de adyacencia y de padresvoid inicializar(){int i, j; for(i = 0; i < NVERT; i++) for(j = 0; j < NVERT; j++){ Padre[i][j] = NULO; W[i][j] = 0; } }

//insertar una arista validando i = jvoid inserta_arista(int i, int j, int w){ if(i == j) W[i][j] = 0; else W[i][j] = w;}

//funciones de soporte para determinar los maximos y minimosint max(int x, int y){ if( x > y) return x; else return y;}

Page 51: grafos en codigos completo

int min(int x, int y){ if( x < y) return x; else return y;}

//algoritmo que calcula la carga maximavoid maxmin(){int i, j, k; //ciclo principal de floyd for(k = 0; k < NVERT; k++) for(i = 0; i < NVERT; i++) for(j = 0; j < NVERT; j++) if( W[i][j] < min( W[i][k], W[k][j]) ){ W[i][j] = min( W[i][k], W[k][j]); Padre[i][j] = k; } //tambien se puede sustituir el if por //W[i][j] = max( W[i][j], min( W[i][k], W[k][j]));}

//algoritmo para imprimir la ruta de la carga máximavoid camino(int origen, int destino){ if( origen == destino){//caso base printf("%d", origen); }else{ if( Padre[origen][destino] == NULO ){//no existe camino printf("No existe camino de %d a %d", origen, destino); }else{ camino(origen, Padre[origen][destino]);//llamada recursiva printf("%d", destino);//imprimir en orden origen, destino } } }

Listado 20 Implementación de maxmin

A continuación se muestra la solución del problema 10099 “The Tourist Guide” del juez de la UVA, donde se usa el algoritmo anterior. Aquí el detalle consiste en considerar que el número máximo de pasajeros disminuye en uno por el lugar que debe ocupar el guía. La solución aceptada por el juez es la siguiente:#include <stdio.h>

using namespace std;

//definiciones para el algoritmo#define NULO -1#define MAXVERT 100

int NVERT;int W[MAXVERT][MAXVERT];int Padre[MAXVERT][MAXVERT];

Page 52: grafos en codigos completo

//inicializar la matriz de adyacencia y de padresvoid inicializar(){int i, j; for(i = 0; i < NVERT; i++) for(j = 0; j < NVERT; j++){ Padre[i][j] = NULO; W[i][j] = 0; } }

//insertar una arista validando i = jvoid inserta_arista(int i, int j, int w){ if(i == j) W[i][j] = 0; else W[i][j] = w;}

//funciones de soporte para determinar los maximos y minimosint max(int x, int y){ if( x > y) return x; else return y;}

int min(int x, int y){ if( x < y) return x; else return y;}

//algoritmo que calcula la carga maximavoid maxmin(){int i, j, k; //ciclo principal de floyd for(k = 0; k < NVERT; k++) for(i = 0; i < NVERT; i++) for(j = 0; j < NVERT; j++) if( W[i][j] < min( W[i][k], W[k][j]) ){ W[i][j] = min( W[i][k], W[k][j]); Padre[i][j] = k; } //tambien se puede sustituir el if por //W[i][j] = max( W[i][j], min( W[i][k], W[k][j]));}

//algoritmo para imprimir la ruta de la carga máximavoid camino(int origen, int destino){ if( origen == destino){//caso base printf("%d", origen); }else{ if( Padre[origen][destino] == NULO ){//no existe camino printf("No existe camino de %d a %d", origen, destino); }else{ camino(origen, Padre[origen][destino]);//llamada recursiva printf("%d", destino);//imprimir en orden origen, destino } } }

int main(){int N, R;int C1, C2, P;int S, D, T;int caso, i;

Page 53: grafos en codigos completo

int maximo, num_viajes;

caso = 1; while(1){ scanf("%d %d\n", &N, &R); if(!N && !R) break; NVERT = N; inicializar(); for(i = 0; i < R; i++){ scanf("%d %d %d\n", &C1, &C2, &P); inserta_arista( C1 - 1, C2 - 1, P); inserta_arista( C2 - 1, C1 - 1, P); } //ejecutar el algoritmo maxmin(); //mostrar los resultados scanf("%d %d %d\n", &S, &D, &T); printf("Scenario #%d\n", caso++);

//restar lugar del guía de turistas maximo = W[S-1][D-1] - 1;

//calcular el numero máximo de viajes num_viajes = T / maximo; if( (T % maximo) != 0 ) num_viajes++;

printf("Minimum Number of Trips = %d\n", num_viajes); printf("\n"); } }

Listado 21 Solución del problema 10099

Ahora pasamos a analizar el problema contrario, supongamos que deseamos minimizar el esfuerzo necesario para completar una tarea a partir de un conjunto de restricciones que nos imponen el esfuerzo requerido en cada etapa. Si analizamos el problema nos damos cuenta que en la secuencia v1, v2, …, vn el esfuerzo máximo que debemos realizar corresponde a max( w(v1, v2), w(v2, v3), …, w(vn-1, vn)). El problema se resuelve eligiendo la ruta o secuencia que minimice dicha cantidad. A este problema se le conoce como distancia mínima y para ponerlo a funcionar es necesario que todas aristas no conectadas en el grafo deberán inicializarse con infinito, indicando que para ellas se requiere un esfuerzo muy grande que no será elegido durante las iteraciones. A continuación se presenta la implementación de dicho algoritmo://definiciones para el algoritmo#define INFINITO 10000000

Page 54: grafos en codigos completo

#define NULO -1

int W[NVERT][NVERT];int Padre[NVERT][NVERT];

//inicializar la matriz de adyacencia y de padresvoid inicializar(){int i, j; for(i = 0; i < NVERT; i++){ for(j = 0; j < NVERT; j++){ Padre[i][j] = NULO; W[i][j] = INFINITO; } W[i][i] = 0; }}

//insertar una arista validando i = jvoid inserta_arista(int i, int j, int w){ if(i == j) W[i][j] = 0; else W[i][j] = w;}

//funciones de soporte para determinar los maximos y minimosint max(int x, int y){ if( x > y) return x; else return y;}

int min(int x, int y){ if( x < y) return x; else return y;}

//algoritmo que calcula la carga minimavoid minmax(){int i, j, k; //ciclo principal de floyd for(k = 0; k < NVERT; k++) for(i = 0; i < NVERT; i++) for(j = 0; j < NVERT; j++) if( W[i][j] > max( W[i][k], W[k][j]) ){ W[i][j] = max( W[i][k], W[k][j]); Padre[i][j] = k; } //tambien se puede sustituir el if por //W[j] = min( W[i][j], max( W[i][k], W[k][j]));}

//algoritmo para imprimir la ruta de la carga máximavoid camino(int origen, int destino){ if( origen == destino){//caso base printf("%d", origen); }else{ if( Padre[origen][destino] == NULO ){//no existe camino

Page 55: grafos en codigos completo

printf("No existe camino de %d a %d", origen, destino); }else{ camino(origen, Padre[origen][destino]);//llamada recursiva printf("%d", destino);//imprimir en orden origen, destino } } }

Listado 22 Implementación de minimax

Nótese que la implementación de minmax es también muy simple y solo cambia el hecho de cómo se interpretan las aristas no conectadas en la matriz de pesos. En este caso las aristas (i,i) siguen siendo cero y las (i,j) que no pertenecen al conjunto de aristas valen infinito. En seguida se muestra la solución del problema 10048 “Audiophobia” de la UVA, usando el algoritmo anterior.#include <stdio.h>

using namespace std;

#define MAXVERT 100

//definiciones para el algoritmo#define INFINITO 10000000#define NULO -1

int NVERT;int W[MAXVERT][MAXVERT];int Padre[MAXVERT][MAXVERT];

//inicializar la matriz de adyacencia y de padresvoid inicializar(){int i, j; for(i = 0; i < NVERT; i++){ for(j = 0; j < NVERT; j++){ Padre[i][j] = NULO; W[i][j] = INFINITO; } W[i][i] = 0; }}

//insertar una arista validando i = jvoid inserta_arista(int i, int j, int w){ if(i == j) W[i][j] = 0; else W[i][j] = w;}

//funciones de soporte para determinar los maximos y minimosint max(int x, int y){ if( x > y) return x; else return y;}

Page 56: grafos en codigos completo

int min(int x, int y){ if( x < y) return x; else return y;}

//algoritmo que calcula la carga minimavoid minmax(){int i, j, k; //ciclo principal de floyd for(k = 0; k < NVERT; k++) for(i = 0; i < NVERT; i++) for(j = 0; j < NVERT; j++) if( W[i][j] > max( W[i][k], W[k][j]) ){ W[i][j] = max( W[i][k], W[k][j]); Padre[i][j] = k; } //tambien se puede sustituir el if por //W[j] = min( W[i][j], max( W[i][k], W[k][j]));}

//algoritmo para imprimir la ruta de la carga máximavoid camino(int origen, int destino){ if( origen == destino){//caso base printf("%d", origen); }else{ if( Padre[origen][destino] == NULO ){//no existe camino printf("No existe camino de %d a %d", origen, destino); }else{ camino(origen, Padre[origen][destino]);//llamada recursiva printf("%d", destino);//imprimir en orden origen, destino } } }

int main(){int caso;int S, C, Q;int c1, c2, d;int minimo;

caso = 1; while(1){ scanf("%d %d %d\n", &C, &S, &Q); if(!C && !S && !Q) break; if(caso>1) printf("\n");

//leer las aristas del grafo NVERT = C; inicializar(); while(S){ scanf("%d %d %d\n", &c1, &c2, &d); inserta_arista(c1 - 1, c2 - 1, d);

Page 57: grafos en codigos completo

inserta_arista(c2 - 1, c1 - 1, d); S--; } //encontrar los minimos minmax(); //imprimir los resultados printf("Case #%d\n", caso++); //leer las consultas while(Q){ //leer la consulta scanf("%d %d\n", &c1, &c2); minimo = W[c1 - 1][ c2 - 1]; //validar la salida if( minimo == INFINITO) printf("no path\n"); else printf("%d\n", minimo); Q--; } } }

Listado 23 Solución del problema 10048

Como complemento al presente material les recomiendo leer la página methods to solve de Steven Halim de la NUS. En los capítulos del libro de Cormen pueden encontrar material adicional acerca de las aplicaciones de los algoritmos de caminos más cortos. En especial resulta interesante el calcular la cerradura transitiva de la matriz de adyacencias usando Floyd. Así mismo se muestra la relación entre el producto de matrices y el algoritmo de Floyd. Otro algoritmo interesante basado en una combinación de Bellman-Ford y dijkstra se muestra como algoritmo de Johnson.

Page 58: grafos en codigos completo

7. Algoritmos de Flujos

Los algoritmos de flujos resuelven el problema de encontrar el flujo máximo de una fuente a un sumidero respetando una serie de restricciones. La primera de ellas que el flujo los flujos se miden como el flujo que sale de un nodo, si así ocurriera el flujo se considera positivo, en caso contrario tenemos un flujo negativo. De esta forma, si el flujo de i a j es positivo entonces el flujo de j a i es negativo. La fuente tiene un flujo neto positivo, el sumidero tiene un flujo neto negativo y los nodos intermedios en los caminos que van de la fuente al sumidero tienen un flujo neto igual a cero. A esta propiedad se le conoce como conservación del flujo y es el equivalente a las leyes de conservación de la materia en física y leyes de Kirchoff en electricidad. Así el flujo neto que sale de la fuente es igual al flujo que entra al sumidero. Para todos los demás nodos el flujo neto debe ser cero, entendiendo como flujo neto a la suma de todos los flujos que entran y salen de un nodo. Por ultimo, ningún flujo debe sobrepasar la capacidad máxima indicada para cada arista en el grafo que representa la red de nodos.

Basado en las propiedades y restricciones anteriores se desarrollo el algoritmo de Ford-Fulkerson cuyo seudocódigo se presenta a continuación:

FORD-FULKERSON( f, s)1. Para cada arista (u, v) en el grafo

f[u][v] = 0 f[v][u] = 0

2. Mientras exista un camino de flujo residual entre f y s incremento = min(cap(u,v) tal que (u,v) está en el camino) para cada arista (u,v) en el camino

o f[u][v] = f[u][v] + incrementoo f[v][u] = -f[u][v]

Page 59: grafos en codigos completo

Para comprender mejor el algoritmo anterior es necesario definir algunos conceptos. Primero decimos que un grafo que representa flujos es un grafo dirigido y ponderado, donde el peso de las aristas representa una capacidad máxima de transportar un flujo. El flujo residual es el flujo disponible en una determinada arista una vez que se ha enviado flujo por ella (en ningún caso el flujo neto residual debe ser mayor a la capacidad de dicha arista ni menor que cero). El flujo residual lo calculamos como la capacidad – flujo_actual, donde flujo_actual es el flujo que ya se ha ocupado en alguna iteración del algoritmo. Un camino de flujo residual es aquel camino de la fuente al sumidero donde todas las aristas en el camino tienen un flujo residual mayor a cero.

El algoritmo comienza por hacer que el flujo actual en todas las aristas del grafo sea igual a cero, en consecuencia el flujo residual será igual a la capacidad de las mismas. El siguiente paso es encontrar un camino de la fuente al sumidero donde todas las aristas incluidas en el camino tengan una capacidad residual mayor a cero. La cantidad máxima de flujo que puede enviarse al sumidero por dicho camino corresponde como es lógico al valor de la capacidad residual mínima en dicho camino. A esta cantidad se le denomina incremento en el flujo, debido a que se suma al flujo actual en todas las aristas en el camino encontrado. La consecuencia inmediata es que el flujo residual se verá modificado y la arista con la menor capacidad estará transportando el flujo máximo (su flujo residual se convertirá en cero) y por lo tanto no deberá ser considerada en la siguiente iteración del algoritmo. Este proceso se repite siempre que pueda encontrarse un nuevo camino de flujo residual (un camino donde todas las aristas tengan un flujo residual mayor a cero). Al final el flujo máximo que puede enviarse de la fuente al sumidero corresponde a la suma de todos los incrementos calculados con cada nuevo camino encontrado.

El algoritmo de Ford-Fulkerson vdepende fuertemente del método que se use para encontrar los caminos de flujo residual y

Page 60: grafos en codigos completo

estos a su vez dependen de la forma en la que se represente el grafo. Por un lado, la representación de matrices hace muy rápido el encontrar el valor de los flujos y las capacidades de cada arista pero hace lento el encontrar los nodos adyacentes y por lo tanto la búsqueda de caminos. Por otro lado, las listas de adyacencias hacen muy rápido el encontrar los nodos adyacentes pero hacen lento el encontrar el valor de los flujos y capacidades. A continuación se presenta una implementación basada en matrices de adyacencia en donde se aprovecha la simplicidad del manejo de dicha estructura de datos.#include <stdio.h>#include <list>

using namespace std;

//definiciones para el algoritmo#define MAXVERT 100#define NULO -1#define INFINITO 100000000

//definición de una estructura para almacenar los flujos actuales y capacidadestypedef struct{ int flujo; int capacidad;}FLUJOS;

//el grafo se almacena como una matrizFLUJOS grafo[MAXVERT][MAXVERT];int nvert, padre[MAXVERT];

//valores iniciales de los flujos antes de insertar aristasvoid inicia_grafo(){int i, j; for(i = 0; i < nvert; i++) for(j = 0; j < nvert; j++) grafo[i][j].capacidad = 0;}

//se considera que puede haber mas de una arista entre cada para de verticesvoid inserta_arista(int origen, int destino, int capacidad){ grafo[origen][destino].capacidad += capacidad;}

//busqueda de caminos residuales, devuelve verdadero al encontrar un caminoint BFS(int fuente, int sumidero){int visitado[MAXVERT], u, v, residual;list<int> cola; //inicializar la busqueda for(u = 0; u < nvert; u++){ padre[u] = NULO; visitado[u] = 0; } cola.clear();

Page 61: grafos en codigos completo

//hacer la busqueda visitado[fuente] = 1; cola.push_back(fuente); //ciclo principal de la busqueda por anchura

while(!cola.empty()){ //saca nodo de la cola

u = cola.front(); cola.pop_front(); for(v = 0; v < nvert; v++){ //elige aristas con flujo residual mayor a cero en el recorrido residual = grafo[u][v].capacidad - grafo[u][v].flujo; if(!visitado[v] && ( residual > 0)){ cola.push_back(v);//mete nodo a la cola padre[v] = u;//guarda a su padre

visitado[u] = 1;//lo marca como visitado } } } //devolver estado del camino al sumidero al terminar el recorrido return visitado[sumidero];}

//algoritmo de ford-fulkersonint ford_fulkerson(int fuente, int sumidero){int i, j , u;int flujomax, incremento, residual; //los flujos a cero antes de iniciar el algoritmo for(i = 0; i < nvert; i++) for(j = 0; j < nvert; j++) grafo[i][j].flujo = 0; flujomax = 0; //mientras existan caminos de flujo residual while(BFS(fuente, sumidero)){ //busca el flujo minimo en el camino de f a s incremento = INFINITO;//inicializa incremento a infinito //busca el flujo residual mínimo en el camino de fuente a sumidero for(u = sumidero; padre[u] != NULO; u = padre[u]){ residual = grafo[padre[u]][u].capacidad- grafo[padre[u]][u].flujo; incremento = min( incremento, residual);

} //actualiza los valores de flujo, flujo máximo y residual en el camino for(u = sumidero; padre[u] != NULO; u = padre[u]){ //actualiza los valores en el sentido de fuente a sumidero grafo[padre[u]][u].flujo += incremento; //hace lo contrario en el sentido de sumidero a fuente grafo[u][padre[u]].flujo -= incremento; } // muestra la ruta for (u=sumidero; padre[u]!=(-1); u=padre[u]) { printf("%d<-",u); } printf("%d añade %d de flujo adicional\n", fuente,incremento); flujomax += incremento; }//al salir del ciclo ya no quedan rutas de incremento de flujo //se devuelve el ciclo maximo return flujomax;}

Page 62: grafos en codigos completo

int main(){int narist;int a, b, c;int fuente, sumidero;int flujo;int i, j;

//leer parametros del grafo scanf("%d %d\n", &nvert, &narist); //inicializar el grafo inicia_grafo(); //leer las aristas while(narist){ //leer arista (a,b) con capacidad c scanf("%d %d %d\n", &a, &b, &c); inserta_arista(a, b, c); narist--; } //leer la consulta scanf("%d %d\n", &fuente, &sumidero); flujo = ford_fulkerson(fuente, sumidero); printf("El flujo maximo entre %d y %d es %d\n", fuente, sumidero, flujo); printf("El flujo entre los vertices quedo asi\n"); for(i = 0; i < nvert; i++) for(j = 0; j < nvert; j++) if( (i != j) && (grafo[i][j].flujo != 0) ) printf("( %d, %d) = %d\n", i, j, grafo[i][j].flujo); return 0;}

Listado 24 Implementación de Ford-Fulkerson

La implementación que se muestra en el listado anterior puede mejorarse si combinan las propiedades de la matriz con las listas, quizá teniendo el grafo almacenado de las dos formas. Aquí debemos considerar que las listas de adyacencia solo sirven para almacenar los nodos adyacentes y se sigue usando la matriz para consultar los valores de los flujos y de las capacidades de cada arista. Debido a que pueden existir más de una arista entre cado par de vértices, resulta útil tener una estructura que almacene datos sin permitir repetidos como el mapa. De este modo, el grafo se almacenaría como sigue:typedef pair<int, int> FLUJOS

vector< map<int, FLUJOS> > grafo;

Page 63: grafos en codigos completo

Usando dichas estructuras definidas en la STL podemos sacar el máximo partido de la eficiencia de las listas de adyacencia con la facilidad de los mapas para localizar datos al tiempo que se puede iterar de manera eficiente sobre ellos. Se deja como ejercicio modificar la implementación para añadir las mejoras sugeridas.

A continuación se muestra la solución del problema 820 “Internet Bandwidth”, en donde se usa el algoritmo de Ford-Fulkerson. En este problema es importante señalar que se trata de un grafo no dirigido y que el ancho de banda se llena sumando el valor absoluto del flujo de datos (es decir, aquí no se cumple cap(u,v) = -cap(v,u)). Esto provoca que la matriz sea simétrica y que el flujo residual sea igual en ambos sentidos. Esto supone una posible mejora si se toma en cuenta la simetría de la matriz de cualquier forma y para hacer más simple de entender, se ha dejado la solución basada en la matriz (el tiempo de ejecución es bastante aceptable 0.110).#include <iostream>#include <list>

using namespace std;

#define MAXVERT 101#define NULO -1

int G[MAXVERT][MAXVERT];int nvert, narist, P[MAXVERT];

int camino(int f, int s){int u, v, visitado[MAXVERT];list<int> cola; //inicializar la busqueda for(u = 1; u <= nvert; u++){ visitado[u] = 0; P[u] = NULO; }

//meter fuente a la cola cola.push_back(f); visitado[f] = 1;

Page 64: grafos en codigos completo

while(!cola.empty()){ //sacar nodo de la cola u = cola.front(); cola.pop_front(); //recorrer los adyacentes a u for(v = 1; v <= nvert; v++){ //si no fue visitado y tiene flujo residual if(!visitado[v] && (G[u][v] > 0)){ cola.push_back(v);//meter adyacente a la cola P[v] = u;//guardar al padre de v visitado[v] = 1;//marcarlo como visitado } } } //si existe camino de f a s, entonces s fue visitado return visitado[s];}

int main(){int u, v, c;int f, s;int flujo, menor;int n; n = 1; while(1){ //leer numero de vertices en el grafo cin >> nvert; if(!nvert) break; //leer fuente, sumidero, numero de aristas cin >> f >> s >> narist; //borrar el grafo for(u = 1; u <= nvert; u++) for(v = 1; v <= nvert; v++) G[u][v] = 0;

//leer las aristas while(narist){ //leer arista (u,v) con capacidad c cin >> u >> v >> c; //inserta aristas (u,v) y (v,u) G[u][v] += c;//capacidad igual en ambos sentidos G[v][u] += c; narist--; }

Page 65: grafos en codigos completo

flujo = 0; //Ford Fulkerson de f a s while(camino(f, s)){ //encuentra la arista de menor peso en el camino menor = G[P[s]][s]; for(v = s; P[v] != NULO; v = P[v]) if(menor > G[P[v]][v]) menor = G[P[v]][v]; //actualizar el flujo residual en el camino for(v = s; P[v] != NULO; v = P[v]){ //actualiza (u,v) y (v,u) G[P[v]][v] -= menor;//flujo residual igual en ambos sentidos G[v][P[v]] -= menor; } //actualiza el valor del flujo neto de f a s flujo += menor; } //imprime los resultados cout << "Network " << n++ << endl; cout << "The bandwidth is " << flujo << "." << endl; cout << endl; } return 0;}

Listado 25 Solución del problema 820

Otro problema interesante que se puede reducir a un problema de flujo máximo es el del aparejamiento bipartito máximo. Para este problema se tiene un grafo bipartido, donde bipartido significa que se pueden identificar en el grafo dos subconjuntos de vértices L y R de tal manera que uno de los extremos de las aristas está en R y el otro L. Las aristas (u,v) donde u y v pertenecen al mismo conjunto no están permitidas.

Este tipo de grafos sirve para resolver problemas como el de asignación de tareas. Si suponemos que el conjunto R representa a un grupo de trabajadores y que L corresponde a un conjunto de tareas, entonces las aristas representan la relación de que un trabajador puede realizar determinada tarea. El problema consiste en asignar la mayor cantidad de tareas para que sean realizadas por los trabajadores.

Page 66: grafos en codigos completo

El planteamiento general del problema es el siguiente, dado un grafo no dirigido G = (V, E), un aparejamiento es un subconjunto M en E tal que para todos los vértices v en V, cuando más una arista de M es incidente en v. Se dice entonces que el vértice v esta aparejado en M si alguna arista en M es incidente en v, de otra forma v no esta aparejado. Un aparejamiento M es máximo si cumple con el requisito de ser de cardinalidad máxima, es decir que para cualquier otro subconjunto M’, se cumple |M| >= |M’|.

En este caso, se restringirá el universo del problema a los grafos bipartidos, de tal forma que V = L U R. De esta forma podemos decir que un vértice pertenece L pero no a R y viceversa, dicho de otro modo los conjunto L y R son disjuntos. En este caso, todas las aristas en E tienen un vértice en L y el otro en R.

Se puede usar el algoritmo de Ford-Fulkerson para resolver dicho problema. Si tenemos el grafo bipartido G = (V, E) deberemos formar a partir de él el grafo de flujos G’ = (V’, E’) de la siguiente manera. Dejemos que V’ sea V U {s,t} donde s es la fuente y t el sumidero. Si la partición de G es L U R, el conjunto de aristas en G’ son las aristas de E dirigidas de L a R, más V nuevas aristas. Expresado en la notación de conjuntos tenemos lo siguiente:

E’ = {(s,u) tal que u esta en L} U {(u,v) tal que u esta en L, v en R y (u,v) en E} U {(v,t) tal que v esta en R}.

Para completar la construcción del grafo de flujos es necesario asignar una capacidad unitaria a cada arista en E’.