有向图的强连通分量--Tarjan算法---代码分析

本文就是做个代码分析,顺便说下理解。

一、预备知识:

需要知道什么是: 回边、前向边、交叉边
在这里插入图片描述

二、上代码:

#include<algorithm>
#define NIL -1

using namespace std;

class Graph
{
	int n;
	list<int>*adj;
	void SCCUtil(int u,int disc[],int low[],int &time,
					bool stackMember[],stack<int>&stk);
public:
	Graph(int _n){	n = _n; adj = new list<int>[n]; }
	~Graph(){ delete [] adj; }
	void addEdge(int v, int w){  adj[v].push_back(w); }
	void SCC();
};

list动态数组存邻接表,SCC()是对外开放的打印图的强连通分量的方法,它调用SCCUtil()来查找强连通分量,完成始化和调用任务。

SCC():

void Graph::SCC() 
{ 
    int *disc = new int[n]; 
    int *low = new int[n]; 
    bool *stackMember = new bool[n]; 
    stack<int> stk; 
  	int time = 0;
  	
    for (int i = 0; i < n; i++) 
    { 
        disc[i] = NIL; 
        low[i] = NIL; 
        stackMember[i] = false; 
    } 
  	 
    for (int i = 0; i < n; i++) 
        if (disc[i] == NIL) 		//disc也充当visited数组的作用,标记是否已访问 
            SCCUtil(i, disc, low,time, stackMember,stk); 
}

SCCUtil():
SCCUtil形参说明:
u: 要访问的下一个节点
disc[] :disc[u]存的顶点u在深度优先遍历中的次序(存的是被发现的时间)
low[]:low[u]存的是u节点通过其子孙或者它自己的回边能够到达的次序最小的祖先的次序
time: 时间戳,传的是引用
stackMember[]:标记进栈的顶点有哪些,栈内只有当前节点及其祖先
stk:栈,用于存放当前节点及其祖先,当找到一个强连通分量时,会将栈顶元素退出,
一直到将这个强连通分量退完

(注:当low[u] = disc[u]的时候,说明当前层处理的顶点u是一个强连通分量的根,因为算法一开始令low[u]=disc[u]=++time,即u顶点被访问的时间的为 比它父顶点time+1,而它能到达的次序最小的顶点也为这个,所以,如果后面发现它没有回边到自己的祖先,那它肯定是一个强连通分量的根(可能自成强连通分量,当它的子女的被先发现是强连分量的时候会先出栈和输出)=

void Graph::SCCUtil(int u, int disc[], int low[], int &time,
					bool stackMember[], stack<int>&stk)
{
	disc[u] = low[u] = ++time;	  
	stk.push(u);
	stackMember[u] = true;
	
	list<int>::iterator it;
	for(it = adj[u].begin(); it != adj[u].end(); it++)
	{
		int v = *it;
		if( disc[v] == -1)		//未访问过
		{
			SCCUtil(v,disc,low,time,stackMember,stk);
			
			low[u] = min(low[u], low[v]);		
		}
		else if(stackMember[v] == true)		//如果顶点v是u的祖先
		{
			low[u] = min(low[u], disc[v]);
		}
	}
	
	int w = 0;
	if( low[u] == disc[u])	//找到了一个强连通分量, 
	{		//u为该强连通分量所在DFS子树的根 
		while(stk.top() != u)	//只能把以u为根打出来,不能把整个栈弹出 
		{
			w = (int) stk.top();
			cout<< w << " ";
			stackMember[w] = false; 
			stk.pop();
		}
		w = (int) stk.top();  // u 
		cout<< w << " "<<endl;
		stackMember[w] = false; 
		stk.pop();
	}
}

算法的过程:
从传进来的第一个顶点u开始,先记录的它被发现的时间(次序)disc[u],把low[u]也设为这个,然后把它进栈,再遍历的它的邻居,如果他的邻居没有被访问过,那就递归访问它,它的邻居就是它的子女,子女可能也会找到它自己的子女,所以顶点u可能会有很多子孙,如果它的子女发现了一个访问过的顶点v,要先看下它是不自己的祖先,是的话low[u] = min(low[u],disc[v])会让它的low[u]的值等于disc[v](其实我觉得那句写成low[u] = disc[v]完全没问题,刚测试了,答案是一样的);不是的话忽略。假如递归到了叶子节点(它没有指向子女的边了),如果它有指向祖先的边(不是指向祖先的边忽略),那它可以更新low值,如果没有,在结束边遍历后它的low值会等于disc值,所以它自成一个连通分量,然后打印输出,退栈。之后程序回溯到这个叶子子女的双亲这一层,检查下它的子女的low值有没有比自己小,有就更新,没有就算了,然后继续遍历邻居看看自己有没有回边(指祖先的边)或者还有邻居没被访问。遍历完邻居后,看下low值等不等于disc值,等于,自己就是一个强连通分量的根,然后把这个强连同分量退掉,然后回溯balabalalalala…

看看维基上的动画吧,做的真好
顶点边上数字是那个顶点udisc[u]low[u]
在这里插入图片描述
ps:想暂停可以下下来,用windows medie player打开。

三、Tarjan原理理解

为什么low[u]==disc[u]u就是某个一个强连通分量的根呢?
因为深度优先搜索的时候,是以深度优先 的只有处理完了一个分支,它才会处理下一个分支,所以,一开始先假设所有的顶点都是自成一个单独的强连通分量,除非它u自己有回边到自己的祖先或者它u的子孙有回边能到它u的祖先以形成一个圈,否则这个顶点u及其子孙都和它u的祖先不在一个强连通分量里,而这个时候它的low没有被更新过。所以low[u] = disc[u]u肯定是某个连同分量的根。

四、全部代码和测试:

#include<iostream>
#include<list>
#include<stack>
#include<algorithm>
#define NIL -1

using namespace std;

class Graph
{
	int n;
	list<int>*adj;
	void SCCUtil(int u,int disc[],int low[],int &time,
					bool stackMember[],stack<int>&stk);
public:
	Graph(int _n){	n = _n; adj = new list<int>[n]; }
	~Graph(){ delete [] adj; }
	void addEdge(int v, int w);
	void SCC();
};
void Graph::addEdge(int v, int w)
{
	adj[v].push_back(w); 
}
/************SCCUtil形参说明:
u ---> 要访问的下一个节点
disc ---->disc[u]存的顶点u在深度优先遍历中的次序(存的是被发现的时间)
low ----->low[u]存的是u节点通过其子孙或者它自己的回边能够到达的次序最小的祖先的次序 
time ----> 时间戳,传的是引用
stackMember ----->标记进栈的顶点有哪些,栈内只有当前节点及其祖先
stk ------->栈,用于存放当前节点及其祖先,当找到一个强连通分量时,会栈顶元素退出,
	一直到将这个强连通分量退完 
*/
void Graph::SCCUtil(int u, int disc[], int low[], int &time,
					bool stackMember[], stack<int>&stk)
{
	disc[u] = low[u] = ++time;	  
	stk.push(u);
	stackMember[u] = true;	// 
	
	list<int>::iterator it;
	for(it = adj[u].begin(); it != adj[u].end(); it++)
	{
		int v = *it;
		if( disc[v] == -1)
		{
			SCCUtil(v,disc,low,time,stackMember,stk);
			
			low[u] = min(low[u], low[v]);		
		}
		else if(stackMember[v] == true)			//v是u的祖先
		{
			low[u] = min(low[u], disc[v]);		//改成low[u] = disc[v]也可以
		}
	}
	
	int w = 0;
	if( low[u] == disc[u])	//找到了一个强连通分量, 
	{		//u为该强连通分量所在DFS子树的根 
		while(stk.top() != u)	//只能把以u为根打出来,不能把整个栈弹出 
		{
			w = (int) stk.top();
			cout<< w << " ";
			stackMember[w] = false; 
			stk.pop();
		}
		w = (int) stk.top();  // u 
		cout<< w << " "<<endl;
		stackMember[w] = false; 
		stk.pop();
	}
}	
				
void Graph::SCC() 
{ 
    int *disc = new int[n]; 
    int *low = new int[n]; 
    bool *stackMember = new bool[n]; 
    stack<int> stk; 
  	int time = 0;
  	
    for (int i = 0; i < n; i++) 
    { 
        disc[i] = NIL; 
        low[i] = NIL; 
        stackMember[i] = false; 
    } 
  	 
    for (int i = 0; i < n; i++) 
        if (disc[i] == NIL) 		//disc也充当visited数组的作用,标记是否已访问 
            SCCUtil(i, disc, low,time, stackMember,stk); 
} 
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; 
} 

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值