有向图
有向图同无向图的区别为每条边带有方向,表明从一个顶点至另一个顶点可达。有向图的算法多依赖深度搜索算法。
本文主要介绍有向图的基本算法,涉及图的表示、可达性、检测环、图的遍历、拓扑排序以及强连通检测等算法。
本文的有向图特指有向无权图
1 定义有向图
采用邻接表结构存储边信息,同时提供reverse接口生成反向图,倒置每个边的方向,该接口在后续其他算法中会用到。
/**
* 采用邻接表表示的有向图
*/
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 有向图的可达性
有向图的可达性是指给定一个或一组顶点,判断是否可以到达图中其他顶点。垃圾清除常见算法“标记-清除”算法中,采用有向图的可达性算法
标记所有可以被访问的对象,然后在回收阶段,仅仅回收那些未被标记的对象。
/**
* 基于深度优先的有向图可达性算法
* 求出给定顶点或一组顶点,有向图中能到达的点
*/
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 单点有向路径和单点最短有向路径
分别采用深度优先搜索和广度优先搜索实现
有向图的路径
/**
* 单点有向路径,给定顶点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();
}
}
有向图的最短路径,基于广度优先算法
/**
* 基于广度优先搜索的单向路径算法;
* 在此方法下,求得的路径为最短路径(忽略边权重)
*/
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