Tarjan 算法寻找强连通分量
如果所有顶点对之间存在路径,则有向图是强连通的。有向图的强连通分量 ( SCC ) 是最大强连通子图。例如,下图中有 3 个 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)。