强连通分量
如果所有顶点对之间存在路径,则有向图是强连通的。有向图的强连通分量 ( SCC ) 是最大强连通子图。例如,下图中有 3 个 SCC。
我们可以使用 Kosaraju 算法在 O(V+E) 时间内找到所有强连通分量。以下是详细的 Kosaraju 算法。
1)创建一个空堆栈'S'并对图进行DFS遍历。在DFS遍历中,对一个顶点的相邻顶点调用递归DFS后,将该顶点压入栈中。在上图中,如果我们从顶点 0 开始 DFS,我们在堆栈中得到的顶点为 1、2、4、3、0。
2)反转所有弧的方向以获得转置图。
3)当 S 不为空时,从 S 中逐个弹出一个顶点。让弹出的顶点为'v'。以 v 为源,做 DFS(调用 DFSUtil(v))。从 v 开始的 DFS 打印 v 的强连通分量。在上面的示例中,我们按 0、3、4、2、1 的顺序处理顶点(从堆栈中逐一弹出)。
这是如何运作的?
上述算法是基于 DFS 的。它做了两次 DFS。如果从 DFS 起点可以到达所有顶点,则图的 DFS 会生成一棵树。否则 DFS 会生成一个森林。因此,只有一个 SCC 的图的 DFS 总是产生一棵树。需要注意的重要一点是,当存在多个 SCC 时,DFS 可能会生成一棵树或一片森林,具体取决于所选的起点。例如,在上图中,如果我们从顶点 0 或 1 或 2 开始 DFS,我们会得到一棵树作为输出。如果我们从 3 或 4 开始,我们会得到一个森林。要查找和打印所有 SCC,我们希望从顶点 4(这是一个汇顶点)开始 DFS,然后移动到 3,它是剩余集合(不包括 4)中的汇,最后是任何剩余顶点(0, 1、2)。那么我们如何找到这个挑选顶点的序列作为 DFS 的起点呢?不幸的是,没有直接的方法可以得到这个序列。但是,如果我们对图进行 DFS 并根据它们的完成时间存储顶点,我们确保连接到其他 SCC(除了它自己的 SCC)的顶点的完成时间将始终大于顶点的完成时间在另一个 SCC 中(见此证明)。例如,在上面示例图的 DFS 中,完成时间 0 始终大于 3 和 4(与 DFS 考虑的顶点序列无关)。并且完成时间 3 总是大于 4。DFS 不保证其他顶点,例如完成时间 1 和 2 可能小于或大于 3 和 4,具体取决于 DFS 考虑的顶点顺序。所以为了使用这个属性,我们对完整图进行 DFS 遍历,并将每个完成的顶点推入堆栈。在堆栈中,3 总是出现在 4 之后,0 出现在 3 和 4 之后。
在下一步中,我们反转图形。考虑 SCC 的图表。在反转图中,连接两个组件的边是反转的。因此 SCC {0, 1, 2} 成为接收器,而 SCC {4} 成为源。如上所述,在堆栈中,我们总是在 3 和 4 之前有 0。因此,如果我们使用堆栈中的顶点序列对反转图进行 DFS,我们会处理从汇点到源的顶点(在反转图中)。这就是我们想要实现的目标,而这正是一张一张打印 SCC 所需要的。
以下是 Kosaraju 算法的 C++ 实现。
C++
// C++ Implementation of Kosaraju's algorithm to print all SCCs
#include
#include
#include
using namespace std;
class Graph
{
int V; // No. of vertices
list *adj; // An array of adjacency lists
// Fills Stack with vertices (in increasing order of finishing
// times). The top element of stack has the maximum finishing
// time
void fillOrder(int v, bool visited[], stack &Stack);
// A recursive function to print DFS starting from v
void DFSUtil(int v, bool visited[]);
public:
Graph(int V);
void addEdge(int v, int w);
// The main function that finds and prints strongly connected
// components
void printSCCs();
// Function that returns reverse (or transpose) of this graph
Graph getTranspose();
};
Graph::Graph(int V)
{
this->V = V;
adj = new list[V];
}
// A recursive function to print DFS starting from v
void Graph::DFSUtil(int v, bool visited[])
{
// Mark the current node as visited and print it
visited[v] = true;
cout << v << " ";
// Recur for all the vertices adjacent to this vertex
list::iterator i;
for (i = adj[v].begin(); i != adj[v].end(); ++i)
if (!visited[*i])
DFSUtil(*i, visited);
}
Graph Graph::getTranspose()
{
Graph g(V);
for (int v = 0; v < V; v++)
{
// Recur for all the vertices adjacent to this vertex
list::iterator i;
for(i = adj[v].begin(); i != adj[v].end(); ++i)
{
g.adj[*i].push_back(v);
}
}
return g;
}
void Graph::addEdge(int v, int w)
{
adj[v].push_back(w); // Add w to v’s list.
}
void Graph::fillOrder(int v, bool visited[], stack &Stack)
{
// Mark the current node as visited and print it
visited[v] = true;
// Recur for all the vertices adjacent to this vertex
list::iterator i;
for(i = adj[v].begin(); i != adj[v].end(); ++i)
if(!visited[*i])
fillOrder(*i, visited, Stack);
// All vertices reachable from v are processed by now, push v
Stack.push(v);
}
// The main function that finds and prints all strongly connected
// components
void Graph::printSCCs()
{
stack Stack;
// Mark all the vertices as not visited (For first DFS)
bool *visited = new bool[V];
for(int i = 0; i < V; i++)
visited[i] = false;
// Fill vertices in stack according to their finishing times
for(int i = 0; i < V; i++)
if(visited[i] == false)
fillOrder(i, visited, Stack);
// Create a reversed graph
Graph gr = getTranspose();
// Mark all the vertices as not visited (For second DFS)
for(int i = 0; i < V; i++)
visited[i] = false;
// Now process all vertices in order defined by Stack
while (Stack.empty() == false)
{
// Pop a vertex from stack
int v = Stack.top();
Stack.pop();
// Print Strongly connected component of the popped vertex
if (visited[v] == false)
{
gr.DFSUtil(v, visited);
cout << endl;
}
}
}
// Driver program to test above functions
int main()
{
// Create a graph given in the above diagram
Graph g(5);
g.addEdge(1, 0);
g.addEdge(0, 2);
g.addEdge(2, 1);
g.addEdge(0, 3);
g.addEdge(3, 4);
cout << "Following are strongly connected components in "
"given graph \n";
g.printSCCs();
return 0;
}
Java
// Java implementation of Kosaraju's algorithm to print all SCCs
import java.io.*;
import java.util.*;
import java.util.LinkedList;
// This class represents a directed graph using adjacency list
// representation
class Graph
{
private int V; // No. of vertices
private LinkedList adj[]; //Adjacency List
//Constructor
Graph(int v)
{
V = v;
adj = new LinkedList[v];
for (int i=0; i i =adj[v].iterator();
while (i.hasNext())
{
n = i.next();
if (!visited[n])
DFSUtil(n,visited);
}
}
// Function that returns reverse (or transpose) of this graph
Graph getTranspose()
{
Graph g = new Graph(V);
for (int v = 0; v < V; v++)
{
// Recur for all the vertices adjacent to this vertex
Iterator i =adj[v].listIterator();
while(i.hasNext())
g.adj[i.next()].add(v);
}
return g;
}
void fillOrder(int v, boolean visited[], Stack stack)
{
// Mark the current node as visited and print it
visited[v] = true;
// Recur for all the vertices adjacent to this vertex
Iterator i = adj[v].iterator();
while (i.hasNext())
{
int n = i.next();
if(!visited[n])
fillOrder(n, visited, stack);
}
// All vertices reachable from v are processed by now,
// push v to Stack
stack.push(new Integer(v));
}
// The main function that finds and prints all strongly
// connected components
void printSCCs()
{
Stack stack = new Stack();
// Mark all the vertices as not visited (For first DFS)
boolean visited[] = new boolean[V];
for(int i = 0; i < V; i++)
visited[i] = false;
// Fill vertices in stack according to their finishing
// times
for (int i = 0; i < V; i++)
if (visited[i] == false)
fillOrder(i, visited, stack);
// Create a reversed graph
Graph gr = getTranspose();
// Mark all the vertices as not visited (For second DFS)
for (int i = 0; i < V; i++)
visited[i] = false;
// Now process all vertices in order defined by Stack
while (stack.empty() == false)
{
// Pop a vertex from stack
int v = (int)stack.pop();
// Print Strongly connected component of the popped vertex
if (visited[v] == false)
{
gr.DFSUtil(v, visited);
System.out.println();
}
}
}
// Driver method
public static void main(String args[])
{
// Create a graph given in the above diagram
Graph g = new Graph(5);
g.addEdge(1, 0);
g.addEdge(0, 2);
g.addEdge(2, 1);
g.addEdge(0, 3);
g.addEdge(3, 4);
System.out.println("Following are strongly connected components "+
"in given graph ");
g.printSCCs();
}
}
// This code is contributed by Aakash Hasija
Python
# Python implementation of Kosaraju's algorithm to print all SCCs
from collections import defaultdict
#This class represents a directed graph using adjacency list representation
class Graph:
def __init__(self,vertices):
self.V= vertices #No. of vertices
self.graph = defaultdict(list) # default dictionary to store graph
# function to add an edge to graph
def addEdge(self,u,v):
self.graph[u].append(v)
# A function used by DFS
def DFSUtil(self,v,visited):
# Mark the current node as visited and print it
visited[v]= True
print v,
#Recur for all the vertices adjacent to this vertex
for i in self.graph[v]:
if visited[i]==False:
self.DFSUtil(i,visited)
def fillOrder(self,v,visited, stack):
# Mark the current node as visited
visited[v]= True
#Recur for all the vertices adjacent to this vertex
for i in self.graph[v]:
if visited[i]==False:
self.fillOrder(i, visited, stack)
stack = stack.append(v)
# Function that returns reverse (or transpose) of this graph
def getTranspose(self):
g = Graph(self.V)
# Recur for all the vertices adjacent to this vertex
for i in self.graph:
for j in self.graph[i]:
g.addEdge(j,i)
return g
# The main function that finds and prints all strongly
# connected components
def printSCCs(self):
stack = []
# Mark all the vertices as not visited (For first DFS)
visited =[False]*(self.V)
# Fill vertices in stack according to their finishing
# times
for i in range(self.V):
if visited[i]==False:
self.fillOrder(i, visited, stack)
# Create a reversed graph
gr = self.getTranspose()
# Mark all the vertices as not visited (For second DFS)
visited =[False]*(self.V)
# Now process all vertices in order defined by Stack
while stack:
i = stack.pop()
if visited[i]==False:
gr.DFSUtil(i, visited)
print""
# Create a graph given in the above diagram
g = Graph(5)
g.addEdge(1, 0)
g.addEdge(0, 2)
g.addEdge(2, 1)
g.addEdge(0, 3)
g.addEdge(3, 4)
print ("Following are strongly connected components " +
"in given graph")
g.printSCCs()
#This code is contributed by Neelam Yadav
输出:
Following are strongly connected components in given graph
0 1 2
3
4
时间复杂度:上述算法调用 DFS,找到图的反向并再次调用 DFS。对于使用邻接表表示的图,DFS 需要 O(V+E)。反转图也需要 O(V+E) 时间。为了反转图,我们简单地遍历所有邻接列表。
上述算法是渐近最佳算法,但还有其他算法,如 Tarjan 算法和基于路径的算法,它们具有相同的时间复杂度,但使用单个 DFS 找到 SCC。 Tarjan 算法将在下一篇文章中讨论。
Tarjan 算法寻找强连通分量
应用:
SCC 算法可以用作许多仅适用于强连通图的图算法的第一步。
在社交网络中,一群人通常具有很强的联系(例如,一个班级的学生或任何其他常见的地方)。这些群体中的许多人通常喜欢一些常见的页面或玩常见的游戏。 SCC算法可用于找到这样的群组,并向群组中尚未点过普遍喜欢的页面或玩过游戏的人推荐普遍喜欢的页面或游戏。
参考:
http://en.wikipedia.org/wiki/Kosaraju%27s_algorithm
https://www.youtube.com/watch?v=PZQ0Pdk15RA
您可能还想查看 Tarjan 的算法来查找强连接组件。