【图论】Tarjan算法详解



在学习Tarjan算法之前,要先了解强连通的相关知识点!


  • 强连通: 在一个有向图G里,如果有两个点(a、b)可以相互到达,我们就叫这两个顶点(a,b)为强连通。

  • 强连通图: 如果在一个有向图G中,每两个点都强连通(可以相互到达),我们就叫这个图为强连通图。

  • 强连通分量(SCC): 在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做 强连通分量,孤立的点也是一个强连通分量。


例:下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。

在这里插入图片描述



Tarjan算法

Tarjan算法是一种用来求解有向图强连通分量的线性时间的算法。可以找强连通分量,也可以找缩点、割点等。


算法思路:

Tarjan算法是基于DFS的,每个强连通分量为搜索树的一棵子树,搜索时把当前搜索树中未处理的节点加入一个栈,回溯时判断栈顶到栈中节点是否为强连通分量。

为了使这颗搜索树在遇到强连通分量的节点的时候能顺利进行。在DFS时每个点都需要记录两个数组:

  • DFN[u]: 代表u点DFS到的时间,即时间戳,简单来说就是 第几个被搜索到的。可知在同一个DFS树的子树中,DFN[u] 越小,则其越浅。

  • Low[u]: 代表在DFS树中,u或u的子树 能够追溯到的最早的栈中节点的 时间戳。



算法过程:

(1)数组的初始化:对图进行深度优先搜索(DFS),在搜索过程中用 DFN 记录搜索的顺序。当首次搜索到点u 时,DFN 与 Low 数组的值都为 到该点的时间。


(2)堆栈:采用栈(记录已经搜索过的但是未删除的点),每搜索到一个点,将它压入栈顶。如果这个点有 出度 就继续往下找,直到找到底。


(3)每次返回时都将子节点与该节点的Low值进行比较,谁小就取谁,保证最小的子树根。

  • 当点u 有与点u’ 相连时,如果此时(时间为DFN[u]时)u’不在栈中,u的 Low 值为两点的 Low值 中较小的一个。

  • 当点u 有与点u’ 相连时,如果此时(时间为DFN[u]时)u’在栈中,u的 Low 值为 u的 Low值 和u’的 DFN值 中较小的一个


(4)每当搜索到一个点经过以上操作后(也就是子树已经全部遍历)的 Low值等于DFN值( DFN[u] == Low[u]),则将它以及在它之上的元素弹出栈。这些出栈的元素组成一个强连通分量。

原因:u点在DFS树中,子节点(后代)不能找到更浅的点,那么 u点及其后代构成一个SCC(强连通分量)。且 u点 是这个强连通分量的根节点(因为这个Low[] 值是这个强连通分量里最小的。)


(5)继续搜索(或许会更换搜索的起点,因为整个有向图可能分为两个不连通的部分),直到所有点被遍历。


在这里插入图片描述

伪代码:

tarjan(u){
  DFN[u]=Low[u]=++Index      // 为节点u设定次序编号和Low初值
  Stack.push(u)              // 将节点u压入栈中
 
  for each (u, v) in E       // 枚举每一条边
    if (v is not visted)   // 如果节点v未被访问过
           tarjan(v)       // 继续向下找
      Low[u] = min(Low[u], Low[v])   //更新这个点能指出去的最浅的时间戳
    else if (v in S)       // 如果节点u还在栈内
      Low[u] = min(Low[u], DFN[v])
 
  if (DFN[u] == Low[u])      // 如果节点u是强连通分量的根
       do{
        v = S.pop            // 将v退栈,为该强连通分量中一个顶点
    }while(u == v);
}


流程演示
  • 从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=Low[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。

在这里插入图片描述


  • 返回节点5,发现DFN[5]=Low[5],退栈后{5}为一个强连通分量。

在这里插入图片描述

  • 返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以Low[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树边,所以Low[3]=Low[4]=1。

在这里插入图片描述


  • 继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以Low[2]=DFN[4]=5。返回1后,发现DFN[1]=Low[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。

在这里插入图片描述


  • 至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。


  • 可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(V+E)。


还是没能理解?那就再来一个例子演示


  • 首先a、b、c、d依次入栈:

在这里插入图片描述


在这里插入图片描述

在这里插入图片描述


在这里插入图片描述


  • 直到e不能再往下搜索,此时判断 dfn[4] == low[4],因此e是一个强连通分量,并从栈中弹出。

    在这里插入图片描述


  • 然后节点d入栈:

在这里插入图片描述

  • 接下来 节点d 指向节点b,因此把节点d的 low值更新为2。

    在这里插入图片描述

  • 然后开始回溯,发现节点d:(5,2)不等,节点c:(3,2)不等,直到发现节点b:(2,2)相等,说明以b为根的子树就是一个强连通分量,同时在栈中弹出。

在这里插入图片描述

  • 接下来已经回溯到节点a,然后指向节点f,于是f入栈:

在这里插入图片描述

  • 依次节点g入栈:

在这里插入图片描述

  • 因为节点g 指向节点a,所以把节点g 的low值更新为1。

在这里插入图片描述


  • 然后开始回溯,由节点g 回溯到节点f,把f的low值也更新为1,再回溯到a,就得到了最后一个强连通分量。

在这里插入图片描述


Code:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
 
public class Tarjan {
	private int numOfNode;  //节点数
	private List< ArrayList<Integer> > graph;    //图
	private List< ArrayList<Integer> > result;   //保存极大强连通图
	private boolean[] inStack;   //表示节点是否在栈内,因为在stack中寻找一个节点不方便。这种方式查找快
	private Stack<Integer> stack;
	private int[] dfn;
	private int[] low;
	private int time;    //time表示在栈中的编号
	
	public Tarjan(List< ArrayList<Integer> > graph, int numOfNode){  //带参构造
		this.graph = graph;
		this.numOfNode = numOfNode;
		this.inStack = new boolean[numOfNode];
		this.stack = new Stack<Integer>();  //栈中元素都为整数
		dfn = new int[numOfNode];
		low = new int[numOfNode];
		Arrays.fill(dfn, -1);   //将dfn所有元素都置为-1,其中dfn[i]=-1代表节点i还有没被访问过
		Arrays.fill(low, -1);
		result = new ArrayList<ArrayList<Integer>>();
	}
	
	public List< ArrayList<Integer> > run(){
		for(int i=0;i<numOfNode;i++){
			if(dfn[i] == -1){
				tarjan(i);
			}
		}
		return result;
	}
	
	//算法的主要代码
	public void tarjan(int current){  //代表第几个点在处理,递归的是点
		dfn[current] = low[current] = time++;   //新的点首先初始化
		inStack[current] = true;  //表示在栈里
		stack.push(current);  //进栈
		
		for(int i=0; i<graph.get(current).size(); i++){  //搜索相连节点
			int next = graph.get(current).get(i);
			if(dfn[next] == -1){                         //如果没被访问过(-1代表没有被访问)
				tarjan(next);                            //递归调用
				low[current]=Math.min(low[current], low[next]);  //更新所能到的上层节点(涉及到强连通分量子树最小根的事情)
			}else if(inStack[next]){   //如果在栈中,并且被访问过
				low[current]=Math.min(low[current], dfn[next]);  //到栈中最上端的节点
			}
		}
		
		if(low[current] == dfn[current]){  //发现是整个强连通分量子树里的最小根
			ArrayList<Integer> temp = new ArrayList<Integer>();
			int j = -1;
			while(current!=j){
				j = stack.pop();     //出栈,并且输出
				inStack[j] = false;   //修改状态为不在栈中
				temp.add(j);
			}
			result.add(temp);			
		}
	}
	
 
 
 
	public static void main(String[] args) {
		//创建图
		int numOfNode = 6;
		List< ArrayList<Integer> > graph = new ArrayList<ArrayList<Integer>>();
		for(int i=0; i<numOfNode; i++){
			graph.add(new ArrayList<Integer>());
		}
		graph.get(0).add(1);
		graph.get(0).add(2);
		graph.get(1).add(3);
		graph.get(2).add(3);
		graph.get(2).add(4);
		graph.get(3).add(0);
		graph.get(3).add(5);
		graph.get(4).add(5);
		//调用Tarjan算法求极大连通子图
		Tarjan t = new Tarjan(graph, numOfNode);
		List< ArrayList<Integer> > result = t.run();  //结果代码
		
        //打印结果
		for(int i=0; i<result.size(); i++){
			for(int j=0; j<result.get(i).size(); j++){
				System.out.print(result.get(i).get(j) + " ");
			}
			System.out.println();
		}
 
	}
}


例题

原题链接:http://acm.hdu.edu.cn/showproblem.php?pid=1269

题意: 一个有向图,有 n 个点(n <= 10000)和 m 条边(m <= 100000)。判断整个图是否强连通,如果是,输出 “Yes”,否则,输出“No”。

思路: Kosaraju 算法模板题,直接求整个图是否为强连通分量。


Code:

#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int N = 100010;dfn
vector<int> G[N];
int dfn[N],low[N],sccno[N],s[N];
int cnt,dfn,top;
void dfs(int u) {
    s[top++] = u;
    dfn[u] = low[u] = ++dfn;
    for(int i=0; i<G[u].size(); i++) {
        int v=G[u][i];
        if(!dfn[v]){
            dfs(v);
            low[u] = min(low[u],low[v]);
        }
        else if(!sccno[v])
            low[u] = min(low[u],dfn[v]);
    }
    if(low[u]==dfn[u]){
        cnt++;
        while(1){
            int v = s[--top];
            sccno[v] = cnt;
            if(u==v)    break;
        }
    }
}
void Tarjan(int n) {
    cnt = top = dfn = 0;
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(sccno,0,sizeof(sccno));
    for(int i=1;i<=n;i++){
        if(!dfn[i])
            dfs(i);
    }
}
int main() {
    int n,m,u,v;
    while(cin>>n>>m && (n || m)) {
        for(int i=1;i<=n;i++)   G[i].clear();
        for(int i=1;i<=m;i++) {
            cin>>u>>v;
            G[u].push_back(v);
        }
        Tarjan(n);
        cnt==1?cout<<"Yes"<<endl:cout<<"No"<<endl;
    }
    return 0;
}


参考:Tarjan算法

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值