Atlasingeniería

Estructuras de datosTablas hash y grafosTema 2

Representación de grafos

El mismo grafo se puede guardar como matriz o como listas de adyacencia. La elección cambia qué preguntas son baratas y cuánta memoria hace falta.

Para este tema conviene tener claro:Listas enlazadas

Un mapa de rutas, las dependencias de un proyecto y una red social son el mismo objeto: elementos y relaciones entre ellos. Antes de recorrerlo o de buscar caminos hay que decidir cómo se guarda, porque esa decisión fija el costo de todo lo que venga después.

Vértices, aristas y las variantes

Un grafo son vértices y aristas. Si la relación es simétrica el grafo es no dirigido; si no lo es, cada arista tiene origen y destino. Las aristas pueden llevar peso: distancia, costo, capacidad.

Dos números mandan en el análisis: la cantidad de vértices VV y la de aristas EE. Un grafo es denso cuando EE se acerca a V2V^2 y ralo cuando se parece a VV. Casi todos los grafos reales son ralos, y eso decide gran parte de lo que sigue.

Antes de seguir, predecí

Un grafo de mil vértices con cinco vecinos promedio. ¿Cuánta memoria ocupa la matriz de adyacencia?

La matriz de adyacencia

La matriz de adyacencia es un arreglo de V×VV \times V donde la celda (i,j)(i,j) dice si existe la arista, o cuánto pesa. Preguntar si dos vértices son vecinos es una lectura: O(1)O(1).

El precio es la memoria, Θ(V2)\Theta(V^2) sin importar cuántas aristas haya, y el costo de recorrer los vecinos de un vértice, que obliga a mirar la fila entera: O(V)O(V), la mayoría celdas vacías. En un grafo no dirigido la matriz es simétrica y alcanza con guardar media.

Las listas de adyacencia

Las listas de adyacencia guardan, para cada vértice, sólo sus vecinos reales. La memoria pasa a ser Θ(V+E)\Theta(V+E) y recorrer los vecinos de un vértice cuesta lo que ese vértice tiene, no lo que podría tener.

Lo que se pierde es la consulta directa: saber si existe una arista puntual obliga a recorrer la lista del vértice, O(grado)O(grado). Para un grafo ralo la cuenta cierra a favor de las listas, y por eso son la representación por defecto.

El grafo completo. V = 5, E = 5: bien ralo, como casi cualquier grafo real.

1 / 4
Cinco vértices y cinco aristas. Una matriz reservaría 25 celdas para guardar esas cinco; las listas guardan diez entradas, dos por arista.

Comparar E contra V al cuadrado

La regla práctica sale de comparar EE con V2V^2. Con un grafo denso o algoritmos que consultan aristas sueltas todo el tiempo, la matriz gana. Con un grafo ralo que se recorre —BFS, DFS, caminos mínimos—, ganan las listas: esos algoritmos terminan en O(V+E)O(V+E) con listas y arrastran un O(V2)O(V^2) con matriz.

Hay un caso a favor de la matriz aunque el grafo sea ralo: los algoritmos que la usan como matriz, por ejemplo calcular caminos entre todos los pares.

Representaciones intermedias

Hay representaciones intermedias. La lista de aristas guarda sólo las ternas origen, destino y peso: es lo mínimo para algoritmos que las ordenan por peso, como el árbol generador mínimo.

Para grafos grandes y estáticos se usa la forma comprimida: un arreglo con todos los vecinos concatenados y otro con dónde empieza cada vértice. Ocupa poco, recorre rápido por localidad de memoria y no admite cambios sin reconstruirla.

Dos detalles que se pagan caro

Dos detalles se pagan caro si se ignoran. El primero: en un grafo no dirigido cada arista aparece dos veces en las listas, así que la memoria es 2E2E y un recorrido ingenuo puede procesarla dos veces.

El segundo: los vértices conviene numerarlos de 00 a V1V-1 y traducir los nombres reales con una tabla hash. Con índices enteros, el estado de los algoritmos —visitados, distancias, padres— son arreglos comunes en vez de diccionarios.

Elegir la representación

Lista de adyacenciaMatriz de adyacenciaLista de aristas
MemoriaO(V + E)O(V²)O(E)
¿Hay arista entre u y v?O(grado de u)O(1)O(E)
Recorrer los vecinos de uO(grado de u)O(V)O(E)
Agregar una aristaO(1)O(1)O(1)
Recorrer todas las aristasO(V + E)O(V²)O(E)
Conviene cuandoel grafo es ralo, que es casi siemprees denso o se consulta mucho por paressólo hay que ordenar aristas, como en Kruskal
La fila resaltada es la que decide: BFS y DFS recorren vecinos todo el tiempo, y ahí la matriz paga O(V) por vértice aunque tenga dos vecinos.

La cuenta que ordena la decisión es cuántas aristas tiene el grafo comparado con cuántas podría tener. Un grafo con VV vértices puede tener hasta V2V^2 aristas, y los grafos reales casi nunca se acercan: una red social con mil millones de cuentas no tiene mil millones de amigos por cuenta, tiene unos cientos. Por eso la lista de adyacencia es el default, y la matriz aparece sólo cuando el grafo es chico o genuinamente denso.

Más a fondo · formalRalo y denso, con números

Un grafo es denso cuando EE se acerca a V2V^2 y ralo cuando se acerca a VV. El punto donde conviene cambiar de representación no es una constante sino el cruce de los dos costos: la matriz gana cuando V2V^2 es comparable a V+EV + E, o sea cuando EE es del orden de V2V^2.

Con V=1000V = 1000: la matriz ocupa un millón de casillas siempre, y la lista, 1000+E1000 + E. Con E=5000E = 5000 —cinco vecinos promedio, que es un grafo perfectamente normal— la lista usa unas 170 veces menos memoria. Para que la matriz empiece a convenir haría falta que cada vértice tuviera cientos de vecinos.

Hay un caso en que la matriz gana igual, y es cuando el algoritmo la necesita como matriz: Floyd-Warshall recorre V3V^3 combinaciones y consulta pares directamente, así que ahí la representación no es una elección de almacenamiento sino parte del algoritmo. Y multiplicar matrices de adyacencia tiene un significado propio: la potencia kk-ésima cuenta cuántos caminos de largo exactamente kk hay entre cada par.

Cierre

Matriz y listas describen el mismo grafo con costos opuestos: la matriz responde rápido por una arista y paga V2V^2 de memoria; las listas ocupan V+EV+E y recorren vecinos sin desperdicio. Con grafos ralos, que son casi todos, las listas son el default.

Autoevaluación

¿Lo entendiste?

¿Cuánta memoria usa una matriz de adyacencia?
Para recorrer un grafo ralo con BFS o DFS, ¿qué representación conviene?
¿Cuándo conviene la matriz aunque el grafo sea ralo?
En un grafo no dirigido guardado con listas, ¿cuántas veces aparece cada arista?