Grafos

También podría gustarte

Está en la página 1de 66

GRAFOS

1. Definiciones bsicas: Un grafo es la representacin por medio de conjuntos de relaciones arbitrarias entre objetos. Existen dos tipos de grafos segn la relacin entre los objetos sea unvoca o biunvoca. Los primeros forman los grafos dirigidos o dgrafos y los segundos los grafos no dirigidos o simplemente grafos. En la mayor parte de los algoritmos que sern nuestro objeto de estudio se hace referencia a la termologa bsica que se propone a continuacin. Dicha terminologa; por desgracia, no es estndar y puede llegar a variar en los distintos textos que existen en la materia. Cuando exista ambigedad se harn las aclaraciones segn sea necesario. Un grafo dirigido o dgrafo consiste de un conjunto de vrtices V y un conjunto de arcos A. Los vrtices se denominan nodos o puntos; los arcos tambin se conocen como aristas o lneas dirigidas que representan que entre un par de vrtices existe una relacin unvoca aRb pero no bRa. De modo que los arcos se representan comnmente 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 tambin por medio de una flecha, tal como se muestra en la figura 1.

Figura 1 Grafo dirigido G = V , A} { donde V = { v1 , v 2 , , v n } , A = { a1 , a 2 , , a n } y a =(v


i

, vk )

tal que

v j , vk V

. En dicho grafo se

entiende que (vi , v j ) (v j , vi ) y en muchos casos solo existe uno de los pares de vrtices. Un vrtice que solo tiene arcos saliendo de l se denomina fuente y un vrtice que solo tiene arcos dirigidos hacia l se denomina sumidero. Dicha nomenclatura es importante cuando los dgrafos se usan para resolver problemas de flujos. Un grafo no dirigido, o grafo, al igual que un dgrafo consiste de un conjunto de vrtices V y un conjunto de arcos A. La diferencia consiste en que la existencia de aRb presupone que bRa tambin existe y adems 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 crculos representan los vrtices y las lneas representan los arcos.
a b

Figura 2 Grafo no dirigido donde V = { v1 , v 2 , , v n } , A = { a1 , a 2 , , a n } y ai =(v j , v k ) tal que v , v V . En dicho grafo se entiende que ( v , v ) ( v , v ) y adems (v , v ) = (v , v ) , donde ambos pares de vrtices representan el mismo arco.
j k

G = V , A} {

Existen adems grafos en donde los arcos tienen asociado algn valor en cuyo caso hablamos de grafos ponderados y ahora se representan los arcos como tripletas. Sigue existiendo la informacin de los vrtices unidos por dicho arco adems de la informacin del peso de dicho arco. As pues el arco se representa como a = (v , v , w) donde v , v son el origen y destino y w es el peso respectivamente.
i j
i j

Un nodo b se dice que es adyacente al nodo a si existe el arco (a, b), tmese en cuenta que para un grafo no dirigido necesariamente a es tambin adyacente a b. Esto no ocurre en los grafos dirigidos donde la existencia de (a, b) no implica que (b, a) tambin 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 tambin incide en el nodo a debido a que tambin existe (b, a). El nmero 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. Tambin 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 definirn los conceptos en su momento.

2. Mtodos de representacin 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 ms comunes y la forma de implementarlas. La primera forma es por medio de una matriz de adyacencias, con este mtodo se tiene una matriz de tamao nxn, donde n es el numero de vrtices o nodos en el grafo. Una forma simple de ver la informacin 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 (rengln 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 5

1 0 1 0 0 1

2 1 0 1 1 1

3 0 1 0 1 0

4 0 1 1 0 1

5 1 1 0 1 0

3 5 4

Figura 3 Grafo no ponderado y su matriz de adyacencia Debe notarse que para un grafo no dirigido la matriz de adyacencia es simtrica y que la diagonal principal contiene ceros. Esto puede llegar a aprovecharse para ahorrar tiempo en algunos algoritmos. La representacin por medio de matriz se prefiere para algoritmos donde el numero de arcos es grande en proporcin al numero de vrtices. Si sucediera lo contrario se prefiere la representacin por medio de listas de adyacencia.

2 1 2 2 4

5 5 4 5 1

/ 3 / 3 2 / / 4 /

Figura 4 Lista de adyacencia para el grafo de la figura 3 Las estructuras de datos para las dos formas de representacin anteriores pueden modelarse en C como sigue:
char grafo[MAX_VERT][MAX_VERT], visitado[MAX_VERT];

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 Representacin por matriz de adyacencia Para encontrar los adyacentes al vrtice i se tendra que construir un ciclo que evaluara en el rengln i aquellas columnas que tienen un uno. Como en el siguiente fragmento de cdigo, 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 vrtice j En las implementaciones de algoritmos se darn ms detalles acerca del manejo de las estructuras de datos. Por ahora revisemos la versin 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; }

} 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 Versin 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 insercin y no considerar la existencia de (j, i) para cada (i, j). La implementacin para grafos ponderados por medio de matrices se presenta a continuacin:
#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++) } } int suma_pesos(int x, int y){ if( x == INFINITO || y == INFINITO) return INFINITO; else return x + y; } grafo[i][j] = grafo[j][i] = INFINITO;

Listado 4 Grafos ponderados por medio de matrices

Adicionalmente se muestra una funcin para sumar pesos que permite solucionar el problema de sumar aristas con valor de infinito. Lo cual es muy comn en algoritmos con grafos ponderados. Ahora podemos revisar la versin con listas de adyacencias. Podemos notar que es necesario utilizar un par que guarde el nodo destino adems 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 = w grafo[i].push_back(ady); }

Listado 5 Grafos ponderados con listas de adyacencia En muchos casos es necesario ordenar las aristas de un grafo ponderado de acuerdo a su peso. Ante tal situacin es apropiado definir una estructura que contenga la informacin 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 expansin de los recorridos de un grafo o se encuentran los rboles de expansin mnima.
typedef pair< int , int > ARISTA priority_queue< int, ARISTA> cola;

Listado 6 Definicin 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

construir algoritmos sobre grafos, entre ellas estn las de conjuntos disjuntos que se discutirn ms adelante.

3. Algoritmos bsicos de bsqueda Existen dos tcnicas bsicas para recorrer los vrtices de un grafo, la bsqueda por profundidad (DFS) y la bsqueda por anchura (BFS). La bsqueda por profundidad se usa cuando queremos probar si una solucin 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 bsqueda por anchura se usa para aquellos algoritmos en donde resulta critico elegir el mejor camino posible en cada momento como sucede en dijkstra. A continuacin se muestra el algoritmo de la bsqueda 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 bsqueda. El siguiente listado contiene la implementacin del recorrido por anchura para un grafo completamente conectado (existe al menos un camino entre cualquier par de vrtices en el grafo) y para un grafo que no lo esta.
//algoritmo para grafo completamente conectado void BFS(int v){//v es el nodo de inicio del recorrido list<int> cola;//cola de adyacentes list<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]){//aadir a la cola solo los no visitados visitado[*aux] = 1;//marcarlos como visitados cola.push_back(*aux);//aadirlos a la cola //aqui podriamos aadir codigo para hacer algo mientras //recorremos el grafo } aux++;//avanzar al siguiente adyacente del nodo actual } } }

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

Listado 7 BFS o recorrido por anchura Para que el cdigo 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 aade los vrtices recin visitados a la cola puede aadirse cdigo para hacer algo mientras se recorre el grafo, un ejemplo de esto ltimo lo pueden encontrar en mi solucin 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 cdigo lo pueden consultar en el editor de la pgina de entrenamiento de la UTM, navegando hasta la carpeta jorge/10009. El algoritmo de la bsqueda 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 continuacin se muestran ambos enfoques as como la rutina para hacer la bsqueda en grafos que no estn completamente conectados. A continuacin se presenta el listado con la implementacin de la bsqueda por profundidad o DFS. Tambin se ha aadido en la versin 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.

void DFS(int v){//v es el nodo de inicio del recorrido list<int> pila;//pila de nodos adyacentes list<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]){//aadir a la pila solo los no visitados visitado[*aux] = 1; pila.push_back(*aux); //aqui podemos aadir cdigo para hacer algo mientras //realizamos el recorrido } aux++;//avanzar al siguiente adyacente del nodo actual } } } //esta seria la versin recursiva del algoritmo anterior en cuyo caso no se necesita la pila void 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 conectados void 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 ms corto entre cualquier par de vrtices y es por ello que forman la

base de dichos algoritmos. El recorrido por profundidad sirve por otro lado para averiguar si un par de grafos estn conectados. A continuacin 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 lnea que guarde en un arreglo de padres cuando se meten los vrtices 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 aadir una lnea como padre[nodo_actual] = *aux; dentro del ciclo que aade los nodos no visitados a la pila o cola. Se muestra como ejemplo la solucin del problema 10009, cuyo listado se incluye a continuacin.
#include<stdio.h> #include<list> #include<vector> using namespace std; int padre[26], visitado[26];//auxiliares para el recorrido vector< list<int> > grafo(26);//almacenar el grafo char c1[256], c2[256];//ciudades a explorar int casos, aristas, consultas; //funciones de soporte para manejar cada consulta y cada caso //antes de cada caso se debe borrar el grafo void borra_grafo(){//limpia las aristas del grafo para cada caso nuevo int 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 visitados void inicializa_busqueda(){//en cada consulta se inicializan los valores int 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 } }

//funcion de bsqueda por anchura que almacena el padre de cada nodo durante el //recorrido void BFS(int v){//recorre el grafo por anchura a partir de v list<int> cola;//cola de adyacentes list<int>::iterator aux, fin;//iteradores para recorrer adyacentes del //nodo actual int 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 } } }

//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 consultas fflush(stdin); borra_grafo();//limpiar el grafo antes de leer las aristas while(aristas > 0){//leer las aristas y almacenarlas en el grafo scanf("%s %s\n", c1, c2);//leer arista como ciudad1 ciudad2 fflush(stdin); nodo1 = c1[0] - 'A';//encontrar posicion en la lista de nodo2 = c2[0] - 'A';//adyacentes en el grafo grafo[nodo1].push_back(nodo2);//mete la arista en grafo no grafo[nodo2].push_back(nodo1);//dirigido aristas--;//actualizar numero de aristas por leer } while(consultas > 0){//leer las consultas scanf("%s %s\n", c1, c2);//leer origen y destino de //la consulta fflush(stdin); nodo1 = c1[0] - 'A';//encontrar posiciones en la lista de nodo2 = 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 resolver if(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 tambin puede ser resuelto si se sustituye la bsqueda por anchura usando

una bsqueda por profundidad porque las condiciones del problema hacen que los rboles resultantes sean idnticos. Las bsquedas 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 articulacin. En el siguiente apartado se mostrarn algoritmos basados en alguna variacin de los recorridos y se pedir hacer referencia a esta seccin con el fin de enfocarse en la tcnica nueva sin explicar nuevamente el esquema general del recorrido usado.

4. Teorema de los parntesis 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 informacin es posible hacer una clasificacin 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 an. 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 comnmente en los grafos dirigidos. El teorema de los parntesis dice que los tiempos de inicio y finalizacin de la visita de un nodo y sus adyacentes forman una estructura de parntesis perfectamente anidados. Esta estructura de parntesis representa de alguna forma el rbol de recorrido, aunque tambin puede representar un bosque. Dicha informacin se usa para algoritmos como el de los componentes conexos y los puntos de articulacin. Otra aplicacin resulta de la ordenacin topolgica de los vrtices en el grafo. Ambos algoritmos se presentan a continuacin en forma de seudocdigo y luego se sugiere una forma de implementarlos. El algoritmo del ordenamiento topolgico 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 planeacin y por lo regular se aplican en el manejo de proyectos de diversa ndole

como pueden ser inversiones, manufactura y construccin. Simplemente el vestirse requiere que se siga una secuencia apropiada; por ejemplo, nadie se pone los calcetines despus de ponerse los zapatos. ORDENAMIENTO_TOPOLOGICO(G) DFS(G) y calcular f[v] para cada v en G Conforme se termina de visitar un vrtice insertar al frente de una lista Devolver la lista como resultado Los tiempos de finalizacin y de inicio de visita de un vrtice 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 continuacin a manera de seudocdigo. DFS(G) Para cada nodo u en G o color[u] = blanco o 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 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, recin 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 informacin 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 informacin de los recorridos tal como se muestran aqu fue tomada del capitulo 22 del libro de Cormen 2da edicin. La implementacin de los algoritmos anteriores, usando c y la STL, se muestra a continuacin.
#include<stdio.h> #include<vector> #include<list> using namespace std; vector< list<int> > grafo(10);//representacion del grafo int 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 int main(){ //variables para capturar el grafo int 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 datos void 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 //finalizacin, para la demostracin del teorema de los parentesis void DFS_VISIT(int u){ //iteradores para manejar la lista de adyacentes a u list<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 finalizacin 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.in arreglo d d[0] = 1, d[1] = 2, d[2] = 9, d[3] = 4, d[4] = 3, d[5] = 10,

arreglo f f[0] = 8, f[1] = 7, f[2] = 12, f[3] = 5, f[4] = 6, f[5] = 11,

Si ordenamos los tiempos de descubrimiento y finalizacin, escribiendo los nmeros de nodos, tendremos la siguiente secuencia que ilustra el teorema de los parntesis. (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 implementacin del ordenamiento topolgico, tal como aparece en seudocdigo mostrado anteriormente. Se muestran nicamente las modificaciones que es necesario hacer al DFS_VISIT, para almacenar la lista con el orden de finalizacin.
//cdigo modificado para almacenar el orden de finalizacin requerido para el //odenamiento topologico void DFS_VISIT(int u){ //iteradores para manejar la lista de adyacentes a u list<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 vrtice 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 resultados for( aux = orden.begin(); aux != orden.end(); aux++) printf(%d, , *aux); printf(\n); }

Listado 11 Modificaciones a DFS para ordenamiento topolgico

Para que el cdigo anterior funcione, se necesita que la lista que almacenar el ordenamiento sea declarada de manera global. En el cdigo anterior la variable tipo lista de enteros se llama orden. Cuando se prueba con el grafo de la figura 22.7 de la segunda edicin del cormen, el resultado es el siguiente:
[jorge@localhost ~]$ ./ord_topo<ord_topo.in 8, 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

An 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 razn por la que se explica la diferencia es el orden en el que se enumeran los nodos y luego se visitan. A continuacin se lista el seudocdigo del algoritmo que encuentra los componentes fuertemente conectados del grafo. Por definicin un componente fuertemente conectado de un grafo es un subgrafo C en el que para cada par de vrtices 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 vrtices 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 G 2. encontrar T 3. llamar DFS(T), pero en el ciclo principal de DFS, los vrtices se exploran en orden decreciente del f[u] calculado en el paso 1 4. Sacar los vrtices de cada rbol generado en el paso 3 como un componente fuertemente conexo por separado.

A continuacin 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 topolgico de los vrtices en G. Por lo que el paso 1 se puede sustituir por obtener el ordenamiento topolgico de G. Y en el paso 3 diramos que se recorre el grafo en profundidad usando el ordenamiento topolgico calculado en el paso 1. Tambin se incluye una implementacin 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 calcula void 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 T void 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 impresin de los elementos de cada SCC void DFS_VISIT2(int u){ //iteradores para manejar la lista de adyacentes a u list<int>::iterator v, fin; color[u] = GRIS; tiempo++; d[u] = tiempo; 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 cdigo anterior se deben declarar los grafos G y T como variables globales. Se deja como ejercicio hacer la implementacin completa y probar con el grafo de la figura 22.10 del libro de cormen. Adicionalmente al cdigo anterior, es posible realizar una implementacin que localice los puntos de articulacin 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 anlisis of computer algorithms. Se transcribe a continuacin en el siguiente seudocdigo:
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{ aadir (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 lnea donde se encontr un componente, se puede vaciar la lista T de los vrtices v que forman parte de un componente o imprimir los vrtices v que corresponden a los puntos de articulacin en el grafo, con excepcin de la raz del rbol de bsqueda. La implementacin del algoritmo anterior se lista a continuacin:
#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){//implementacin del algoritmo de busqueda de puntos list<int>::iterator aux, fin;//de articulacin pair<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); 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 Implementacin de puntos de articulacin En el cdigo anterior hace falta validar que la raz del rbol de bsqueda en profundidad no siempre es un punto de articulacin. La raz siempre tiene un dfs_number igual a cero y es por ello que aparece como punto de articulacin. Es de notarse que todos los miembros, a excepcin del punto de articulacin, de un componente tienen el mismo valor de LOW.

5. rboles de expansin mnima En ocasiones se presenta el problema de elegir uno de varios rboles de expansin que cumplan con el requisito de que la suma total del peso de sus vrtices sea la mnima posible. Este es un problema de optimizacin 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 mnimo y que adems permita que todas estn conectadas. Existen una serie de algoritmos basados en una tcnica de programacin 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 eleccin de las aristas de menor peso que no forman ciclos, para poder elegir dichas aristas es necesario usar un mtodo de almacenamiento que las ordene de menor a mayor peso. Dado que dicho mtodo 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 implementacin hace uso de la estructura UNION-FIND recomendada por el libro de cormen y que aparece dentro de su propio apartado dentro de la seccin estructuras del temario. El seudocdigo del Kruskal se muestra a continuacin: MST-KRUSKAL(G,w) 1. A es el conjunto vaco 2. para cada vrtice v en G make-set(v) 3. ordenar las aristas de menor a mayor peso 4. 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

Las operaciones en negritas corresponden a la implementacin de UNION-FIND. El resultado del algoritmo es el rbol de expansin representado por el conjunto de aristas incluidas en A. A continuacin se muestra una implementacin basada en la STL. Esta implementacin 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; //implementacin de UNION-FIND #define MAX 1000 // ajustarlo apropiadamente (tamao mximo 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)); }

//definiciones para usar en el algorimo de Kruskal #define ARISTA pair<int, int> #define ARISTA_PONDERADA pair<int, ARISTA> int nvert, narist; //representacin del grafo con un vector de aristas vector<ARISTA_PONDERADA> G(14), A(14);//14 aristas para la prueba //algoritmo de kruskal void kruskal(){ ARISTA a; int i, j;//contadores de aristas y vertices int 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;//contadores int u, v, w;//datos de las aristas ARISTA a;//arista ARISTA_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();

//se imprimen los resultados printf("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 Implementacin de Kruskal A continuacin se presenta el uso del algoritmo de Kruskal para la solucin del problema 10397 Connect the Campus del juez en lnea 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 precisin, las distancias se almacenan como enteros y luego se calcula la raiz cuadrada. Tambin se sobrecargo el operador de comparacin para que hiciera correctamente las comparaciones de los pesos de las aristas. La solucin aceptada es la siguiente:
#include<stdio.h> #include<math.h> #include<vector> #include<algorithm> using namespace std; //implementacin de UNION-FIND #define MAX 1000 // ajustarlo apropiadamente (tamao mximo 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)

{ 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; //representacin del grafo con un vector de aristas vector<ARISTA_PONDERADA> G; //sobrecarga del operador menor para comparar class LessWeightedEdge{ public: bool operator()(const ARISTA_PONDERADA &p, const ARISTA_PONDERADA &q) const{ return (p.first < q.first); } }; //algoritmo de kruskal void kruskal(){ ARISTA a; int i;//contadores de aristas y vertices int 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

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

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

Listado 14 Solucin del problema 10397 Ahora se presenta el algoritmo de Prim para encontrar el rbol de expansin mnima. Aqu la diferencia con el algoritmo anterior es que solo se mantienen dos conjuntos, el de los vrtices incluidos en el rbol y el de los que no lo estn. El procedimiento consiste en elegir la arista de menor peso que une un vrtice en el conjunto del rbol con un vrtice que no esta en el rbol. El seudocdigo se presenta a continuacin: PRIM(G, r) 1. para cada vrtice u en G clave[u] = infinito padre[u] = NULO 2. clave[r] = 0 3. Meter los vrtices u de G a una cola de prioridad Q con clave[u] 4. Mientras no este vaca Q Extraer un vrtice de Q y llamarlo u Para cada vrtice 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 continuacin se propone una implementacin basada en la cola de prioridad de la STL y usando un algoritmo similar al de la bsqueda 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 vrtice 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 meter los pesos como nmeros negativos. Tambin 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 implementacin es tener un contador que revise que todos los vrtices fueron visitados y hacer que el ciclo principal termine antes. La implementacin del algoritmo anterior se presenta a continuacin:
#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 aadida ap.first = (*aux).first*(-1); 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 Implementacin de Prim El problema 10397 tambin puede resolverse usando el algoritmo de Prim si se ponen a cero los pesos de los caminos ya construidos, esto se hara despus del cdigo que construye el grafo. Para ello sera necesario hacer bsquedas en la lista de adyacencias o usar una representacin por medio de matrices de adyacencias. La matriz de adyacencias se justifica aqu debido a que el grafo es muy denso. La solucin por este medio se deja como ejercicio. Es tambin claro que para encontrar el peso total del rbol de expansin

mnima solo se tienen que sumar todas las entradas del arreglo de claves. 6. Algoritmos para las rutas ms cortas En este apartado se revisarn los algoritmos de las rutas ms cortas de Dijkstra y Floyd, por ser los ms conocidos y tiles para resolver los problemas de la ACM. Otros algoritmos como el de Bellman-Ford y el de Warshall solo se mencionarn a manera de seudocdigo. Todos los algoritmos de esta seccin usan la desigualdad del tringulo, 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 inicializacin y para determinar una mejor ruta. Dichos algoritmos se listan a continuacin a manera de seudocdigo: Inicializacin(G, s) 1. para cada vrtice 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 continuacin 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 funcionando a pesar de encontrar aristas con pesos negativos con la condicin 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 FALSO 4. 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 ms cortas sern almacenadas en el arreglo d y los caminos de s al resto de los vrtices puede encontrarse por medio del algoritmo recursivo de caminos que se estudio en el apartado 3 Algoritmos bsicos de bsqueda y que se transcribe a continuacin a manera de seudocdigo: Camino(u,v) 1. si u = v imprime u 2. en caso contrario si padre[v] = NULO no existe camino de u a v 3. 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 implementacin del algoritmo de Bellman-Ford se deja como ejercicio.

A continuacin se presenta el seudocdigo de un algoritmo eficiente para trabajar con grafos dirigidos acclicos o dags. Este algoritmo aprovecha que no existen ciclos en el grafo para proveer de un algoritmo de eficiencia lineal. Se hace uso del ordenamiento topolgico como parte del preprocesamiento del grafo. A continuacin se presenta el seudocdigo de las rutas ms cortas en un dag. Rutas-cortas-dags(G, s) 1. Ordenar topolgicamente G 2. Inicializacin(G,s) 3. para cada vrtice u, ordenado topolgicamente para cada vrtice v que es adyacente a u Relajamiento(u,v) Una aplicacin importante del algoritmo anterior es para construir el anlisis temporal de proyectos usando PERT. La ruta ms larga ofrecida por el algoritmo anterior corresponde a la ruta crtica que trata de reducirse usando PERT. Ahora es el momento de analizar con detalle uno de los algoritmos clsicos para encontrar las rutas ms cortas. Se trata del algoritmo de Dijkstra, el cual por medio de una tcnica vida actualiza un vector de padres y de uno de distancias mnimas. Las rutas pueden encontrarse con el algoritmo recursivo de caminos que se describi en prrafos anteriores. A continuacin se lista el seudocdigo del algoritmo de Dijkstra. DIJKSTRA(G,s) 1. Inicializacin(G,s) 2. S es el conjunto vaco 3. Meter los vrtices u a una cola de prioridad Q de acuerdo a d[u] 4. mientras Q no este vaca extraer el minimo de Q en u S = S union {u}

para cada v adyacente a u relajamiento(u,v) La implementacin del algoritmo de Dijkstra es muy similar a la del algoritmo de Prim. El conjunto S representa a los vrtices ya visitados por el algoritmo y que por tanto no sern 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 continuacin se presenta una implementacin basada en las colas de prioridad de la STL, ntese que es casi idntica a la implementacin 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; //implementacin del algoritmo de dijkstra void dijkstra(int s){//nodo de origen s 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; //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;//recuperamos vertice u visitado[u] = 1;//aadir 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 ms 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 dijkstra 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 dirigido por lo se inserta solo en una direccin 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 Implementacin de Dijkstra El algoritmo anterior se prob con el grafo de la figura 24.6 del cormen. Una vez ms 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 solucin 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 ms cortas en el grafo de las personas mayores y luego las rutas ms 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 cdigo fue necesario usar parmetros para la funcin dijsktra, ntese que los grafos se pasan por referencia para no generar copias. El cdigo de la solucin aceptada se muestra a continuacin.
#include #include #include #include <stdio.h> <queue> <vector> <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 mayores vector< 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 padres void 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; //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;//aadir 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 ms 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 informacin del caso anterior for(i = 0; i < 26; i++){ GMenores[i].clear(); GMayores[i].clear(); }

//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 Solucin del problema 10171

La solucin del listado anterior puede mejorarse en tiempo si hacemos que el ciclo principal de Dijkstra termine cuando todos los nodos estn marcados como visitados, para ello basta con llevar un contador y encontrar la forma de contar el nmero de vrtices. En el caso de este problema se podra llevar un arreglo de banderas que se ponga a 1 cada vez que un vrtice se aade al grafo y luego se contaran los 1s para saber cuantos vrtices existen en el grafo. Las banderas tendran que inicializarse a 0. El tiempo an 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 ms grandes. Ahora se presentan los algoritmos que encuentran las distancias ms cortas entre todos los pares de vrtices. An cuando esto puede conseguirse con un ciclo que vare el vrtice de origen en Dijsktra, por lo general se usa el algoritmo de Floyd por tener una implementacin ms simple como se ver a continuacin. El algoritmo basa su funcionamiento en una tcnica de programacin dinmica que almacena al paso de cada iteracin 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 tringulo y para que el algoritmo funcione es necesario hacer una inicializacin con los siguientes valores: 0 Wij = el peso de la arista dirigida (i,j)

si 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 Bellman-Ford. El mtodo consiste en hacer iteraciones del proceso de relajacin para cada arista en el grafo, tomando en cada paso un vrtice intermedio diferente. La manera ms 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 mtodos presentados con anterioridad es posible reconstruir el camino ms 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 relajacin. El algoritmo de Floyd se presenta a continuacin a manera de seudocdigo: Floyd-Warshall 1. n = numero de vrtices 2. para k = 1 to n 3. para i = 1 to n 4. 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 seudocdigo la implementacin es directa, a continuacin se muestra la implementacin del algoritmo para la inicializacin, insertar una arista, encontrar las rutas ms cortas y para recuperar el camino.
//definiciones para el algoritmo #define INFINITO 10000000 #define NULO -1 //matrices de pesos y de padres int W[NVERT][NVERT]; int Padre[NVERT][NVERT];

//inicializar la matriz de adyacencia y de padres void inicializar(){ int i, j; for(i = 0; i < NVERT; i++) 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 = j void inserta_arista(int i, int j, int w){ if(i == j) W[i][j] = 0; else W[i][j] = w; } //validacion para suma con infinito int suma(int x, int y){ if( x == INFINITO || y == INFINITO) return INFINITO; else return x + y; } //algoritmo que calcula las rutas ms cortas void 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 corta void 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 Implementacin de Floyd Como puede verse la implementacin del algoritmo es bastante simple y es por ello que se prefiere sobre Dijkstra cuando el tamao del problema es reducido (NVERT < 200). La razn para no usar

siempre Floyd es que su eficiencia es cbica en el numero de vrtices como puede deducirse fcilmente de su implementacin. El resultado de la ruta ms corta entre i y j se encuentra en W[i][j] si es que existe dicha ruta, en caso contrario la ruta tendr un valor de infinito. Algo similar reportara 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 inicializacin es clave para encontrar los resultados correctos y debe hacerse antes de ejecutar el algoritmo y de ingresar las aristas a la matriz. Otra observacin importante para lograr una implementacin exitosa es el hacer las sumas con infinito de manera correcta. A continuacin se presenta la solucin 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 solucin es muy similar a la presentada con el algoritmo de Dijkstra, aunque como es de esperarse el tiempo de ejecucin es mayor.
#include <stdio.h> //matrices de pesos int grafo_mayores[30][30], grafo_menores[30][30]; //variables a usar en el algoritmo int 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 problema while(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;//validacin i = j 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 Solucin del problema 10171 con Floyd

El algoritmo de Floyd es muy verstil a pesar de ser muy simple y se usa para encontrar la solucin de problemas donde es necesario encontrar el mejor camino basado en restricciones de carga mxima o de esfuerzo mnimo. En esos casos es necesario modificar el proceso de relajacin para sustituirlo por uno que elija en cada iteracin la mejor solucin en funcin de las restricciones. A continuacin se presentan los algoritmos llamados maxmin y minmax construidos sobre una modificacin a Floyd. Primero revisaremos el problema de encontrar la carga mxima que es posible transportar en una ruta determinada, cuando en cada segmento de la ruta existe una restriccin del mximo que se puede transportar por dicho segmento. Analizando con calma el problema es fcil deducir que la mxima carga a transportar por el camino v1, v2, , vn corresponde al mnimo( w(v1, v2), w(v2, v3), w(vn-1, vn)) y la carga mxima que se puede llevar del nodo v1 al nodo vn corresponde al mximo entre las distintas rutas que pueden formarse en el grafo. De este modo tenemos un problema del tipo maxmin, donde las aristas que no estn conectadas en el grafo debern inicializarse con cero indicando que por ellas se puede transportar una carga de cero. La implementacin de maxmin sera como se muestra a continuacin:
//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 padres void 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 = j void 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 minimos int 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 maxima void 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 mxima void 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 Implementacin de maxmin A continuacin se muestra la solucin 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 nmero mximo de pasajeros disminuye en uno por el lugar que debe ocupar el gua. La solucin 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]; //inicializar la matriz de adyacencia y de padres void 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 = j void 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 minimos int 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 maxima void 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 mxima void 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 int int int int int

main(){ N, R; C1, C2, P; S, D, T; caso, i; 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 gua de turistas maximo = W[S-1][D-1] - 1; //calcular el numero mximo 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 Solucin 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 mximo 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 mnima y para ponerlo a funcionar es necesario que todas aristas no conectadas en el grafo debern inicializarse con infinito, indicando que para ellas se requiere un esfuerzo muy grande que no

ser elegido durante las iteraciones. A continuacin se presenta la implementacin de dicho algoritmo:
//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 padres void 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 = j void 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 minimos int 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 minima void 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 mxima void 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 22 Implementacin de minimax Ntese que la implementacin de minmax es tambin muy simple y solo cambia el hecho de cmo 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 solucin 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 padres void 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 = j void 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 minimos int 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 minima void 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 mxima void 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 int int int int

main(){ caso; S, C, Q; c1, c2, d; 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); 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 Solucin del problema 10048 Como complemento al presente material les recomiendo leer la pgina methods to solve de Steven Halim de la NUS. En los captulos del libro de Cormen pueden encontrar material adicional acerca de las aplicaciones de los algoritmos de caminos ms cortos. En especial resulta interesante el calcular la cerradura transitiva de la matriz de adyacencias usando Floyd. As mismo se muestra la relacin entre el producto de matrices y el algoritmo de Floyd. Otro algoritmo interesante basado en una combinacin de Bellman-Ford y dijkstra se muestra como algoritmo de Johnson.

7. Algoritmos de Flujos Los algoritmos de flujos resuelven el problema de encontrar el flujo mximo 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 conservacin del flujo y es el equivalente a las leyes de conservacin de la materia en fsica 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 dems 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, ningn flujo debe sobrepasar la capacidad mxima 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 seudocdigo se presenta a continuacin: 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] + incremento o f[v][u] = -f[u][v]

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 mxima 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 ningn 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 iteracin 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 mxima de flujo que puede enviarse al sumidero por dicho camino corresponde como es lgico al valor de la capacidad residual mnima 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 mximo (su flujo residual se convertir en cero) y por lo tanto no deber ser considerada en la siguiente iteracin 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 mximo 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 mtodo que se use para encontrar los caminos de flujo residual y

estos a su vez dependen de la forma en la que se represente el grafo. Por un lado, la representacin de matrices hace muy rpido 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 bsqueda de caminos. Por otro lado, las listas de adyacencias hacen muy rpido el encontrar los nodos adyacentes pero hacen lento el encontrar el valor de los flujos y capacidades. A continuacin se presenta una implementacin 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 //definicin de una estructura para almacenar los flujos actuales y capacidades typedef struct{ int flujo; int capacidad; }FLUJOS; //el grafo se almacena como una matriz FLUJOS grafo[MAXVERT][MAXVERT]; int nvert, padre[MAXVERT]; //valores iniciales de los flujos antes de insertar aristas void 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 vertices void inserta_arista(int origen, int destino, int capacidad){ grafo[origen][destino].capacidad += capacidad; } //busqueda de caminos residuales, devuelve verdadero al encontrar un camino int 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();

//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-fulkerson int 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 mnimo 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 mximo 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 aade %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; }

int int int int int int

main(){ narist; a, b, c; fuente, sumidero; flujo; 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 Implementacin de Ford-Fulkerson La implementacin 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 ms de una arista entre cado par de vrtices, resulta til tener una estructura que almacene datos sin permitir repetidos como el mapa. De este modo, el grafo se almacenara como sigue:
typedef pair<int, int> FLUJOS vector< map<int, FLUJOS> > grafo;

Usando dichas estructuras definidas en la STL podemos sacar el mximo 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 implementacin para aadir las mejoras sugeridas. A continuacin se muestra la solucin del problema 820 Internet Bandwidth, en donde se usa el algoritmo de FordFulkerson. En este problema es importante sealar 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 simtrica y que el flujo residual sea igual en ambos sentidos. Esto supone una posible mejora si se toma en cuenta la simetra de la matriz de cualquier forma y para hacer ms simple de entender, se ha dejado la solucin basada en la matriz (el tiempo de ejecucin 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;

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 int int int int

main(){ u, v, c; f, s; flujo, menor; 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--; }

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 Solucin del problema 820 Otro problema interesante que se puede reducir a un problema de flujo mximo es el del aparejamiento bipartito mximo. Para este problema se tiene un grafo bipartido, donde bipartido significa que se pueden identificar en el grafo dos subconjuntos de vrtices 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 estn permitidas. Este tipo de grafos sirve para resolver problemas como el de asignacin 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 relacin 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.

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 vrtices v en V, cuando ms una arista de M es incidente en v. Se dice entonces que el vrtice 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 mximo si cumple con el requisito de ser de cardinalidad mxima, 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 vrtice 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 vrtice 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 particin de G es L U R, el conjunto de aristas en G son las aristas de E dirigidas de L a R, ms V nuevas aristas. Expresado en la notacin 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 construccin del grafo de flujos es necesario asignar una capacidad unitaria a cada arista en E.

También podría gustarte