本章继续介绍一些图相关的重要算法。

拓扑排序

课程表

共有 numCourses 门课可选,即为 0 到 numsCourses - 1。
给定数组 prerequisites,其中 prerequisites[i] = [a, b],表示选修课程a之前必须选修b。
返回学完所有课程的学习顺序,如果有多种顺序,返回任意一种即可。如果不可能完成所有课程,返回空数组。

输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]

本题是典型的拓扑排序问题,首先要解决有没有解的问题。
把每门课看做一个节点,必须先修的课伸出一条边指向后修的课,假如图中出现环,
比如选修a之前必须选修b,而选修b之前必须选修a,这样两门课都不可能修上,所以有环的图是一定没有拓扑排序的。
我们可以用深度搜索来查找有没有环,可以在代码中看到。有解的情况下,拓扑排序就是图的逆后序,即后序遍历反转。
以numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]为例。

上图的一个后序遍历为[3,1,2,0],则逆后序为[0,2,1,3]。

// https://leetcode.cn/problems/course-schedule-ii/submissions/612543424
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        boolean[] visited = new boolean[numCourses];
        List<Integer> onStack = new ArrayList<>();
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }
        for (int[] pair : prerequisites) {
            graph.get(pair[1]).add(pair[0]);
        }
        for (int i = 0; i < numCourses; i++) {
            if (!visited[i]) {
                if (hasCycle(i, visited, onStack, graph)) {
                    return new int[]{};
                }
            }
        }

        Deque<Integer> topoOrder = new ArrayDeque<>();
        for (int i = 0; i < numCourses; i++) {
            visited[i] = false;
        }

        for (int i = 0; i < numCourses; i++) {
            if (!visited[i]) {
                reversePost(i, visited, graph, topoOrder);
            }
        }

        int[] result = new int[topoOrder.size()];
        for (int i = 0; i < result.length; i++) {
            result[i] = topoOrder.pollFirst();
        }

        return result;
    }

    private boolean hasCycle(int i , boolean[] visited, List<Integer> onStack, List<List<Integer>> graph) {
        visited[i] = true;
        onStack.add(i);
        for (int desc : graph.get(i)) {
            if (!visited[desc]) {
                if (hasCycle(desc, visited, onStack, graph)) {
                    onStack.remove(onStack.size() - 1);
                    return true;
                }
            } else {
                if (onStack.contains(desc)) {
                    onStack.remove(onStack.size() - 1);
                    return true;
                }
            }
        }
        onStack.remove(onStack.size() - 1);
        return false;
    }

    private void reversePost(int i, boolean[] visited, List<List<Integer>> graph, Deque<Integer> topoOrder) {
        visited[i] = true;
        for (int desc : graph.get(i)) {
            if (!visited[desc]) {
                reversePost(desc, visited, graph, topoOrder);
            }
        }
        topoOrder.addFirst(i);
    }

最小生成树

连接所有点的最小费用


给定points数组,表示二维平面上的点,
点之间的距离为 曼哈顿距离:|x0 - x1| + |y0 - y1|。
返回构成一棵树的最小总费用,设点有n个,树的边有n - 1条,且没有环。

这里我们介绍Prime算法的即时版本,以数组[[0,0],[1,1],[1,0],[2,1]]为例。
首先我们将所有节点按是否在最小生成树中分为两个集合,数组distTo[i]记录点i到最小生成树中点的最小距离,初始为无穷大。

  • 首先将点0放入生成树中,distTo[0]设为0,更新所有相邻节点的distTo值,选其中最小的边对应的点作为下一个加入生成树。
  • 加入点2,更新distTo[1]为1,因为边(2,1)更短,同理更新distTo[3]为2,选取最小边(2,1)加入生成树。
  • 加入点1,更新distTo[3]为1,将边(1,3)加入生成树。
  • 加入点3,最小生成树形成,边长总和为1 + 1 + 1 = 3。
// https://leetcode.cn/problems/min-cost-to-connect-all-points/submissions/612678927
    public int minCostConnectPoints(int[][] points) {
        int n = points.length;
        boolean[] marked = new boolean[n];
        Integer[] edgeTo = new Integer[n];
        int[] distTo = new int[n];
        int sum = 0;
        TreeSet<Integer> unmarkedNodes = new TreeSet<>((a, b) -> 
            distTo[a] != (distTo[b]) ? Integer.compare(distTo[a], distTo[b])
                : Integer.compare(a, b));
        distTo[0] = 0;
        edgeTo[0] = 0;
        unmarkedNodes.add(0);
        while (!unmarkedNodes.isEmpty()) {
            int minNode = unmarkedNodes.pollFirst();
            visit(minNode, marked, unmarkedNodes, points, edgeTo, distTo);
            sum += distTo[minNode];
        }

        return sum;
    }

    private void visit(int i, boolean[] marked, TreeSet<Integer> unmarkedNodes, int[][] points, Integer[] edgeTo, int[] distTo) {
        marked[i] = true;
        for (int j = 0; j < points.length; j++) {
            if (j != i && !marked[j]) {
                int dist = dist(points[i][0], points[i][1], points[j][0], points[j][1]);
                if (edgeTo[j] == null || dist < distTo[j]) {
                    edgeTo[j] = i;
                    if (unmarkedNodes.contains(j)) {
                        unmarkedNodes.remove(j);
                    }
                    distTo[j] = dist;
                    unmarkedNodes.add(j);
                }
            }
        }
    }


    private int dist(int x0, int y0, int x1, int y1) {
        return (int)(Math.abs(x1 - x0) + Math.abs(y1 - y0));
    }

注意TreeSet的比较器,TreeSet中存储的是节点序号,但是按distTo值排序,如果直接比较distTo的值,相等时会相互覆盖,
所以distTo相等时还需要比较节点序号,保证不同节点不会相互覆盖。
更新distTo值时,也要先删除TreeSet中对应的节点,更新完再插入,不然排序会混乱。

最短路径

这里限定为求解权值非负的有向图,因为如果存在负权重环(即权重和为负的环),就没有最短路径,因为每绕环走一圈都会减小总权重。
这里通过下图介绍dijkstra算法,求非负权重有向图(可包含环)的最短路径。

  • 先将点0加入visited数组中,设置distTo[0]=0,对0的每条连接到未遍历点的边作松弛操作,即如果distTo[0]加上边的权重weight小于distTo[i],i为另一个点,更新distTo[i]=distTo[0]+weight,这里distTo[1]和distTo[2]被更新,在数组中被标为黄色
  • 选择distTo值最小的未遍历点加入visited数组,即点1,松弛点1的边,更新dist[2]和dist[3]
  • 将点2加入visited数组,没有可松弛的边
  • 将点3加入visited数组,distTo数组即点0到每个点的最短路径
// https://leetcode.cn/problems/design-graph-with-shortest-path-calculator/submissions/612817157/
class Graph {
    private static class IndexPQ<V extends Comparable<V>> {
        V[] keys;
        int[] heap, rank;
        int size;
        public IndexPQ(int cap) {
            size = 0;
            keys = (V[])new Comparable[cap];
            heap = new int[cap];
            rank = new int[cap];
        }

        public void insert(int i, V v) {
            if (contains(i)) {
                return;
            }
            keys[i] = v;
            heap[size] = i;
            rank[i] = size;
            size++;
            swim(size - 1);
        }

        public boolean contains(int i) {
            return keys[i] != null;
        }

        public void change(int i, V v) {
            if (!contains(i)) {
                return;
            }
            keys[i] = v;
            sink(rank[i]);
            swim(rank[i]);
        }

        public int delMin() {
            int min = heap[0];
            swap(0, size - 1);
            size--;
            sink(0);

            keys[min] = null;
            return min;
        }

        public boolean isEmpty() {
            return size == 0;
        }

        private void swim(int i) {
            while (i != 0 && keys[heap[i]].compareTo(keys[heap[(i - 1)/ 2]]) < 0) {
                swap(i, (i - 1) / 2);
                i = (i - 1) / 2;
            }
        }

        private void sink(int i) {
            while (i + i + 1 < size) {
                int next = i + i + 1;
                if (next + 1 < size && keys[heap[next + 1]].compareTo(keys[heap[next]]) < 0) {
                    next++;
                }
                if (keys[heap[i]].compareTo(keys[heap[next]]) <= 0) {
                    return;
                }
                swap(i, next);
                i = next;
            }
        }

        private void swap(int i, int j) {
            int tmp = heap[i];
            heap[i] = heap[j];
            heap[j] = tmp;
            rank[heap[i]] = i;
            rank[heap[j]] = j;
        }
    }

    private List<List<int[]>> adjList;

    public Graph(int n, int[][] edges) {
        adjList = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            adjList.add(new ArrayList<>());
        }
        for (int[] edge : edges) {
            addEdge(edge);
        }
    }
    
    public void addEdge(int[] edge) {
        adjList.get(edge[0]).add(new int[]{edge[1], edge[2]});
    }
    
    public int shortestPath(int node1, int node2) {
        Integer[] distTo = new Integer[adjList.size()];
        distTo[node1] = 0;
        for (int i = 0; i < distTo.length; i++) {
            distTo[i] = i == node1 ? 0 : Integer.MAX_VALUE;
        }
        IndexPQ<Integer> indexPQ = new IndexPQ<>(adjList.size());
        indexPQ.insert(node1, 0);
        while (!indexPQ.isEmpty()) {
            int minNode = indexPQ.delMin();
            if (minNode == node2) {
                return distTo[node2];
            }
            relax(minNode, adjList, distTo, indexPQ);
        }

        return -1;
    }

    private void relax(int node, List<List<int[]>> adjList, Integer[] distTo,  IndexPQ<Integer> indexPQ) {
        for (int[] edge : adjList.get(node)) {
            if (distTo[edge[0]] > distTo[node] + edge[1]) {
                distTo[edge[0]] = distTo[node] + edge[1];
                if (indexPQ.contains(edge[0])) {
                    indexPQ.change(edge[0], distTo[edge[0]]);
                } else {
                    indexPQ.insert(edge[0], distTo[edge[0]]);
                }
            }
        }
    }
}

上方代码实现了一个可以求最短路径的图类,用IndexPQ代替TreeSet,IndexPQ的具体实现可以开专栏来讲。