有向图的基本算法-Java实现

有向图
有向图同无向图的区别为每条边带有方向,表明从一个顶点至另一个顶点可达。有向图的算法多依赖深度搜索算法。
本文主要介绍有向图的基本算法,涉及图的表示、可达性、检测环、图的遍历、拓扑排序以及强连通检测等算法。

1 定义有向图

采用邻接表结构存储边信息,同时提供reverse接口生成反向图,倒置每个边的方向,该接口在后续其他算法中会用到。

/**
 * 采用邻接表表示的有向图
 */
public class DiGraph {
    private final int V;
    private int E;
    private ArrayList[] 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 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 pathTo(int v)
    {
        Stack 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 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 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 pathTo(int v)
    {
        Stack 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 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 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 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 pre;    // 保存前序遍历的结果
    private ArrayDeque post;   // 保存后序的遍历结果
    private ArrayDeque reversePost;    //保存逆后序的遍历结果
    public DepthFirstOrder(DiGraph G)
    {
        marked = new boolean[G.V()];
        pre = new ArrayDeque<>();
        post = new ArrayDeque<>();
        reversePost = new ArrayDeque<>();
        for (int v=0; v pre(){ return pre; }
    public Iterable post(){ return post; }
    public Iterable 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 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 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,有向图构造数据

有向图的基本算法-Java实现_第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:构造无环图的输入用例

有向图的基本算法-Java实现_第2张图片

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

你可能感兴趣的:(有向图的基本算法-Java实现)