En informática , un gráfico es un tipo de datos abstracto que pretende implementar los conceptos de gráfico no dirigido y gráfico dirigido del campo de la teoría de grafos dentro de las matemáticas .
Una estructura de datos de grafo consiste en un conjunto finito (y posiblemente mutable) de vértices (también llamados nodos o puntos ), junto con un conjunto de pares desordenados de estos vértices para un grafo no dirigido o un conjunto de pares ordenados para un grafo dirigido. Estos pares se conocen como aristas (también llamados enlaces o líneas ), y para un grafo dirigido también se conocen como aristas pero también a veces flechas o arcos . Los vértices pueden ser parte de la estructura del grafo, o pueden ser entidades externas representadas por índices enteros o referencias .
Una estructura de datos gráfica también puede asociar a cada borde algún valor de borde , como una etiqueta simbólica o un atributo numérico (costo, capacidad, longitud, etc.).
Las operaciones básicas proporcionadas por una estructura de datos gráfica G generalmente incluyen: [1]
Las estructuras que asocian valores a los bordes generalmente también proporcionan: [1]
La siguiente tabla muestra el costo de complejidad temporal de realizar varias operaciones en gráficos, para cada una de estas representaciones, con | V | el número de vértices y | E | el número de aristas. [ cita requerida ] En las representaciones matriciales, las entradas codifican el costo de seguir una arista. Se supone que el costo de las aristas que no están presentes es ∞.
Las listas de adyacencia generalmente se prefieren para la representación de gráficos dispersos , mientras que una matriz de adyacencia se prefiere si el gráfico es denso; es decir, el número de aristas es cercano al número de vértices al cuadrado , o si uno debe poder buscar rápidamente si hay una arista que conecta dos vértices. [5] [6]
La complejidad temporal de las operaciones en la representación de la lista de adyacencia se puede mejorar almacenando los conjuntos de vértices adyacentes en estructuras de datos más eficientes, como tablas hash o árboles de búsqueda binaria balanceados (la última representación requiere que los vértices se identifiquen por elementos de un conjunto ordenado linealmente, como números enteros o cadenas de caracteres). Una representación de vértices adyacentes mediante tablas hash conduce a una complejidad temporal promedio amortizada de para probar la adyacencia de dos vértices dados y para eliminar una arista y una complejidad temporal promedio amortizada [7] de para eliminar un vértice dado x de grado . La complejidad temporal de las otras operaciones y el requisito de espacio asintótico no cambian.
La paralelización de problemas de grafos enfrenta desafíos significativos: cálculos basados en datos, problemas no estructurados, localidad deficiente y alta relación de acceso a datos por cómputo. [8] [9] La representación gráfica utilizada para arquitecturas paralelas juega un papel importante para enfrentar esos desafíos. Las representaciones mal elegidas pueden aumentar innecesariamente el costo de comunicación del algoritmo, lo que disminuirá su escalabilidad . A continuación, se consideran arquitecturas de memoria compartida y distribuida.
En el caso de un modelo de memoria compartida , las representaciones gráficas utilizadas para el procesamiento paralelo son las mismas que en el caso secuencial, [10] ya que el acceso paralelo de solo lectura a la representación gráfica (por ejemplo, una lista de adyacencia ) es eficiente en la memoria compartida.
En el modelo de memoria distribuida , el enfoque habitual es particionar el conjunto de vértices del grafo en conjuntos . Aquí, es la cantidad de elementos de procesamiento (PE) disponibles. Las particiones del conjunto de vértices se distribuyen luego a los PE con índice coincidente, además de a los bordes correspondientes. Cada PE tiene su propia representación de subgrafo , donde los bordes con un punto final en otra partición requieren atención especial. Para interfaces de comunicación estándar como MPI , el ID del PE que posee el otro punto final tiene que ser identificable. Durante el cálculo en algoritmos de grafos distribuidos, pasar información a lo largo de estos bordes implica comunicación. [10]
La partición del gráfico debe realizarse con cuidado: existe un equilibrio entre una comunicación baja y una partición de tamaño uniforme [11]. Sin embargo, la partición de un gráfico es un problema NP-hard, por lo que no es factible calcularlas. En su lugar, se utilizan las siguientes heurísticas.
Partición 1D: cada procesador obtiene vértices y sus correspondientes aristas salientes. Esto puede entenderse como una descomposición por filas o por columnas de la matriz de adyacencia. Para los algoritmos que operan en esta representación, esto requiere un paso de comunicación de todos a todos, así como tamaños de búfer de mensajes, ya que cada PE tiene potencialmente aristas salientes hacia todos los demás PE. [12]
Partición 2D: Cada procesador obtiene una submatriz de la matriz de adyacencia. Supongamos que los procesadores están alineados en un rectángulo , donde y son la cantidad de elementos de procesamiento en cada fila y columna, respectivamente. Entonces cada procesador obtiene una submatriz de la matriz de adyacencia de dimensión . Esto se puede visualizar como un patrón de tablero de ajedrez en una matriz. [12] Por lo tanto, cada unidad de procesamiento solo puede tener aristas salientes a los PE en la misma fila y columna. Esto limita la cantidad de socios de comunicación para cada PE a un máximo de los posibles.
Los gráficos con billones de aristas se encuentran en el aprendizaje automático , el análisis de redes sociales y otras áreas. Se han desarrollado representaciones gráficas comprimidas para reducir los requisitos de memoria y de E/S. Se pueden aplicar técnicas generales como la codificación de Huffman , pero la lista de adyacencia o la matriz de adyacencia se pueden procesar de maneras específicas para aumentar la eficiencia. [13]
La búsqueda en amplitud (BFS) y la búsqueda en profundidad (DFS) son dos métodos estrechamente relacionados que se utilizan para explorar todos los nodos de un componente conectado determinado . Ambos comienzan con un nodo arbitrario, la " raíz ". [14]