强连通分量(Tarjan算法)

强连通分量

有向图G = (V, E)的一个强连通分支就是一个最大的顶点集合C,对于C中的每一对顶点u和v,有u可达v和v可达u,即顶点u和v是互相可达的。

算法思想

Tarjan算法基于对图深度优先搜索,每个强连通分量C对应深搜树中的一颗子树T(u),搜索时,把当前深搜树中未处理的节点加入一个顶点栈S,回溯时可以判断栈顶到根u为一个强连通分量C。

定义dfn[u]为顶点u搜索的次序编号(发现时间),low[u]为u或u的后代能追溯到的最早的栈中节点次序号。当dfn[u] = low[u]时,以u为根的搜索子树且在栈S中的所有节点是一个强连通分量。
强连通分量(Tarjan算法)_第1张图片
分析tarjan(u)过程对顶点v的检查条件if v in S
设当前顶点u,探索边e = (u, v)时,对边e分类讨论
case #1: 是树边,则low[u] = min {low[u], low[v]}
case #2: 是反向边,则low[u] = min {low[u], dfn[v] }
case #3: 是交叉边
(a)如果v所属的强连通分量scc[v]未访问,则low[u] = min {low[u], dfn[v] }
(b)如果v所属的强连通分量scc[v]已访问,则不考虑顶点v
case #4: 是正向边,则low[u] <= dfn[u] < dfn[v], 所以不考虑顶点v

算法实现

package graph;

import java.util.ArrayList;
import java.util.Stack;

public class AdjListDirectedGraph<V, E> {
	private Vertex<V>[] adj;// 邻接表
	private int n;// 顶点数目
	private int time = 0;
	private int sccNum;// 强连通分量数目
	private int[] scc;// 顶点v的强连通分量编号
	private int[] dfn;// dfs发现时间
	private int[] low;// 可追溯的最小dfn顶点
	private int[] parent;// 在深搜树中的父节点
	private Stack<Integer> stack;// 顶点栈

	@SuppressWarnings("unchecked")
	AdjListDirectedGraph(int capacity) {
		adj = new Vertex[capacity];
		n = 0;
	}

	public void addVertex(int v, V value) {
		adj[v] = new Vertex<>(value);
		n++;
	}

	public void addEdge(int u, int v) {
		addEdge(u, v, null);
	}

	public void addEdge(int u, int v, E value) {
		Edge prev = null;//
		for (Edge curr = adj[u].first; curr != null; curr = curr.next) {
			if (v == curr.v) {
				curr.value = value;
				return;
			} else if (v < curr.v) {
				if (prev == null) {
					adj[u].first = new Edge<E>(u, v, value, curr);
				} else {
					prev.next = new Edge<E>(u, v, value, curr);
				}
				return;
			}
			prev = curr;
		}

		// 插入到链表尾部
		if (prev == null) {
			adj[u].first = new Edge<E>(u, v, value, null);// 第一个边结点
		} else {
			prev.next = new Edge<E>(u, v, value, prev.next);// 插入到边链表的尾部
		}
	}

	public void printAdjList() {
		StringBuilder vs = new StringBuilder();
		StringBuilder es = new StringBuilder();
		boolean wrc = false;
		vs.append("{");
		es.append("{");
		for (int i = 0; i < n; i++) {
			vs.append(adj[i].value);
			if (i < n - 1) {
				vs.append(", ");
			}

			for (Edge p = adj[i].first; p != null; p = p.next) {
				if (wrc)
					es.append(", ");
				es.append("(");
				es.append(adj[i].value);
				es.append(", ");
				es.append(adj[p.v].value);
				es.append(")");
				wrc = true;
			}
		}
		vs.append("}");
		es.append("}");
		System.out.println(vs);
		System.out.println(es);
	}

	void initialize() {
		time = 0;
		sccNum = 0;
		parent = new int[n];
		dfn = new int[n];
		low = new int[n];
		scc = new int[n];
		stack = new Stack<>();
		for (int u = 0; u < n; u++) {
			parent[u] = -1;
			dfn[u] = 0;
			low[u] = 0;
			scc[u] = 0;
		}
	}

	public int stronglyConnectedComponent() {
		initialize();// 初始化搜索信息
		for (int u = 0; u < n; u++) {
			if (dfn[u] == 0) {
				tarjan(u);// 从顶点u开始搜索
			}
		}
		return sccNum;
	}

	@SuppressWarnings("unchecked")
	void printConnectedComponent() {
		ArrayList<Integer>[] components = new ArrayList[sccNum + 1];// [1..sccNum]
		for (int i = 1; i <= sccNum; i++) {
			components[i] = new ArrayList<>();// 第i个连通分量
		}

		for (int u = 0; u < adj.length; u++) {
			components[scc[u]].add(u);
		}

		// 打印每个连通分量
		for (int i = 1; i <= sccNum; i++) {
			System.out.print("{");
			for (int j = 0; j < components[i].size(); j++) {
				int v = components[i].get(j);// 顶点v
				System.out.print(adj[v].value);
				if (j < components[i].size() - 1) {
					System.out.print(", ");
				}
			}
			System.out.print("}");
			System.out.println();
		}
	}

	/**
	 * tarjan算法计算顶点low值和强连通分量
	 * 
	 * @param u
	 */
	void tarjan(int u) {
		dfn[u] = ++time;
		low[u] = dfn[u];
		stack.push(u);// 顶点u进栈
		for (Edge e = adj[u].first; e != null; e = e.next) {
			int v = e.v;// 
			if (dfn[v] == 0) {// 顶点未被访问
				parent[v] = u;
				tarjan(v);// 从顶点v继续搜索
				if (low[v] < low[u]) {// 顶点u的后代可追溯的最早栈中顶点
					low[u] = low[v];
				}
			} else if (scc[v] == 0) {// 顶点v已进栈, 尚未出栈
				//if v in stack S, then low[u] = min {low[u], dfn[v]}
				//case #1:  是反向边,顶点v在栈中
				//case #2:  是交叉边,如果v所属的强连通分量scc[v]未访问,则v在栈中
				//case #3:  是交叉边,如果v所属的强连通分量scc[v]已访问,则v不在栈中
				//case #4:  是前向边,无论v是否在栈中,low[u] <= dfn[u] < dfs[v], 所以不会更新low[u]
				if (dfn[v] < low[u]) {// 顶点u自己可追溯的最早栈中顶点
					low[u] = dfn[v];
				}
			}
		}

		if (low[u] == dfn[u]) {// 强连通分量C对应的深搜树根u
			sccNum++;
			int x;
			while ((x = stack.pop()) != u) {
				scc[x] = sccNum;
			}
			scc[x] = sccNum;// x = u
		}
	}

	static class Edge<E> {
		int u;// 
		int v;
		E value;
		Edge<E> next;// 顶点u相同的下一条边

		Edge(int u, int v, E value, Edge<E> next) {
			super();
			this.u = u;
			this.v = v;
			this.value = value;
			this.next = next;
		}

		@Override
		public String toString() {
			return "<" + u + ", " + v + ">";
		}
	}

	static class Vertex<V> {
		V value;
		Edge first;// 第一条边

		Vertex(V value) {
			super();
			this.value = value;
		}

		@Override
		public String toString() {
			return "" + value + "";
		}

	}

}

测试代码

package graph;

import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class TestAdjListDirectedGraph {

	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		int n = in.nextInt();// 顶点数
		int m = in.nextInt();// 边数
		in.nextLine();
		AdjListDirectedGraph<Character, Object> graph = new AdjListDirectedGraph<>(n);
		for (int i = 0; i < n; i++) {
			graph.addVertex(i, (char) ('a' + i));
		}

		Pattern pattern = Pattern.compile("<([a-z]), ([a-z])>");// 
		for (int i = 0; i < m; i++) {
			String s = in.nextLine();
			Matcher matcher = pattern.matcher(s);
			while (matcher.find()) {
				// pair (u, v)
				int u = matcher.group(1).charAt(0) - 'a';
				int v = matcher.group(2).charAt(0) - 'a';
				graph.addEdge(u, v);
			}
		}

		graph.stronglyConnectedComponent();// 计算强连通分量
		graph.printConnectedComponent();// 打印强连通分量
	}

}

测试案例

test case #1:
input:
6 10









result:
{f}
{c}
{a, b, d, e}

你可能感兴趣的:(算法导论,算法,图论)