DAG vs Topological Sort - tenji/ks GitHub Wiki

有向无环图 & 拓扑排序

一、有向无环图

在图论中,如果一个有向图从任意顶点出发无法经过若干条边回到该点,则这个图是一个有向无环图(英语:Directed Acyclic Graph,缩写:DAG)。

有向无环图和树是什么关系?

因为有向无环图中从一个点到另一个点有可能存在两种路线,因此有向无环图未必能转化成树,但任何有向树均为有向无环图。

更多关于图的内容,传送门

二、拓扑排序

2.1 Kahn 算法

Kahn 是广度优先搜索(BFS)吗?

Kahn's algorithm is almost a BFS.

过程

  1. 识别没有入度的顶点;
  2. 将该顶点添加到队列;
  3. 将该顶点从图中删除,对应的就是出队列以及依赖该顶点的顶点入度减一;
  4. 重复以上流程。

以上循环会在不再有入度为零的顶点时结束。发生这种情况的原因有两个:

  1. 没有剩余顶点。我们已将它们全部从图中取出,并将它们添加到拓扑排序中;
  2. 还剩下一些顶点,但它们都有传入边(Incoming Edge)。这意味着该图有环,并且不存在拓扑排序。

伪代码

L ← Empty list that will contain the sorted elements
S ← Set of all nodes with no incoming edges
while S is not empty do
    // 入度为零的顶点出队
    remove a node n from S
    insert n into L
    for each node m with an edge e from n to m do
        // 将该顶点从图中删除,对应的就是出队列以及依赖该顶点的顶点入度减一
        remove edge e from the graph
        // 没有入度的顶点加入队列
        if m has no other incoming edges then
            insert m into S
if graph has edges then
    return error (graph has at least one cycle)
else
    return L (a topologically sorted order)

复杂度

假设 E 表示边的数量;V 表示节点的数量。

  • 时间复杂度:O(E + V)
  • 空间复杂度:O(V)

代码实现

使用邻接表存储图,关于邻接表,传送门

import java.util.*;

public class Main {

    // Function to return array containing vertices in
    // Topological order.
    public static int[] topologicalSort(List<List<Integer> > adj, int V)
    {
        // Array to store indegree of each vertex
        int[] indegree = new int[V];
        for (int i = 0; i < V; i++) {
            for (int it : adj.get(i)) {
                indegree[it]++;
            }
        }

        // Queue to store vertices with indegree 0
        Queue<Integer> q = new LinkedList<>();
        for (int i = 0; i < V; i++) {
            if (indegree[i] == 0) {
                q.offer(i);
            }
        }

        int[] result = new int[V];
        int index = 0;
        while (!q.isEmpty()) {
            int node = q.poll();
            result[index++] = node;

            // Decrease indegree of adjacent vertices as the
            // current node is in topological order
            for (int it : adj.get(node)) {
                indegree[it]--;

                // If indegree becomes 0, push it to the
                // queue
                if (indegree[it] == 0) {
                    q.offer(it);
                }
            }
        }

        // Check for cycle
        if (index != V) {
            System.out.println("Graph contains cycle!");
            return new int[0];
        }

        return result;
    }

    public static void main(String[] args)
    {
        // Number of nodes
        int n = 6;

        // Edges
        int[][] edges = { { 0, 1 }, { 1, 2 }, { 2, 3 },
                          { 4, 5 }, { 5, 1 }, { 5, 2 } };

        // Graph represented as an adjacency list
        List<List<Integer> > adj = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            adj.add(new ArrayList<>());
        }

        // Constructing adjacency list
        for (int[] edge : edges) {
            adj.get(edge[0]).add(edge[1]);
        }

        // Performing topological sort
        System.out.println("Topological sorting of the graph: ");
        int[] result = topologicalSort(adj, n);

        // Displaying result
        for (int i : result) {
            System.out.print(i + " ");
        }
    }
}

2.2 DFS 算法

过程

  1. 创建一个具有 n 个顶点和 m 向边的图;
  2. 初始化一个堆栈和一个大小为 n 的数组记录已访问的顶点;
  3. 对于图中每个未访问的顶点,执行以下操作:
    • 以顶点为参数调用 DFS 函数;
    • 在 DFS 函数中,将顶点标记为已访问,并对顶点的所有未访问邻居递归调用 DFS 函数;
    • 一旦访问了所有邻居,就将顶点压入堆栈。
  4. 操作完成之后,顶点已被访问过,从堆栈中弹出元素并将它们追加到输出列表中,直到堆栈为空;
  5. 结果列表是图的拓扑排序顺序。

伪代码

L ← Empty list that will contain the sorted nodes
while exists nodes without a permanent mark do
    select an unmarked node n
    visit(n)

function visit(node n)
    if n has a permanent mark then
        return
    if n has a temporary mark then
        stop   (graph has at least one cycle)

    mark n with a temporary mark

    for each node m with an edge from n to m do
        visit(m)

    mark n with a permanent mark
    add n to head of L

复杂度

假设 E 表示边的数量;V 表示节点的数量。

  • 时间复杂度:O(E + V)
  • 空间复杂度:O(V)

代码实现

import java.util.*;

public class TopologicalSort {

    // Function to perform DFS and topological sorting
    static void topologicalSortUtil(int v, List<List<Integer> > adj,
                        boolean[] visited,
                        Stack<Integer> stack)
    {
        // Mark the current node as visited
        visited[v] = true;

        // Recur for all adjacent vertices
        for (int i : adj.get(v)) {
            if (!visited[i])
                topologicalSortUtil(i, adj, visited, stack);
        }

        // Push current vertex to stack which stores the
        // result
        stack.push(v);
    }

    // Function to perform Topological Sort
    static void topologicalSort(List<List<Integer> > adj, int V)
    {
        // Stack to store the result
        Stack<Integer> stack = new Stack<>();
        boolean[] visited = new boolean[V];

        // Call the recursive helper function to store
        // Topological Sort starting from all vertices one
        // by one
        for (int i = 0; i < V; i++) {
            if (!visited[i])
                topologicalSortUtil(i, adj, visited, stack);
        }

        // Print contents of stack
        System.out.print("Topological sorting of the graph: ");
        while (!stack.empty()) {
            System.out.print(stack.pop() + " ");
        }
    }

    // Driver code
    public static void main(String[] args)
    {
        // Number of nodes
        int V = 4;

        // Edges
        List<List<Integer> > edges = new ArrayList<>();
        edges.add(Arrays.asList(0, 1));
        edges.add(Arrays.asList(1, 2));
        edges.add(Arrays.asList(3, 1));
        edges.add(Arrays.asList(3, 2));

        // Graph represented as an adjacency list
        List<List<Integer> > adj = new ArrayList<>(V);
        for (int i = 0; i < V; i++) {
            adj.add(new ArrayList<>());
        }

        for (List<Integer> i : edges) {
            adj.get(i.get(0)).add(i.get(1));
        }

        topologicalSort(adj, V);
    }
}

拓扑排序的结果顺序唯一吗?

一般情况下,拓扑排序并非唯一。有向无环图仅仅在存在一条路径可以包含其所有顶点的情况下,有唯一的拓扑排序方式,这时,拓扑排序与它们在这条路径中出现的顺序相同。

三、Leetcode 题目

∞、参考链接

⚠️ **GitHub.com Fallback** ⚠️