有向图
有向图同无向图的区别为每条边带有方向,代表从一个顶点至另外一个顶点可达。有向图的算法多依赖深度搜索算法。
本文主要介绍有向图的基本算法,涉及图的表示、可达性、检测环、图的遍历、拓扑排序以及强连通检测等算法。java
1 定义有向图算法
采用邻接表结构存储边信息,同时提供reverse接口生成反向图,倒置每一个边的方向,该接口在后续其余算法中会用到。post
/** * 采用邻接表表示的有向图 */ public class DiGraph { private final int V; private int E; private ArrayList<Integer>[] adj; public DiGraph(int V) { this.V = V; E = 0; adj = new ArrayList[V]; for (int i = 0; i < V; i++) { adj[i] = new ArrayList<>(); } } public DiGraph(Scanner scanner) { this(scanner.nextInt()); int E = scanner.nextInt(); for (int i = 0; i < E; i++) { int v = scanner.nextInt(); int w = scanner.nextInt(); addEdge(v, w); } } public void addEdge(int v, int w) { // 添加一条v指向w的边 adj[v].add(w); E++; } /** * 返回有向图的反向图, 将每条边的方向反转 */ public DiGraph reverse() { DiGraph diGraph = new DiGraph(V); for (int v = 0; v < V; v++) { for (int w : adj[v]) { diGraph.addEdge(w, v); } } return diGraph; } public void show() { System.out.println("V: " + V); System.out.println("E: " + E); for (int i = 0; i < V; i++) { System.out.print(i + ": "); for (Integer integer : adj[i]) { System.out.print(integer + " "); } System.out.println(); } } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 输入用例参加附录1 DiGraph diGraph = new DiGraph(scanner); // 输入结果见附录2 diGraph.show(); } }
2 有向图的可达性测试
有向图的可达性是指给定一个或一组顶点,判断是否能够到达图中其余顶点。垃圾清除常见算法“标记-清除”算法中,采用有向图的可达性算法
标记全部能够被访问的对象,而后在回收阶段,仅仅回收那些未被标记的对象。this
/** * 基于深度优先的有向图可达性算法 * 求出给定顶点或一组顶点,有向图中能到达的点 */ public class DirectedDFS { private boolean[] marked; // 标记每一个顶点是否可到达 public DirectedDFS(DiGraph G, int s) { marked = new boolean[G.V()]; dfs(G, s); } public DirectedDFS(DiGraph G, Iterable<Integer> sources) { marked = new boolean[G.V()]; for (int v : sources) { if(!marked[v]){ dfs(G, v); } } } private void dfs(DiGraph G, int v) { marked[v] = true; for (int w : G.adj(v)) { if(!marked[w]) dfs(G, w); } } public boolean marked(int v) { return marked[v]; } public static void main(String[] args) { // 输入用例参加附录1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 输出结果参加附录3 // 测试顶点2到达的点 System.out.println("顶点2到达的点"); DirectedDFS reachable = new DirectedDFS(diGraph, 2); for (int i = 0; i < diGraph.V(); i++) if(reachable.marked(i)) System.out.print(i + " "); System.out.println(); // 测试一组点:1,2,6可以到达的点 System.out.println("1,2,6可以到达的点"); DirectedDFS reachable2 = new DirectedDFS(diGraph, Arrays.asList(1, 2, 6)); for (int i = 0; i < diGraph.V(); i++) if(reachable2.marked(i)) System.out.print(i + " "); System.out.println(); } }
3 单点有向路径和单点最短有向路径code
分别采用深度优先搜索和广度优先搜索实现
有向图的路径对象
/** * 单点有向路径,给定顶点v,肯定对于图中任一点w; * 是否存在v到w的路径,并输出路径; * 注意,深度优先搜索的路径没法保证是最短路径 */ public class DigraghDepthFirstPaths { // 标记点是否可达 private boolean[] marked; // 记录到达点的那条边 private int[] edge; private final int s; public DigraghDepthFirstPaths(DiGraph G, int s) { this.s = s; marked = new boolean[G.V()]; edge = new int[G.V()]; edge[s] = s; dfs(G, s); } private void dfs(DiGraph G, int v) { marked[v] = true; for (int w : G.adj(v)) { if(!marked[w]){ edge[w] = v; dfs(G, w); } } } public boolean hasPathTo(int v){ return marked[v]; } public Stack<Integer> pathTo(int v) { Stack<Integer> paths = new Stack<>(); for (int x=v; x!=s; x=edge[x]){ paths.add(x); } paths.add(s); return paths; } public static void main(String[] args) { // 输入用例参加附录1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 输出结果参加附录4 // 构建顶点0到其余顶点的有向路径 DigraghDepthFirstPaths depthFirstPaths = new DigraghDepthFirstPaths(diGraph, 0); System.out.print("顶点0可达的点: "); for (int i = 0; i < diGraph.V(); i++) { if (depthFirstPaths.hasPathTo(i)) System.out.print(i + " "); } System.out.println(); // 是否存在有向路径 if(depthFirstPaths.hasPathTo(12)) System.out.println("0至12存在有向路径"); else System.out.println("0至12不存在有向路径"); // 顶点0到顶点3的一条有向路径 System.out.print("0至3的一条有向路径: "); Stack<Integer> pathTo = depthFirstPaths.pathTo(3); while (!pathTo.isEmpty()){ if (pathTo.size() == 1) System.out.print(pathTo.pop()); else System.out.print(pathTo.pop() + " -> "); } System.out.println(); } }
有向图的最短路径,基于广度优先算法blog
/** * 基于广度优先搜索的单向路径算法; * 在此方法下,求得的路径为最短路径(忽略边权重) */ public class DigraphBreadthFirstPaths { private boolean[] marked; // 采用队列保持带访问的顶点 private ArrayDeque<Integer> enqueue; private int[] edge; private final int s; public DigraphBreadthFirstPaths(DiGraph G, int s) { this.s = s; marked = new boolean[G.V()]; edge = new int[G.V()]; enqueue = new ArrayDeque<>(); enqueue.add(s); bfs(G); } private void bfs(DiGraph G) { while (!enqueue.isEmpty()) { int v = enqueue.poll(); for (int w : G.adj(v)) { if(!marked[w]){ edge[w] = v; marked[w] = true; enqueue.add(w); } } } } public boolean hasPathTo(int v){ return marked[v]; } public Stack<Integer> pathTo(int v) { Stack<Integer> paths = new Stack<>(); for (int x=v; x!=s; x=edge[x]){ paths.add(x); } paths.add(s); return paths; } public static void main(String[] args) { // 输入用例参加附录1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 输出结果参加附录5 // 构建顶点0到其余顶点的有向路径 DigraphBreadthFirstPaths breadthFirstPaths = new DigraphBreadthFirstPaths(diGraph, 0); System.out.print("顶点0可达的点: "); for (int i = 0; i < diGraph.V(); i++) { if (breadthFirstPaths.hasPathTo(i)) System.out.print(i + " "); } System.out.println(); // 是否存在有向路径 if(breadthFirstPaths.hasPathTo(12)) System.out.println("0至12存在有向路径"); else System.out.println("0至12不存在有向路径"); // 顶点0到顶点3的最短路径 System.out.print("0至3的一条有向路径: "); Stack<Integer> pathTo = breadthFirstPaths.pathTo(3); while (!pathTo.isEmpty()){ if (pathTo.size() == 1) System.out.print(pathTo.pop()); else System.out.print(pathTo.pop() + " -> "); } System.out.println(); } }
4 检测有向图的环排序
检测有向图是否包含环,检测图没有环是拓扑排序的前提条件。
多数状况下,须要知道有向图是否包含环,而且输出够成环的边。递归
/** * 基于深度优先搜索检测图中是否包含环 */ public class DirectedCycle { private boolean[] onStack; private Stack<Integer> cycle; private int[] edge; private boolean[] marked; public DirectedCycle(DiGraph G) { onStack = new boolean[G.V()]; edge = new int[G.V()]; marked = new boolean[G.V()]; for (int i = 0; i < G.V(); i++) { if(!marked[i]) dfs(G, i); } } private void dfs(DiGraph G, int v) { onStack[v] = true; marked[v] = true; for (int w : G.adj(v)) { if (this.hasCycle()) return; else if (!marked[w]){ edge[w] = v; dfs(G, w); } // onStack[w]为true代表,当前v节点是一条通过w的抵达,代表w -> v有路径 // 因为v -> w有边,所以必为环 else if(onStack[w]){ cycle = new Stack<>(); for (int x = v; x != w; x=edge[x]) cycle.push(x); cycle.push(w); cycle.push(v); } } onStack[v] = false; } public boolean hasCycle(){ return cycle != null; } public Iterable<Integer> cycle() { return cycle; } public static void main(String[] args) { // 输入用例参加附录1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 输出结果参加附录6 DirectedCycle directedCycle = new DirectedCycle(diGraph); System.out.println("有向图是否包含环: " + (directedCycle.hasCycle() ? "是" : "否")); if (directedCycle.hasCycle()){ System.out.print("其中一条环为:"); for (int i : directedCycle.cycle()) { System.out.print(i + " "); } } System.out.println(); } }
5 顶点的深度优先次序
顶点的深度优先次序分为前序、后序和逆后续,区别是记录点的时机发生在递归调用的前仍是后。该算法产生的pre、post和reversePost
顺序在图的高级算法中十分有用。
public class DepthFirstOrder { private boolean[] marked; private ArrayDeque<Integer> pre; // 保存前序遍历的结果 private ArrayDeque<Integer> post; // 保存后序的遍历结果 private ArrayDeque<Integer> reversePost; //保存逆后序的遍历结果 public DepthFirstOrder(DiGraph G) { marked = new boolean[G.V()]; pre = new ArrayDeque<>(); post = new ArrayDeque<>(); reversePost = new ArrayDeque<>(); for (int v=0; v<G.V(); v++) if (!marked[v]) dfs(G, v); } private void dfs(DiGraph G, int v) { marked[v] = true; pre.add(v); for (int w : G.adj(v)) if(!marked[w]) dfs(G, w); post.add(v); // 按post的倒序保存 reversePost.addFirst(v); } public Iterable<Integer> pre(){ return pre; } public Iterable<Integer> post(){ return post; } public Iterable<Integer> reversePost(){ return reversePost; } public static void main(String[] args) { // 构造无环图的输入参见附录7 DiGraph diGraph = new DiGraph(new Scanner(System.in)); DepthFirstOrder depthFirstOrder = new DepthFirstOrder(diGraph); // 输出结果参加附录8 // 注意:对于同一幅图,构造图的输入顺序不一致 // 会致使输出不相同 System.out.print("前序节点顺序: "); for (int v : depthFirstOrder.pre()) System.out.print(v + " "); System.out.println(); System.out.print("后续节点顺序:"); for (int v : depthFirstOrder.post()) System.out.print(v + " "); System.out.println(); System.out.print("逆后序节点顺序:"); for (int v : depthFirstOrder.reversePost()) System.out.print(v + " "); } }
6 拓扑排序
给定一幅有向图,给出一组顶点排序,在有向图中,全部的边均是前面的点指向后面的点。
拓扑排序依赖图的环检测和逆后序遍历算法。
/** * 计算有向无环图中的全部顶点的拓扑排序, * 一般用于解决优先级限制下的调度问题 */ public class Topological { private Iterable<Integer> order; public Topological(DiGraph G) { DirectedCycle directedCycle = new DirectedCycle(G); if(!directedCycle.hasCycle()) order = new DepthFirstOrder(G).reversePost(); } public boolean isDAG(){ return order == null; } public Iterable<Integer> order(){ return order; } public static void main(String[] args) { // 输入用例参考附录7 DiGraph diGraph = new DiGraph(new Scanner(System.in)); Topological topological = new Topological(diGraph); // 输出结果参见附录9 if (topological.isDAG()) System.out.println("有向图带有环,没法进行拓扑排序"); else{ System.out.print("拓扑排序结果:"); for (int v : topological.order()) { System.out.print(v + " "); } } } }
7 强联通检测
若是存在从v至w的路径,同时还存在从w至v的路径,则称v和w之间是强连通;若是一幅有向图中任意两点间都
是强连通,则这幅有向图也是强连通的。检测强连通算法依赖图的反转和逆后序遍历算法。算法比较简洁,可是
理解起来比较难,须要仔细分析理解。
/** * 有向图的强连通性,该算法依赖逆后序排序、图的反转、无向图的联通性算法 */ public class SCC { private int[] id; private int count; private boolean[] marked; public SCC(DiGraph G) { id = new int[G.V()]; marked = new boolean[G.V()]; DepthFirstOrder depthFirstOrder = new DepthFirstOrder(G.reverse()); for (int v : depthFirstOrder.reversePost()) if(!marked[v]) { dfs(G, v); count++; } } private void dfs(DiGraph G, int v) { id[v] = count; marked[v] = true; for (int w : G.adj(v)) if(!marked[w]) dfs(G, w); } // 两点是不是强连通 public boolean stronglyConnected(int v, int w){ return id[v] == id[w]; } // 强联通份量数 public int count(){ return count; } // 节点所在的联通份量标识符 public int id(int v){ return id[v]; } public static void main(String[] args) { // 带环的图,输入用例参见附录1 DiGraph diGraph = new DiGraph(new Scanner(System.in)); // 输出结果参见附录10 SCC scc = new SCC(diGraph); System.out.println("有向图中强连通份量数:" + scc.count()); System.out.println("节点6与12是不是强连通:" + (scc.stronglyConnected(6, 12) ? "是" : "否")); System.out.println("节点9与12是不是强连通:" + (scc.stronglyConnected(9, 12) ? "是" : "否")); System.out.println("输出联通份量"); for (int i = 0; i < scc.count(); i++) { for (int v = 0; v < diGraph.V(); v++) { if(scc.id[v] == i) System.out.print(v + " "); } System.out.println(); } } }
附录1,有向图构造数据
13 22 4 2 2 3 3 2 6 0 0 1 2 0 11 12 12 9 9 10 9 11 8 9 10 12 11 4 4 3 3 5 7 8 8 7 5 4 0 5 6 4 6 9 7 6
附录2,有向图输出
V: 13 E: 22 0: 1 5 1: 2: 3 0 3: 2 5 4: 2 3 5: 4 6: 0 4 9 7: 8 6 8: 9 7 9: 10 11 10: 12 11: 12 4 12: 9
附录3:有向图的可达性测试
顶点2到达的点 0 1 2 3 4 5 1,2,6可以到达的点 0 1 2 3 4 5 6 9 10 11 12
附录4:基于深度优先搜索的单向路径测试结果
顶点0可达的点: 0 1 2 3 4 5 0至12不存在有向路径 0至3的一条有向路径: 0 -> 5 -> 4 -> 2 -> 3
附录5:基于广度优先搜索的最短路径测试结果
顶点0可达的点: 0 1 2 3 4 5 0至12不存在有向路径 0至3的一条有向路径: 0 -> 5 -> 4 -> 3
附录6:检测环算法的测试输出
有向图是否包含环: 是 其中一条环为:3 2 4 5 3
附录7:构造无环图的输入用例
13 15 0 1 0 5 0 6 2 0 2 3 3 5 5 4 6 4 6 9 7 6 8 7 9 10 9 11 9 12 11 12
附录8:深度优先遍历图的输出结果
前序节点顺序: 0 1 5 4 6 9 10 11 12 2 3 7 8 后续节点顺序:1 4 5 10 12 11 9 6 0 3 2 7 8 逆后序节点顺序:8 7 2 3 0 6 9 11 12 10 5 4 1
附录9:拓扑排序测试输出结果
拓扑排序结果:8 7 2 3 0 6 9 11 12 10 5 4 1
附录10:带环有向图的强连通性测试输出结果
有向图中强连通份量数:5 节点6与12是不是强连通:否 节点9与12是不是强连通:是 输出联通份量 1 0 2 3 4 5 9 10 11 12 6 7 8