You are on page 1of 66

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.

a b

Figura 1 Grafo
dirigido
G ={V , A} donde
V = { v1 , v 2 , , v n } ,
A = { a1 , a 2 ,  , a n } y a =(v
i j , vk ) tal que v j , v k ∈V . En dicho grafo se
entiende que (vi , v j ) ≠ (v j , vi ) y en muchos casos solo existe uno de los
pares de vértices.
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.

a b

Figura 2 Grafo no
dirigido

G ={V , A} 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
j k

dicho grafo se entiende que ( v , v ) ⇔ ( v , v ) y además (v , v ) = (v , v ) ,


i j j i i j j i

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 a = (v , v , w) i j

donde v , v son el origen y destino y w es el peso respectivamente.


i j
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
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.
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 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
1 2

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

2 5 /

1 5 3 4 /

2 4 /

2 5 3 /

4 1 2 /

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];
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 j
En 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;
}
}

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


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 = 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 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 > ARISTA
priority_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
construir algoritmos sobre grafos, entre ellas están las de conjuntos
disjuntos que se discutirán más adelante.
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 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]){//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
}
}
}
//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 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.
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]){//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 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
más corto entre cualquier par de vértices 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 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 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 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 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 también
puede ser resuelto si se sustituye la búsqueda por anchura usando
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.
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
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] = 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, 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 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
//finalización, para la demostración 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 finalización de cada nodo en el grafo.

0 1 2

3 4 5

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 finalización,


escribiendo los números de nodos, tendremos la siguiente secuencia
que ilustra el teorema de los paréntesis.
(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 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 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 resultados


for( 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.
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.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

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 G
2. encontrar T
3. 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
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 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 impresión 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 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
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ón
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 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.
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ío
2. para cada vértice 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 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));
}
//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 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 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)


{
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 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 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] = 0
3. 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
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);
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.
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
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 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 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 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
implementación del algoritmo de Bellman-Ford se deja como
ejercicio.
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 G
2. 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ío
3. 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)
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 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;//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 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 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

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 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;//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();
}
//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
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
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-Warshall
1. n = numero de vértices
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 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 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 más 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 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
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 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;//validación 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 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
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 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 máxima


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 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];

//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 máxima


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 main(){
int N, R;
int C1, C2, P;
int S, D, T;
int caso, i;
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
#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 máxima
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 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 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 máxima


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 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);
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.
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] + 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 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
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 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 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;
}
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;


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;
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--;
}
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.
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’.

You might also like