Está en la página 1de 7

Definicin Un grafo es un objeto matemtico que se utiliza para representar circuitos, redes, etc.

Los grafos son muy utilizados en computacin, ya que permiten resolver problemas muy complejos. Imaginemos que disponemos de una serie de ciudades y de carreteras que las unen. De cada ciudad saldrn varias carreteras, por lo que para ir de una ciudad a otra se podrn tomar diversos caminos. Cada carretera tendr un coste asociado (por ejemplo, la longitud de la misma). Gracias a la representacin por grafos podremos elegir el camino ms corto que conecta dos ciudades, determinar si es posible llegar de una ciudad a otra, si desde cualquier ciudad existe un camino que llegue a cualquier otra, etc. El estudio de grafos es una rama de la algoritmia muy importante. Estudiaremos primero sus rasgos generales y sus recorridos fundamentales, para tener una buena base que permita comprender los algoritmos que se pueden aplicar.

Glosario Un grafo consta de vrtices (o nodos) y aristas. Los vrtices son objetos que contienen informacin y las aristas son conexiones entre vrtices. Para representarlos, se suelen utilizar puntos para los vrtices y lneas para las conexiones, aunque hay que recordar siempre que la definicin de un grafo no depende de su representacin. Un camino entre dos vrtices es una lista de vrtices en la que dos elementos sucesivos estn conectados por una arista del grafo. As, el camino AJLOE es un camino que comienza en el vrtice A y pasa por los vrtices J,L y O (en ese orden) y al final va del O al E. El grafo ser conexo si existe un camino desde cualquier nodo del grafo hasta cualquier otro. Si no es conexo constar de varias componentes conexas. Un camino simple es un camino desde un nodo a otro en el que ningn nodo se repite (no se pasa dos veces). Si el camino simple tiene como primer y ltimo elemento al mismo nodo se denomina ciclo. Cuando el grafo no tiene ciclos tenemos un rbol (ver rboles). Varios rboles independientes forman un bosque. Un rbol de expansin de un grafo es una reduccin del grafo en el que solo entran a formar parte el nmero mnimo de aristas que forman un rbol y conectan a todos los nodos. Segn el nmero de aristas que contiene, un grafo es completo si cuenta con todas las aristas posibles (es decir, todos los nodos estn conectados con todos), disperso si tiene relativamente pocas aristas y denso si le faltan pocas para ser completo. Las aristas son la mayor parte de las veces bidireccionales, es decir, si una arista conecta dos nodos A y B se puede recorrer tanto en sentido hacia B como en sentido hacia A: estos son llamados grafos no dirigidos. Sin embargo, en ocasiones tenemos que las uniones son

unidireccionales. Estas uniones se suelen dibujar con una flecha y definen un grafo dirigido. Cuando las aristas llevan un coste asociado (un entero al que se denomina peso) el grafo es ponderado. Una red es un grafo dirigido y ponderado.

Representacin de grafos Una caracterstica especial en los grafos es que podemos representarlos utilizando dos estructuras de datos distintas. En los algoritmos que se aplican sobre ellos veremos que adoptarn tiempos distintos dependiendo de la forma de representacin elegida. En particular, los tiempos de ejecucin variarn en funcin del nmero de vrtices y el de aristas, por lo que la utilizacin de una representacin u otra depender en gran medida de si el grafo es denso o disperso. Para nombrar los nodos utilizaremos letras maysculas, aunque en el cdigo deberemos hacer corresponder cada nodo con un entero entre 1 y V (nmero de vrtices) de cara a la manipulacin de los mismos.

Representacin por matriz de adyacencia Es la forma ms comn de representacin y la ms directa. Consiste en una tabla de tamao V x V, en que la que a[i][j] tendr como valor 1 si existe una arista del nodo i al nodo j. En caso contrario, el valor ser 0. Cuando se trata de grafos ponderados en lugar de 1 el valor que tomar ser el peso de la arista. Si el grafo es no dirigido hay que asegurarse de que se marca con un 1 (o con el peso) tanto la entrada a[i][j] como la entrada a[j][i], puesto que se puede recorrer en ambos sentidos.
int V,A; int a[maxV][maxV]; void inicializar() { int i,x,y,p; char v1,v2; // Leer V y A memset(a,0,sizeof(a)); for (i=1; i<=A; i++) { scanf("%c %c %d\n",&v1,&v2,&p); x=v1-'A'; y=v2-'A'; a[x][y]=p; a[y][x]=p; } }

En esta implementacin se ha supuesto que los vrtices se nombran con una letra mayscula y no hay errores en la entrada. Evidentemente, cada problema tendr una forma

de entrada distinta y la inicializacin ser conveniente adaptarla a cada situacin. En todo caso, esta operacin es sencilla si el nmero de nodos es pequeo. Si, por el contrario, la entrada fuese muy grande se pueden almacenar los nombres de nodos en un rbol binario de bsqueda o utilizar una tabla de dispersin, asignando un entero a cada nodo, que ser el utilizado en la matriz de adyacencia. Como se puede apreciar, la matriz de adyacencia siempre ocupa un espacio de V*V, es decir, depende solamente del nmero de nodos y no del de aristas, por lo que ser til para representar grafos densos.

Representacin por lista de adyacencia Otra forma de representar un grafo es por medio de listas que definen las aristas que conectan los nodos. Lo que se hace es definir una lista enlazada para cada nodo, que contendr los nodos a los cuales es posible acceder. Es decir, un nodo A tendr una lista enlazada asociada en la que aparecer un elemento con una referencia al nodo B si A y B tienen una arista que los une. Obviamente, si el grafo es no dirigido, en la lista enlazada de B aparecer la correspondiente referencia al nodo A. Las listas de adyacencia sern estructuras que contendrn un valor entero (el nmero que identifica al nodo destino), as como otro entero que indica el coste en el caso de que el grafo sea ponderado. En el ejemplo se ha utilizado un nodo z ficticio en la cola (ver listas, apartado cabeceras y centinelas).
struct nodo { int v; int p; nodo *sig; }; int V,A; // vrtices y aristas del grafo struct nodo *a[maxV], *z; void inicializar() { int i,x,y,peso; char v1,v2; struct nodo *t; z=(struct nodo *)malloc(sizeof(struct nodo)); z->sig=z; for (i=0; i<V; i++) a[i]=z; for (i=0; i<A; i++) { scanf("%c %c %d\n",&v1,&v2,&peso); x=v1-'A'; y=v2-'A'; t=(struct nodo *)malloc(sizeof(struct nodo));

t->v=y; t->p=peso; t->sig=a[x]; a[x]=t; t=(struct nodo *)malloc(sizeof(struct nodo)); t->v=x; t->p=peso; t->sig=a[y]; a[y]=t; } }

En este caso el espacio ocupado es O(V + A), muy distinto del necesario en la matriz de adyacencia, que era de O(V2). La representacin por listas de adyacencia, por tanto, ser ms adecuada para grafos dispersos. Hay que tener en cuenta un aspecto importante y es que la implementacin con listas enlazadas determina fuertemente el tratamiento del grafo posterior. Como se puede ver en el cdigo, los nodos se van aadiendo a las listas segn se leen las aristas, por lo que nos encontramos que un mismo grafo con un orden distinto de las aristas en la entrada producir listas de adyacencia diferentes y por ello el orden en que los nodos se procesen variar. Una consecuencia de esto es que si un problema tiene varias soluciones la primera que se encuentre depender de la entrada dada. Podra presentarse el caso de tener varias soluciones y tener que mostrarlas siguiendo un determinado orden. Ante una situacin as podra ser muy conveniente modificar la forma de meter los nodos en la lista (por ejemplo, hacerlo al final y no al principio, o incluso insertarlo en una posicin adecuada), de manera que el algoritmo mismo diera las soluciones ya ordenadas.

Exploracin de grafos A la hora de explorar un grafo, nos encontramos con dos mtodos distintos. Ambos conducen al mismo destino (la exploracin de todos los vrtices o hasta que se encuentra uno determinado), si bien el orden en que stos son "visitados" decide radicalmente el tiempo de ejecucin de un algoritmo, como se ver posteriormente. En primer lugar, una forma sencilla de recorrer los vrtices es mediante una funcin recursiva, lo que se denomina bsqueda en profundidad. La sustitucin de la recursin (cuya base es la estructura de datos pila) por una cola nos proporciona el segundo mtodo de bsqueda o recorrido, la bsqueda en amplitud o anchura.

Suponiendo que el orden en que estn almacenados los nodos en la estructura de datos correspondiente es A-B-C-D-E-F... (el orden alfabtico), tenemos que el orden que seguira el recorrido en profundidad sera el siguiente: A-B-E-I-F-C-G-J-K-H-D En un recorrido en anchura el orden sera, por contra: A-B-C-D-E-G-H-I-J-K-F Es decir, en el primer caso se exploran primero los verdes y luego los marrones, pasando primero por los de mayor intensidad de color. En el segundo caso se exploran primero los verdes, despus los rojos, los naranjas y, por ltimo, el rosa. Es destacable que el nodo D es el ltimo en explorarse en la bsqueda en profundidad pese a ser adyacente al nodo de origen (el A). Esto es debido a que primero se explora la rama del nodo C, que tambin conduce al nodo D. En estos ejemplos hay que tener en cuenta que es fundamental el orden en que los nodos estn almacenados en las estructuras de datos. Si, por ejemplo, el nodo D estuviera antes que el C, en la bsqueda en profundidad se tomara primero la rama del D (con lo que el ltimo en visitarse sera el C), y en la bsqueda en anchura se explorara antes el H que el G.

Bsqueda en profundidad Se implementa de forma recursiva, aunque tambin puede realizarse con una pila. Se utiliza un array val para almacenar el orden en que fueron explorados los vrtices. Para ello se incrementa una variable global id (inicializada a 0) cada vez que se visita un nuevo vrtice y se almacena id en la entrada del array val correspondiente al vrtice que se est explorando.

La siguiente funcin realiza un mximo de V (el nmero total de vrtices) llamadas a la funcin visitar, que implementamos aqu en sus dos variantes: representacin por matriz de adyacencia y por listas de adyacencia.
int id=0; int val[V]; void buscar() { int k; for (k=1; k<=V; k++) val[k]=0; for (k=1; k<=V; k++) if (val[k]==0) visitar(k); } void visitar(int k) // matriz de adyacencia { int t; val[k]=++id; for (t=1; t<=V; t++) if (a[k][t] && val[t]==0) visitar(t); } void visitar(int k) // listas de adyacencia { struct nodo *t; val[k]=++id; for (t=a[k]; t!=z; t=t->sig) if (val[t->v]==0) visitar(t->v); }

El resultado es que el array val contendr en su i-sima entrada el orden en el que el vrtice i-simo fue explorado. Es decir, si tenemos un grafo con cuatro nodos y fueron explorados en el orden 3-1-2-4, el array val quedar como sigue:
val[1]=2; // el primer nodo fue visto en segundo lugar val[2]=3; // el segundo nodo fue visto en tercer lugar val[3]=1; // etc. val[4]=4;

Una modificacin que puede resultar especialmente til es la creacin de un array "inverso" al array val que contenga los datos anteriores "al revs". Esto es, un array en el que la entrada i-sima contiene el vrtice que se explor en i-simo lugar. Basta modificar la lnea
val[k]=++id;

sustituyndola por
val[++id]=k;

Para el orden de exploracin de ejemplo anterior los valores seran los siguientes:
val[1]=3; val[2]=1; val[3]=2; val[4]=4;

Bsqueda en amplitud o anchura La diferencia fundamental respecto a la bsqueda en profundidad es el cambio de estructura de datos: una cola en lugar de una pila. En esta implementacin, la funcin del array val y la variable id es la misma que en el mtodo anterior.
struct tcola *cola; void visitar(int k) // listas de adyacencia { struct nodo *t; encolar(&cola,k); while (!vacia(cola)) { desencolar(&cola,&k); val[k]=++id; for (t=a[k]; t!=z; t=t->sig) { if (val[t->v]==0) { encolar(&cola,t->v); val[t->v]=-1; } } } }

También podría gustarte