Articulo de referencia

Algoritmo de componentes fuertemente conectadas de Tarjan

O(|V|+|E|) "},"best-time":{"wt":""},"average-time":{"wt":""},"space":{"wt":""},"optimal":{"wt":""},"complete":{"wt":""}},"i":0}}]}"> El algoritmo de componentes fuertemente cone...

El algoritmo de componentes fuertemente conexas de Tarjan es un algoritmo de teoría de grafos para encontrar las componentes fuertemente conexas (CFC) de un grafo dirigido . Se ejecuta en tiempo lineal , igualando el límite de tiempo de métodos alternativos como el algoritmo de Kosaraju y el algoritmo de componentes fuertes basado en caminos . El algoritmo recibe su nombre de su inventor, Robert Tarjan . [ 1 ]

Descripción general

El algoritmo toma como entrada un grafo dirigido y produce una partición de sus vértices en sus componentes fuertemente conexas. Cada vértice del grafo aparece en exactamente una de estas componentes. Cualquier vértice que no esté en un ciclo dirigido forma una componente fuertemente conexa por sí mismo; es decir, cualquier vértice cuyo grado de entrada o de salida sea 0, o cualquier vértice de un grafo dirigido acíclico .

La idea básica del algoritmo es la siguiente: una búsqueda en profundidad (DFS) comienza desde un nodo inicial arbitrario (y las búsquedas en profundidad subsiguientes se realizan en cualquier nodo que aún no se haya encontrado). Como es habitual en la búsqueda en profundidad, la búsqueda visita cada nodo del grafo exactamente una vez, sin volver a visitar ningún nodo que ya haya sido visitado. De este modo, el conjunto de árboles de búsqueda constituye un bosque de expansión del grafo. Los componentes fuertemente conectados se recuperarán como ciertos subárboles de este bosque. Las raíces de estos subárboles se denominan "raíces" de los componentes fuertemente conectados. Cualquier nodo de un componente fuertemente conectado puede servir como raíz, si resulta ser el primer nodo de un componente descubierto mediante la búsqueda.

Invariante de pila

La raíz de un componente fuertemente conectado, según un recorrido de búsqueda en profundidad, es el primer nodo del componente visitado por dicho recorrido. Por lo tanto, la raíz es el último nodo del componente del que se retrocede durante el recorrido. La idea clave del algoritmo de Tarjans es que una raíz también puede expresarse como un nodo desde el cual no se puede alcanzar ningún nodo visitado previamente.

Al igual que en la búsqueda en profundidad estándar, los nodos se colocan en una pila en el orden en que se visitan. A diferencia de la búsqueda en profundidad, cuando esta visita recursivamente un nodo vy sus descendientes, no necesariamente se eliminan todos de la pila al finalizar la llamada recursiva. La propiedad invariante crucial es que un nodo permanece en la pila después de haber sido visitado si y solo si existe una ruta en el grafo de entrada desde él hasta algún nodo anterior en la pila, y los nodos se eliminan al retroceder desde una raíz. En otras palabras, un nodo solo se elimina de la pila de la búsqueda en profundidad cuando se han recorrido todas sus rutas conectadas.

Al final de la llamada que visita vy sus descendientes, sabemos si vtiene un camino a algún nodo anterior en la pila. Si es así, la llamada regresa, dejando ven la pila para preservar el invariante. Si no, entonces vdebe ser la raíz de su componente fuertemente conectado, que consiste en vjunto con cualquier nodo posterior en la pila que v(todos estos nodos tienen caminos de regreso a vpero no a ningún nodo anterior, porque si tuvieran caminos a nodos anteriores entonces vtambién tendrían caminos a nodos anteriores lo cual es falso). El componente conectado enraizado en vse extrae de la pila y se devuelve, preservando nuevamente el invariante.

Teneduría de libros

A cada nodo vse le asigna un entero único v.index, que numera los nodos consecutivamente en el orden en que se descubren. También mantiene un valor v.lowlinkque representa el índice más pequeño de cualquier nodo en la pila que se sabe que es alcanzable desde a vtravés vdel subárbol DFS de , incluido vél mismo. Por lo tanto, vdebe permanecer en la pila si v.lowlink < v.index, mientras que v debe eliminarse como raíz de un componente fuertemente conectado si v.lowlink == v.index. El valor v.lowlinkse calcula durante la búsqueda en profundidad desde v, ya que esto encuentra los nodos que son alcanzables desde v.

El enlace bajo es diferente del punto bajo, que es el índice más pequeño alcanzable desde vcualquier parte del grafo. [ 1 ] : 156 [ 2 ]

El algoritmo en pseudocódigo

El algoritmo Tarjan recibe como entrada: grafo G = ( V , E ) y como salida: conjunto de componentes fuertemente conectadas (conjuntos de vértices). índice := 0 S := pila vacía para cada v en V hacer si v .index no está definido entonces strongconnect( v ) function strongconnect( v ) // Establece el índice de profundidad para v al índice no utilizado más pequeño v .index := index v .lowlink := index index := index + 1 S .push( v ) v .onStack := true // Considerar sucesores de v para cada ( v , w ) en E hacer si w .index no está definido entonces // El sucesor w aún no ha sido visitado; recursión en él strongconnect( w ) v .lowlink := min( v .lowlink, w .lowlink) sino si w .onStack entonces // El sucesor w está en la pila S y por lo tanto en el SCC actual // Si w no está en la pila, entonces ( v , w ) es una arista que apunta a un SCC ya encontrado y debe ignorarse // Ver más abajo con respecto a la siguiente línea v .lowlink := min( v .lowlink, w .index) // Si v es un nodo raíz, extraiga la pila y genere un SCC si v .lowlink = v .index entonces iniciar un nuevo componente fuertemente conectado repetir w := S .pop() w .onStack := false Sumar w al componente fuertemente conectado actual mientras wv generar el componente fuertemente conectado actual

La indexvariable es el contador de nodos de la búsqueda en profundidad. Ses la pila de nodos, que comienza vacía y almacena el historial de nodos explorados pero aún no incorporados a un componente fuertemente conectado. Esta no es la pila de búsqueda en profundidad habitual, ya que los nodos no se extraen a medida que la búsqueda avanza en el árbol; solo se extraen cuando se ha encontrado un componente fuertemente conectado completo.

El bucle más externo busca en cada nodo que aún no se ha visitado, asegurando que los nodos que no son accesibles desde el primer nodo se recorran finalmente. La función strongconnectrealiza una única búsqueda en profundidad del grafo, encontrando todos los sucesores del nodo vy reportando todos los componentes fuertemente conectados de ese subgrafo.

Cuando cada nodo finaliza su recursión, si su enlace inferior (lowlink) aún apunta a su índice, entonces es el nodo raíz de un componente fuertemente conectado, formado por todos los nodos que se encuentran por encima de él en la pila. El algoritmo extrae elementos de la pila hasta el nodo actual inclusive, y presenta todos estos nodos como un componente fuertemente conectado.

En el artículo de Tarjan, cuando westá en la pila, v.lowlinkse actualiza con la asignación . [ 1 ] : 157 Una variación común es usar en su lugar . [ 3 ] [ 4 ] Este algoritmo modificado no calcula los números de enlace bajo como los definió Tarjan, pero la prueba aún identifica los nodos raíz de los componentes fuertemente conectados y, por lo tanto, el algoritmo general sigue siendo válido. [ 2 ]v.lowlink := min(v.lowlink, w.index)v.lowlink := min(v.lowlink, w.lowlink)v.lowlink = v.index

Complejidad

Complejidad temporal : El procedimiento Tarjan se llama una vez por cada nodo; la instrucción forall considera cada arista como máximo una vez. Por lo tanto, el tiempo de ejecución del algoritmo es lineal en el número de aristas y nodos en G, es decirO(|V|+|mi|){\displaystyle O(|V|+|E|)}.

Para lograr esta complejidad, la comprobación de si un elemento westá en la pila debe realizarse en tiempo constante. Esto se puede hacer como en el pseudocódigo anterior: se almacena un indicador en cada nodo que señala si está en la pila y se realiza la comprobación examinando dicho indicador.

Complejidad espacial : El procedimiento Tarjan requiere dos palabras de datos suplementarios por vértice para los campos indexy lowlink, junto con un bit para onStacky otro para determinar cuándo indexno está definido. Además, se requiere una palabra en cada marco de pila para contener vy otra para la posición actual en la lista de aristas. Finalmente, el tamaño de la pila en el peor de los casos Sdebe ser|V|{\displaystyle |V|}(es decir, cuando el gráfico es un componente gigante). Esto proporciona un análisis final deO(|V|(2+5w)){\displaystyle O(|V|\cdot (2+5w))}dóndew{\displaystyle w}es el tamaño de palabra de la máquina. La variación de Nuutila y Soisalon-Soininen lo redujo aO(|V|(1+4w)){\displaystyle O(|V|\cdot (1+4w))}y, posteriormente, la de Pearce solo requiereO(|V|(1+3w)){\displaystyle O(|V|\cdot (1+3w))}. [ 5 ] [ 6 ]

Observaciones adicionales

Si bien el orden de los nodos dentro de cada componente fuertemente conexa no tiene nada de especial, una propiedad útil del algoritmo es que ningún componente fuertemente conexo se identificará antes que ninguno de sus sucesores. Por lo tanto, el orden en que se identifican los componentes fuertemente conexos constituye una ordenación topológica inversa del DAG formado por dichos componentes. [ 7 ]

Donald Knuth describió el algoritmo SCC de Tarjan como una de sus implementaciones favoritas en el libro The Stanford GraphBase . [ 8 ]

También escribió: [ 9 ]

Las estructuras de datos que ideó para este problema encajan a la perfección, de modo que las cantidades que necesitas consultar al explorar un grafo dirigido están siempre a tu alcance. Además, su algoritmo realiza una ordenación topológica como resultado secundario.

Referencias

  1. 1 2 3 Tarjan, RE (1972), "Búsqueda en profundidad y algoritmos de grafos lineales" (PDF) , SIAM Journal on Computing , 1 (2): 146– 160, CiteSeerX 10.1.1.327.8418 , doi : 10.1137/0201010 , archivado del original el 29-08-2017 , recuperado el 07-04-2024 {{citation}}: CS1 maint: bot: estado de la URL original desconocido ( enlace )
  2. 1 2 "Conferencia n.° 19: Búsqueda en profundidad y componentes fuertes" (PDF) , 15-451/651: Diseño y análisis de algoritmos , Carnegie Mellon, 1 de noviembre de 2018
  3. Kordy, Piotr; Langerak, Rom; Mauw, Sjouke; Polderman, Jan Willem (2014), "Un algoritmo simbólico para el análisis de autómatas temporizados robustos" (PDF) , en Jones, Cliff B.; Pihlajasaari, Pekka; Sun, Jun (eds.), FM 2014: Métodos formales – 19.º Simposio Internacional, Singapur, 12-16 de mayo de 2014. Actas , Lecture Notes in Computer Science, vol. 8442, Springer, pp. 351-366 , doi : 10.1007/978-3-319-06410-9_25 , ISBN   978-3-319-06409-3
  4. "Clase 19: Algoritmo de Tarjan para identificar componentes fuertemente conectados en el grafo de dependencias" (PDF) , CS130 Ingeniería de Software , Caltech, Invierno de 2024
  5. Nuutila, Esko (1994), "Sobre cómo encontrar los componentes fuertemente conectados en un grafo dirigido", Information Processing Letters , 49 (1): 9–14 , doi : 10.1016/0020-0190(94)90047-7
  6. Pearce, David, "Un algoritmo eficiente en espacio para detectar componentes fuertemente conectados", Information Processing Letters , 116 (1): 47– 52, doi : 10.1016/j.ipl.2015.08.010
  7. Harrison, Paul, "Ordenación topológica robusta y algoritmo de Tarjan en Python" , consultado el 9 de febrero de 2011.
  8. Knuth, The Stanford GraphBase , páginas 512–519.
  9. Knuth, Donald (2014-05-20), Veinte preguntas para Donald Knuth
  • Código Rosetta , que muestra implementaciones en diferentes lenguajes.
  • Implementación en PHP del algoritmo de componentes fuertemente conectadas de Tarjan.
  • Implementación en JavaScript del algoritmo de componentes fuertemente conectadas de Tarjan.