20 líneas de implementación de código, utilizando el algoritmo de Tarjan para resolver componentes conectados fuertes

Clase de preguntas de Leetcode 丨 Introducción a la última estructura de datos y algoritmos en 2020, el Sr. Zuo Chengyun y el Sr. Ma Yingjun lo llevarán a un entrevistador de Dachang

Marco de algoritmo

Pensemos en una pregunta: ¿Cuál es el principio central del algoritmo para la descomposición de componentes fuertemente conectados?

Si ha leído nuestro artículo anterior, esta pregunta no debería resultarle difícil de responder. Dado que es un componente fuertemente conectado, significa que todos los puntos del componente pueden conectarse entre sí. Entonces, podemos pensar fácilmente que podemos comenzar desde un punto y encontrar un bucle para volver al punto de partida . De esta manera, los puntos que pasan forman parte de los componentes fuertemente conectados.

Pero hay un problema de esta manera, es decir, es necesario asegurar que todos los puntos en el componente fuertemente conectado estén atravesados ​​y que no pueda haber omisiones . También podemos pensar en soluciones a este problema, por ejemplo, podemos usar un algoritmo de búsqueda para buscar todos sus puntos alcanzables y todas las rutas. Pero de esta forma nos encontraremos con otro problema. Este problema es el problema de conexión entre componentes fuertemente conectados .

Veamos un ejemplo:

En la imagen de arriba, si partimos del punto 1, podemos llegar a todos los puntos de la imagen. Pero encontraremos que 1, 2, 3 es un componente fuertemente conectado y 4, 5, 6 son otro. Cuando buscamos el componente fuertemente conectado donde se encuentra 1, es probable que los tres puntos 4, 5 y 6 también aparezcan. Pero el problema es que son autocomponentes y no deben contarse entre los componentes fuertemente conectados de 1.

Si clasificamos el análisis y las ideas anteriores, podemos encontrar que el núcleo del algoritmo de descomposición de componentes conectados fuerte es en realidad resolver estos dos problemas, que es el problema de integridad . Completitud significa que no puede haber omisiones, redundancia ni errores. Una vez que queremos entender el problema central, es fácil construir un marco de pensamiento. A continuación, veremos la descripción del algoritmo y será mucho más fácil de entender.

Detalles del algoritmo

El primer mecanismo del algoritmo de Tarjan es la marca de tiempo , es decir, al atravesar, cada punto atravesado se marca con un valor. Este valor indica qué elemento se atraviesa.

Esto debería ser fácil de entender, solo necesitamos mantener una variable global y dejar que aumente cuando se atraviesa. Anotemos el código Python para que todos lo demuestren:

stamp = 0
stamp_dict = {}
def dfs(u):
    stamp_dict[u] = stamp
    stamp += 1
    for v in Graph[u]:
        dfs(v)
复制代码

A través de la marca de tiempo, podemos saber el orden en que se visita cada punto, y este orden es el orden de avance. Por ejemplo, suponga que uyv son dos puntos, y la marca de tiempo de u es menor que v. Entonces sólo hay dos posibilidades para la relación entre ellos: la primera es que u se puede conectar av, lo que indica que el enlace de u av se puede conectar. La segunda es que u no se puede conectar a v. En este caso, si la conexión inversa de v a u no es significativa, porque no deben estar conectados entre sí.

Entonces, necesitamos encontrar la ruta inversa si queremos encontrar la ruta conectada . En el algoritmo de Kosaraju, usamos el gráfico inverso para lograr esto. En Tarjan, se adoptó otro enfoque. Como ya conocemos la marca de tiempo de cada punto, podemos encontrar la ruta inversa a través de la marca de tiempo. Qué significa eso? De hecho, es muy simple: cuando atravesamos u, si encontramos una v que es menor que u, entonces hay un camino inverso de u a v. Si v no se ha extraído de la pila en este momento, lo que significa que v está aguas arriba de u, entonces también significa que hay una ruta de v a u. Esto muestra que uyv pueden conectarse entre sí.

Ahora que hemos encontrado un par de uyv conectados entre sí, necesitamos registrarlos. Pero la pregunta es ¿cómo sabemos cuándo termina la grabación? ¿Dónde está este límite? El algoritmo de Tarjan diseñó otro mecanismo ingenioso para resolver este problema.

Este mecanismo es el mecanismo bajo, bajo [u] representa el valor mínimo de la marca de tiempo de todos los puntos a los que puede conectarse el punto u. Cuanto menor sea la marca de tiempo, mayor será la posición en el árbol de búsqueda, y también puede entenderse como el punto más alto del árbol de búsqueda al que puede conectarse. Entonces es obvio que este punto es la raíz de un subárbol del árbol de búsqueda donde se encuentra el componente fuertemente conectado del punto u.

Puede haber un pequeño giro aquí, veamos otra imagen:

El número de secuencia del nodo en el gráfico es la marca de tiempo del recorrido recursivo, podemos encontrar que su valor bajo es 1 para cada punto del gráfico. Obviamente, el punto 1 es el antepasado de los tres puntos 2, 3 y 4 del árbol de búsqueda. Es decir, el recorrido de este componente fuertemente conectado comienza desde el punto 1. Cuando el punto 1 sale de la pila, significa que el subárbol con la raíz del árbol de 1 bit se ha atravesado y se han encontrado todos los posibles componentes conectados fuertes.

Esto plantea otro problema. Suponemos que el punto actual es u. ¿Cómo sabemos si el punto u es una raíz de árbol como 1 en la figura ? ¿Hay alguna forma de marcarlo?

Por supuesto que sí. Una característica de estos puntos es que su marca de tiempo es igual a su mínimo . Por lo tanto, podemos usar una matriz para mantener los componentes fuertemente conectados encontrados y vaciar la matriz cuando la raíz del árbol al que se pueden atravesar estos componentes fuertemente conectados esté fuera de la pila.

Podemos escribir el código después de terminar la lógica anterior:

scc = []
stack = []

def tarjan(u):
    dfn[u], low[u] = stamp, stamp
    stamp += 1
 stack.append(u)
    
    for v in Graph[u]:
        if not dfn[v]:
            tarjan(v)
            low[u] = min(low[u], low[v])
        elif v in stack:
         low[u] = min(low[u], dfn[v])
    
   if dfn[u] == low[u]:
        cur = []
        # 栈中u之后的元素是一个完整的强连通分量
        while True:
            cur.append(stack[-1])
            stack.pop()
            if cur[-1] == u:
                break
        scc.append(cur)
复制代码

Finalmente, echemos un vistazo al ejemplo clásico que dijimos antes:

Primero, comenzamos desde 1 y continuamos buscando profundamente hasta el final de 6. Cuando se recorre a 6, DFN [6] = 4, low [6] = 4, cuando 6 sale de la pila, se cumple la condición y 6 se llama independientemente una conexión fuerte Peso.

De la misma manera, la condición también se cumple cuando 5 sale, y obtenemos el segundo componente fuertemente conectado.

Luego retrocedemos al nodo 3, el nodo 3 también puede atravesar el nodo 4 y el 4 se puede conectar al 1. Como ya hay 1 punto en la pila, no continuará recurriendo a 1 punto, solo se actualizará el bajo [4] = 1, y cuando 4 salga, 3 se actualizarán nuevamente, haciendo bajo [3] = 1.

Finalmente regresamos al nodo 1, y atravesamos el nodo 1 al nodo 2. Los 4 puntos que se pueden conectar a 2 ya están en la pila, y DFN [4]> DFN [2], por lo que 2 puntos no se actualizarán. Después de volver a la 1 en punto, no se pueden conectar otros puntos a la 1 en punto, salir. Al salir, encontramos que bajo [1] = DFN [1]. En este momento, los 4 elementos restantes en la pila son todos componentes fuertemente conectados.

En este punto, la introducción de todo el flujo del algoritmo ha terminado, espero que todos puedan disfrutar del contenido de hoy.

Soy Chengzhi y sinceramente les deseo a todos los beneficios cada día. Si aún te gusta el contenido de hoy, acude a un soporte de tres enlaces ~ (me gusta , sigue, adelante )


Autor: Chengzhi
Enlace: https: //juejin.im/post/6875498612537851918
 

 

Supongo que te gusta

Origin blog.csdn.net/qq_46388795/article/details/108751785
Recomendado
Clasificación