📜  Tarjan 算法寻找强连通分量

📅  最后修改于: 2022-05-13 01:57:54.309000             🧑  作者: Mango

Tarjan 算法寻找强连通分量

如果所有顶点对之间存在路径,则有向图是强连通的。有向图的强连通分量 ( SCC ) 是最大强连通子图。例如,下图中有 3 个 SCC。

SCC

我们已经讨论了 Kosaraju 的强连通分量算法。前面讨论的算法需要对 Graph 进行两次 DFS 遍历。在这篇文章中,讨论了只需要一次 DFS 遍历的 Tarjan 算法。

Tarjan 算法基于以下事实:
1. DFS 搜索产生一个 DFS 树/森林
2. 强连通分量形成 DFS 树的子树。
3. 如果我们能找到这些子树的头部,我们可以打印/存储该子树中的所有节点(包括头部),这将是一个 SCC。
4. 从一个SCC 到另一个SCC 没有后边(可以有交叉边,但在处理图形时不会使用交叉边)。

为了找到 SCC 的头部,我们计算圆盘和低阵列(就像对关节点、桥、双连接组件所做的那样)。如前文所述,low[u] 表示可以从以 u 为根的子树到达的最早访问的顶点(发现时间最短的顶点)。如果 disc[u] = low[u],则节点 u 是头。

下图是该方法的说明:

强连通分量仅与有向图有关,但 Disc 和 Low 值同时与有向图和无向图有关,因此在上图中,我们采用了无向图。
在上图中,我们展示了一个图和它的一个 DFS 树(根据遍历边的顺序,同一个图上可能有不同的 DFS 树)。

在 DFS 树中,连续箭头是树边,虚线箭头是后边(DFS 树边
对于每个节点,Disc 和 Low 值在图中显示为 (Disc/Low)。

Disc:这是 DFS 遍历时第一次访问节点的时间。对于 DFS 树中的节点 A、B、C、..、J,Disc 值为 1、2、3、..、10。
低:在 DFS 树中,树边带我们向前,从祖先节点到其后代之一。例如,从节点 C,树边可以带我们到节点 G、节点 I 等。后边带我们向后,从后代节点到其祖先之一。例如,从节点 G 开始,Back 边将我们带到 E 或 C。如果我们同时查看 Tree 和 Back 边,那么我们可以看到,如果我们从一个节点开始遍历,我们可能会通过 Tree 边沿树向下走,并且然后通过后边缘上升。例如,从节点 E,我们可以下到 G,然后再上到 C。类似地,从 E,我们可以下到 I 或 J,然后再上到 F。节点的“低”值表示最顶层可达通过该节点的子树的祖先(具有最小可能的 Disc 值)。因此,对于任何节点,Low 值无论如何都等于其 Disc 值(节点是其自身的祖先)。然后我们查看它的子树,看看是否有任何节点可以将我们带到它的任何祖先。如果子树中有多个后边将我们带到不同的祖先,那么我们采用具有最小 Disc 值的那个(即最上面的那个)。如果我们查看节点 F,它有两个子树。具有节点 G 的子树将我们带到 E 和 C。另一个子树仅将我们带回 F。这里最顶层的祖先是 F 可以到达的 C,因此 F 的低值是 3(C 的 Disc 值)。

根据上面的讨论,应该清楚B、C和D的Low值为1(因为A是B、C和D可以到达的最顶层节点)。同理,E、F、G 的 Low 值为 3,H、I、J 的 Low 值为 6。
对于任何节点 u,当 DFS 启动时,Low 将设置为其 Disc 1 st

然后稍后将对它的每个孩子 v 进行 DFS,低值 u 可以改变它两种情况:
Case1(树边):如果节点v还没有被访问过,那么在v的DFS完成后,low[u]和low[v]的最小值将被更新为low[u]。
低[u] = min(低[u], 低[v]);
情况 2(后边缘):当子 v 已经被访问时,low[u] 和 Disc[v] 的最小值将被更新为 low[u]。
低[u] = min(低[u],盘[v]);

情况二,我们可以用 low[v] 代替 disc[v] 吗? .答案是否定的。如果你能想到为什么答案是NO ,你可能理解了 Low 和 Disc 的概念。

边缘类型

相同的 Low 和 Disc 值有助于解决其他图形问题,例如关节点、桥接和双连通分量。
要跟踪以头部为根的子树,我们可以使用堆栈(在访问时不断推送节点)。当找到一个头节点时,从堆栈中弹出所有节点,直到你从堆栈中取出头。

为了确保,我们不考虑交叉边,当我们到达一个已经访问过的节点时,我们应该只处理访问过的节点,如果它存在于堆栈中,否则忽略该节点。

以下是执行 Tarjan 算法以打印所有 SCC。

C++
// A C++ program to find strongly connected components in a given
// directed graph using Tarjan's algorithm (single DFS)
#include
#include 
#include 
#define NIL -1
using namespace std;
 
// A class that represents an directed graph
class Graph
{
    int V; // No. of vertices
    list *adj; // A dynamic array of adjacency lists
 
    // A Recursive DFS based function used by SCC()
    void SCCUtil(int u, int disc[], int low[],
                stack *st, bool stackMember[]);
public:
    Graph(int V); // Constructor
    void addEdge(int v, int w); // function to add an edge to graph
    void SCC(); // prints strongly connected components
};
 
Graph::Graph(int V)
{
    this->V = V;
    adj = new list[V];
}
 
void Graph::addEdge(int v, int w)
{
    adj[v].push_back(w);
}
 
// A recursive function that finds and prints strongly connected
// components using DFS traversal
// u --> The vertex to be visited next
// disc[] --> Stores discovery times of visited vertices
// low[] -- >> earliest visited vertex (the vertex with minimum
//             discovery time) that can be reached from subtree
//             rooted with current vertex
// *st -- >> To store all the connected ancestors (could be part
//         of SCC)
// stackMember[] --> bit/index array for faster check whether
//                 a node is in stack
void Graph::SCCUtil(int u, int disc[], int low[], stack *st,
                    bool stackMember[])
{
    // A static variable is used for simplicity, we can avoid use
    // of static variable by passing a pointer.
    static int time = 0;
 
    // Initialize discovery time and low value
    disc[u] = low[u] = ++time;
    st->push(u);
    stackMember[u] = true;
 
    // Go through all vertices adjacent to this
    list::iterator i;
    for (i = adj[u].begin(); i != adj[u].end(); ++i)
    {
        int v = *i; // v is current adjacent of 'u'
 
        // If v is not visited yet, then recur for it
        if (disc[v] == -1)
        {
            SCCUtil(v, disc, low, st, stackMember);
 
            // Check if the subtree rooted with 'v' has a
            // connection to one of the ancestors of 'u'
            // Case 1 (per above discussion on Disc and Low value)
            low[u] = min(low[u], low[v]);
        }
 
        // Update low value of 'u' only of 'v' is still in stack
        // (i.e. it's a back edge, not cross edge).
        // Case 2 (per above discussion on Disc and Low value)
        else if (stackMember[v] == true)
            low[u] = min(low[u], disc[v]);
    }
 
    // head node found, pop the stack and print an SCC
    int w = 0; // To store stack extracted vertices
    if (low[u] == disc[u])
    {
        while (st->top() != u)
        {
            w = (int) st->top();
            cout << w << " ";
            stackMember[w] = false;
            st->pop();
        }
        w = (int) st->top();
        cout << w << "\n";
        stackMember[w] = false;
        st->pop();
    }
}
 
// The function to do DFS traversal. It uses SCCUtil()
void Graph::SCC()
{
    int *disc = new int[V];
    int *low = new int[V];
    bool *stackMember = new bool[V];
    stack *st = new stack();
 
    // Initialize disc and low, and stackMember arrays
    for (int i = 0; i < V; i++)
    {
        disc[i] = NIL;
        low[i] = NIL;
        stackMember[i] = false;
    }
 
    // Call the recursive helper function to find strongly
    // connected components in DFS tree with vertex 'i'
    for (int i = 0; i < V; i++)
        if (disc[i] == NIL)
            SCCUtil(i, disc, low, st, stackMember);
}
 
// Driver program to test above function
int main()
{
    cout << "\nSCCs in first graph \n";
    Graph g1(5);
    g1.addEdge(1, 0);
    g1.addEdge(0, 2);
    g1.addEdge(2, 1);
    g1.addEdge(0, 3);
    g1.addEdge(3, 4);
    g1.SCC();
 
    cout << "\nSCCs in second graph \n";
    Graph g2(4);
    g2.addEdge(0, 1);
    g2.addEdge(1, 2);
    g2.addEdge(2, 3);
    g2.SCC();
 
    cout << "\nSCCs in third graph \n";
    Graph g3(7);
    g3.addEdge(0, 1);
    g3.addEdge(1, 2);
    g3.addEdge(2, 0);
    g3.addEdge(1, 3);
    g3.addEdge(1, 4);
    g3.addEdge(1, 6);
    g3.addEdge(3, 5);
    g3.addEdge(4, 5);
    g3.SCC();
 
    cout << "\nSCCs in fourth graph \n";
    Graph g4(11);
    g4.addEdge(0,1);g4.addEdge(0,3);
    g4.addEdge(1,2);g4.addEdge(1,4);
    g4.addEdge(2,0);g4.addEdge(2,6);
    g4.addEdge(3,2);
    g4.addEdge(4,5);g4.addEdge(4,6);
    g4.addEdge(5,6);g4.addEdge(5,7);g4.addEdge(5,8);g4.addEdge(5,9);
    g4.addEdge(6,4);
    g4.addEdge(7,9);
    g4.addEdge(8,9);
    g4.addEdge(9,8);
    g4.SCC();
 
    cout << "\nSCCs in fifth graph \n";
    Graph g5(5);
    g5.addEdge(0,1);
    g5.addEdge(1,2);
    g5.addEdge(2,3);
    g5.addEdge(2,4);
    g5.addEdge(3,0);
    g5.addEdge(4,2);
    g5.SCC();
 
    return 0;
}


Java
// Java program to find strongly connected
// components in a given directed graph
// using Tarjan's algorithm (single DFS)
import java.io.*;
import java.util.*;
 
// This class represents a directed graph
// using adjacency list representation
class Graph{
 
// No. of vertices   
private int V;
 
//Adjacency Lists
private LinkedList adj[];
private int Time;
 
// Constructor
@SuppressWarnings("unchecked")
Graph(int v)
{
    V = v;
    adj = new LinkedList[v];
     
    for(int i = 0; i < v; ++i)
        adj[i] = new LinkedList();
         
    Time = 0;
}
 
// Function to add an edge into the graph
void addEdge(int v,int w)
{
    adj[v].add(w);
}
 
// A recursive function that finds and prints strongly
// connected components using DFS traversal
// u --> The vertex to be visited next
// disc[] --> Stores discovery times of visited vertices
// low[] -- >> earliest visited vertex (the vertex with
//             minimum discovery time) that can be reached
//             from subtree rooted with current vertex
// st -- >> To store all the connected ancestors (could be part
//         of SCC)
// stackMember[] --> bit/index array for faster check
//                   whether a node is in stack
void SCCUtil(int u, int low[], int disc[],
             boolean stackMember[],
             Stack st)
{
     
    // Initialize discovery time and low value
    disc[u] = Time;
    low[u] = Time;
    Time += 1;
    stackMember[u] = true;
    st.push(u);
 
    int n;
     
    // Go through all vertices adjacent to this
    Iterator i = adj[u].iterator();
     
    while (i.hasNext())
    {
        n = i.next();
         
        if (disc[n] == -1)
        {
            SCCUtil(n, low, disc, stackMember, st);
             
            // Check if the subtree rooted with v
            // has a connection to one of the
            // ancestors of u
            // Case 1 (per above discussion on
            // Disc and Low value)
            low[u] = Math.min(low[u], low[n]);
        }
        else if (stackMember[n] == true)
        {
             
            // Update low value of 'u' only if 'v' is
            // still in stack (i.e. it's a back edge,
            // not cross edge).
            // Case 2 (per above discussion on Disc
            // and Low value)
            low[u] = Math.min(low[u], disc[n]);
        }
    }
 
    // head node found, pop the stack and print an SCC
    // To store stack extracted vertices
    int w = -1;
    if (low[u] == disc[u])
    {
        while (w != u)
        {
            w = (int)st.pop();
            System.out.print(w + " ");
            stackMember[w] = false;
        }
        System.out.println();
    }
}
 
// The function to do DFS traversal.
// It uses SCCUtil()
void SCC()
{
     
    // Mark all the vertices as not visited
    // and Initialize parent and visited,
    // and ap(articulation point) arrays
    int disc[] = new int[V];
    int low[] = new int[V];
    for(int i = 0;i < V; i++)
    {
        disc[i] = -1;
        low[i] = -1;
    }
     
    boolean stackMember[] = new boolean[V];
    Stack st = new Stack();
     
    // Call the recursive helper function
    // to find articulation points
    // in DFS tree rooted with vertex 'i'
    for(int i = 0; i < V; i++)
    {
        if (disc[i] == -1)
            SCCUtil(i, low, disc,
                    stackMember, st);
    }
}
 
// Driver code
public static void main(String args[])
{
     
    // Create a graph given in the above diagram
    Graph g1 = new Graph(5);
 
    g1.addEdge(1, 0);
    g1.addEdge(0, 2);
    g1.addEdge(2, 1);
    g1.addEdge(0, 3);
    g1.addEdge(3, 4);
    System.out.println("SSC in first graph ");
    g1.SCC();
 
    Graph g2 = new Graph(4);
    g2.addEdge(0, 1);
    g2.addEdge(1, 2);
    g2.addEdge(2, 3);
    System.out.println("\nSSC in second graph ");
    g2.SCC();
     
    Graph g3 = new Graph(7);
    g3.addEdge(0, 1);
    g3.addEdge(1, 2);
    g3.addEdge(2, 0);
    g3.addEdge(1, 3);
    g3.addEdge(1, 4);
    g3.addEdge(1, 6);
    g3.addEdge(3, 5);
    g3.addEdge(4, 5);
    System.out.println("\nSSC in third graph ");
    g3.SCC();
     
    Graph g4 = new Graph(11);
    g4.addEdge(0, 1);
    g4.addEdge(0, 3);
    g4.addEdge(1, 2);
    g4.addEdge(1, 4);
    g4.addEdge(2, 0);
    g4.addEdge(2, 6);
    g4.addEdge(3, 2);
    g4.addEdge(4, 5);
    g4.addEdge(4, 6);
    g4.addEdge(5, 6);
    g4.addEdge(5, 7);
    g4.addEdge(5, 8);
    g4.addEdge(5, 9);
    g4.addEdge(6, 4);
    g4.addEdge(7, 9);
    g4.addEdge(8, 9);
    g4.addEdge(9, 8);
    System.out.println("\nSSC in fourth graph ");
    g4.SCC();
     
    Graph g5 = new Graph (5);
    g5.addEdge(0, 1);
    g5.addEdge(1, 2);
    g5.addEdge(2, 3);
    g5.addEdge(2, 4);
    g5.addEdge(3, 0);
    g5.addEdge(4, 2);
    System.out.println("\nSSC in fifth graph ");
    g5.SCC();
}
}
 
// This code is contributed by
// Prateek Gupta (@prateekgupta10)


Python3
# Python program to find strongly connected components in a given
# directed graph using Tarjan's algorithm (single DFS)
#Complexity : O(V+E)
  
from collections import defaultdict
  
#This class represents an directed graph
# using adjacency list representation
class Graph:
  
    def __init__(self,vertices):
        #No. of vertices
        self.V= vertices
         
        # default dictionary to store graph
        self.graph = defaultdict(list)
         
        self.Time = 0
  
    # function to add an edge to graph
    def addEdge(self,u,v):
        self.graph[u].append(v)
         
  
    '''A recursive function that find finds and prints strongly connected
    components using DFS traversal
    u --> The vertex to be visited next
    disc[] --> Stores discovery times of visited vertices
    low[] -- >> earliest visited vertex (the vertex with minimum
                discovery time) that can be reached from subtree
                rooted with current vertex
     st -- >> To store all the connected ancestors (could be part
           of SCC)
     stackMember[] --> bit/index array for faster check whether
                  a node is in stack
    '''
    def SCCUtil(self,u, low, disc, stackMember, st):
 
        # Initialize discovery time and low value
        disc[u] = self.Time
        low[u] = self.Time
        self.Time += 1
        stackMember[u] = True
        st.append(u)
 
        # Go through all vertices adjacent to this
        for v in self.graph[u]:
             
            # If v is not visited yet, then recur for it
            if disc[v] == -1 :
             
                self.SCCUtil(v, low, disc, stackMember, st)
 
                # Check if the subtree rooted with v has a connection to
                # one of the ancestors of u
                # Case 1 (per above discussion on Disc and Low value)
                low[u] = min(low[u], low[v])
                         
            elif stackMember[v] == True:
 
                '''Update low value of 'u' only if 'v' is still in stack
                (i.e. it's a back edge, not cross edge).
                Case 2 (per above discussion on Disc and Low value) '''
                low[u] = min(low[u], disc[v])
 
        # head node found, pop the stack and print an SCC
        w = -1 #To store stack extracted vertices
        if low[u] == disc[u]:
            while w != u:
                w = st.pop()
                print (w, end=" ")
                stackMember[w] = False
                 
            print()
             
     
 
    #The function to do DFS traversal.
    # It uses recursive SCCUtil()
    def SCC(self):
  
        # Mark all the vertices as not visited
        # and Initialize parent and visited,
        # and ap(articulation point) arrays
        disc = [-1] * (self.V)
        low = [-1] * (self.V)
        stackMember = [False] * (self.V)
        st =[]
         
 
        # Call the recursive helper function
        # to find articulation points
        # in DFS tree rooted with vertex 'i'
        for i in range(self.V):
            if disc[i] == -1:
                self.SCCUtil(i, low, disc, stackMember, st)
 
 
  
  
  
# Create a graph given in the above diagram
g1 = Graph(5)
g1.addEdge(1, 0)
g1.addEdge(0, 2)
g1.addEdge(2, 1)
g1.addEdge(0, 3)
g1.addEdge(3, 4)
print ("SSC in first graph ")
g1.SCC()
 
g2 = Graph(4)
g2.addEdge(0, 1)
g2.addEdge(1, 2)
g2.addEdge(2, 3)
print ("nSSC in second graph ")
g2.SCC()
 
  
g3 = Graph(7)
g3.addEdge(0, 1)
g3.addEdge(1, 2)
g3.addEdge(2, 0)
g3.addEdge(1, 3)
g3.addEdge(1, 4)
g3.addEdge(1, 6)
g3.addEdge(3, 5)
g3.addEdge(4, 5)
print ("nSSC in third graph ")
g3.SCC()
 
g4 = Graph(11)
g4.addEdge(0, 1)
g4.addEdge(0, 3)
g4.addEdge(1, 2)
g4.addEdge(1, 4)
g4.addEdge(2, 0)
g4.addEdge(2, 6)
g4.addEdge(3, 2)
g4.addEdge(4, 5)
g4.addEdge(4, 6)
g4.addEdge(5, 6)
g4.addEdge(5, 7)
g4.addEdge(5, 8)
g4.addEdge(5, 9)
g4.addEdge(6, 4)
g4.addEdge(7, 9)
g4.addEdge(8, 9)
g4.addEdge(9, 8)
print ("nSSC in fourth graph ")
g4.SCC();
 
 
g5 = Graph (5)
g5.addEdge(0, 1)
g5.addEdge(1, 2)
g5.addEdge(2, 3)
g5.addEdge(2, 4)
g5.addEdge(3, 0)
g5.addEdge(4, 2)
print ("nSSC in fifth graph ")
g5.SCC();
 
#This code is contributed by Neelam Yadav


Javascript


输出:

SCCs in first graph
4
3
1 2 0

SCCs in second graph
3
2
1
0

SCCs in third graph
5
3
4
6
2 1 0

SCCs in fourth graph
8 9
7
5 4 6
3 2 1 0
10

SCCs in fifth graph
4 3 2 1 0 

时间复杂度:上述算法主要调用DFS,DFS对于使用邻接表表示的图需要O(V+E)。