有向图的可达性(遍历)

与无向图不同,有向图的遍历(或者叫排列)基于DFS,分为以下三种:

前序遍历:根最先
后序遍历:根最后
逆后序遍历(拓扑排序)
  • 拓扑排序是将DAG中所有顶点排成一个线性序列,使得图中任意一对顶点 u 和 v,若边<u,v>∈E(G),则 u 在线性序列中出现在 v 之前;
  • 定理1:当且仅当一幅有向图是无环有向图(DAG)时它才能进行拓扑排序(即拓扑有序);
  • 定理2:一幅有向无环图的拓扑顺序即为所有顶点的逆后续排列;
  • 注意:在拓扑排序之前必须检查图中有没有环,如果有则返回null

最短路径树(SPT)

单源最短路径算法(既不能处理负权边,更不能处理负权环):Dijkstra
  • Dij算法的思想其实跟prim算法类似,两种贪心算法都会用添加边的方式构造一棵树:Prim算法每次添加的是离树最近的的非树顶点,Dijkstra算法每次添加的都是离起点最近的非树顶点。同样对应于Prim算法的延时实现和即时实现,Dijkstra算法也有延时(优先队列)和即时(索引优先队列)两种实现,他们代码中的唯一区别就是Dijkstra多了一个松弛的操作。
  • Dij算法的基础在于松弛,根据距离起点的远近依次检查路径,即假设添加一条边总会使路径变得更长,而负权边的存在破坏了这个基础,所以Dij算法不能处理负权边。
  • Dij的算法复杂度:在一幅含有V个顶点,E条边的加权有向图中,使用Dij算法所需空间与V成正比,所需时间与ElogV成正比。
单源最短路径算法(即可以处理负权边,也可以处理负权环):Bellman-Ford
  • 如果s -> v的有向路径上的任意一点m处于负权环上,则讨论s -> v的最短路径没有意义,因为m -> m可以构造任意小的路径;只有加权有向图中至少存在一条s -> v的有向路径并且所有s -> v的有向路径上的任意顶点都不存在于任何负权环中,s -> v的最短路径才存在。
  • 规定如下:
  • 对于起点不可达的顶点,最短路径为 $ + \infty$
  • 对于起点可达但路径上的某点属于一个负权环的顶点,最短路径为 $ - \infty$
  • 对于其他顶点,计算最短路径的权重以及最短路径树
  • 换言之,只有存在最短路径BF算法才会得出解,即给定源点 s,s无法到达任何负权环才能有最短路径解,否则检测到可达的负权环则程序终止,避免陷入死循环。
  • 对于一个含有 V 个顶点 E 条边的加权有向图以任意顺序放松有向图的所有边,重复 V 轮;在最坏情况下基于队列的Bellman-Ford算法解决最短路径问题(或者找到从源点可达的负权环),所需的时间与 EV成正比,空间和V成正比。
完全最短路径算法(可以处理负权边,但不能处理负权环):Flyod

一种基于动态规划的多源最短路算法,其核心代码就是三层循环。算法复杂度较高,对于一个含有N个点的图,时间复杂度与$N^3$成正比。

补充:加权有向无环图的最优SPT算法:拓扑排序+松弛操作
  • 相比Dij算法,AcyclicSP算法只需要按照拓扑顺序依次松弛每个点,每个点只会被松弛一次,因为当 from 被放松后,一定成立 distTo[from] + edge.getWeight() >= distTo[to],在算法结束前都成立;因为 from 松弛后, distTo[from] 就不会变了,而 distTo[to] 只会变小,所以当按照拓扑顺序松弛完所有的点后,最短路径就生成了。
  • 这个算法的复杂度是线性的E+V

实现

/*
加权有向图的实现比加权无向图更加简单一些,区别就是每个点只存储以该点为起点的边
 */

import java.util.*;

public class DirectedGraph {
    // 这里为了简单,用0开始连续的数字代表顶点(顶点ID),实际的实现中应该设计成类 Node 的形式
    private final int V;      // 顶点数目
    private int E;            // 边的数目
    private boolean[] marked; // 用于遍历时标记用
    private HashMap<Integer, TreeSet<DirectedEdge>> adj; // 邻接表用Hash表实现,key=Node_ID, value=相邻的边组成的链表(这里的实现是红黑二叉树)

    public DirectedGraph(int v) {
        this.V = v;
        this.E = 0;
        this.adj = new HashMap<>();
        // 初始化哈希表
        for (int i = 0; i < V; i++) {
            adj.put(i,new TreeSet<>());
        }
    }

    public int getV() {
        return V;
    }

    public int getE() {
        return E;
    }

    // 通过起点和终点返回边
    public DirectedEdge getEdge(int from, int to) {
        for (DirectedEdge e : adj.get(from))
            if (e.to == to)
                return e;
        return null;
    }

    // 只有这里与无向图不同
    public void addEdge(DirectedEdge e) {
        adj.get(e.from).add(e); // 只添加作为起点的边
        E++;
    }

    // 返回 v 点所有的连接边,Iterable<Edge>方便遍历
    Iterable<DirectedEdge> adj(int v) {
        return adj.get(v);
    }

    // 返回加权无向图中所有的边
    public Iterable<DirectedEdge> edges() {
        TreeSet<DirectedEdge> edges = new TreeSet<>();
        for (Map.Entry<Integer,TreeSet<DirectedEdge>> entry : adj.entrySet())
            edges.addAll(entry.getValue());
        return edges;
    }

    /*
    有向图的可达性:与无向图不同,有向图的遍历(或者叫排列),基于无向图的DFS,分为以下三种
     */
    // DFS
    // 1. preOrder                            前序遍历(DFS):递归调用之前将顶点加入队列(先入先出,为了正序输出)
    LinkedList<Integer> preQueue;
    // 2. postOrder                           后序遍历:递归调用之后将顶点加入队列(先入先出,为了正序输出)
    LinkedList<Integer> postQueue;
    // 3. reversePostOrder(topologicalOrder) 逆后序遍历:递归调用之后将顶点压入栈(先入后出,为了逆序输出)
    LinkedList<Integer> reversePostStack;
    /*
     4. TopologicalOrder 拓扑排序:是将DAG中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前
     定理1:当且仅当一幅有向图是无环有向图(DAG)时它才能进行拓扑排序(即拓扑有序)
     定理2:一幅有向无环图的拓扑顺序即为所有顶点的逆后续排列
     所以在拓扑排序之前必须检查图中有没有环,如果有则返回null
     */

    public void depthFirstSearch() {
        marked = new boolean[V]; // 每次遍历之前懒惰初始化,每次都保证初始全是false
        preQueue = new LinkedList<>();
        postQueue = new LinkedList<>();
        reversePostStack = new LinkedList<>(); // 无环时是拓扑排序,当有环时返回null
        dfs(0);

        // 判断有没有环,有环返回null,但实际中应该先判断有没有环,这里只是为了方便一起写
        EdgeWeightedDirectedCycle cycle = new EdgeWeightedDirectedCycle(this); // 注意后面的判断环的dfs方法里加了负权重环的判断,在这里应该删除
        if (cycle.hasCycle())
            reversePostStack = null;
    }

    private void dfs(int root) {
        preQueue.offer(root);
        marked[root] = true;
        for (DirectedEdge edge : this.adj(root)) {
            if (!marked[edge.to])
                dfs(edge.to);
        }
        postQueue.offer(root);
        reversePostStack.push(root);
    }

    @Override
    public String toString() {
        return "UndirectedGraph{" +
                "V=" + V +
                ", E=" + E +
                ", adj=" + adj +
                '}';
    }
}

class DirectedEdge implements Comparable<DirectedEdge> {
    int from;
    int to;
    double weight;
    public DirectedEdge(int from, int to, double weight) {
        this.from = from;
        this.to = to;
        this.weight = weight;
    }

    public int getFrom() {
        return from;
    }

    public int getTo() {
        return to;
    }

    public double getWeight() {
        return weight;
    }

    // 这里定义大小关系,平行边定义为相等,因为前面用的set存邻接边,即不允许添加平行边
    @Override
    public int compareTo(DirectedEdge that) {
        if (this.from == that.from && this.to == that.to)
            return 0;
        else if (this.weight < that.weight)
            return -1;
        else if (this.weight > that.weight)
            return 1;
        else
            return 1;
    }

    @Override
    public String toString() {
        return "DirectedEdge{" +
                "from=" + from +
                ", to=" + to +
                ", weight=" + weight +
                '}';
    }
}


/**
 * 有向加权图最短路径树(SPT):
 *      1. 单源最短路径:Dijkstra
 *      2. 单源最短路径:Bellman-Ford
 *      3. 完全最短路径:Flyod
 *      4. 补充:加权有向无环图的最优SPT算法:拓扑排序+松弛操作
 */

/**
 * Dij算法的思想其实跟prim算法类似,两种算法都会用添加边的方式构造一棵树:Prim算法每次添加的是离树最近的的非树顶点,
 * Dijkstra算法每次添加的都是离起点最近的非树顶点。同样对应于Prim算法的延时实现和即时实现,Dijkstra算法也有延时(优先队列)
 * 和即时(索引优先队列)两种实现,他们代码中的唯一区别就是Dijkstra多了一个松弛的操作。
 */
class Dijkstra {
    private DirectedEdge[] edgeTo; // 到此点路径(指向此点的边),为了记录路径
    private double[] distTo;       // 到此点的路径权重
    private IndexMinPriorityQueue indexMinPq; // 索引优先队列,将顶点ID和到此点的路径权重关联(即时实现)

    public Dijkstra(DirectedGraph graph, int s) {
        edgeTo = new DirectedEdge[graph.getV()];
        distTo = new double[graph.getV()];
        indexMinPq = new IndexMinPriorityQueue<>(graph.getV());
        for (int v = 0; v < graph.getV(); v++)
            distTo[v] = Double.MAX_VALUE; // 先将最短距离置为最大值
        distTo[s] = 0.0; // 设置源点距离为0

        indexMinPq.insert(s ,0.0); // 用顶点s和权重0初始化indexPq
        while (!indexMinPq.isEmpty())
            relax(graph, indexMinPq.delMin());
    }

    // 工具函数:松弛操作,并更新索引优先队列
    private void relax(DirectedGraph graph, int v) {
        for (DirectedEdge edge : graph.adj(v)) { // 遍历所有邻接边
            int w = edge.to;
            if (distTo[edge.from] + edge.getWeight() < distTo[w]) { // 这里多加了edge.getWeight()进行松弛
                edgeTo[w] = edge;
                distTo[w] = distTo[edge.from] + edge.getWeight();
                if (indexMinPq.contains(w))  // 找到非树上的点离源点更近的点,更新或添加
                    indexMinPq.change(w,distTo[w]); // 更新
                else
                    indexMinPq.insert(w,distTo[w]); // 添加
            }
        }
    }

    // 查询最短路径长度
    public double distTo(int v) {
        return distTo[v];
    }

    // 查询是否存在最短路径
    public boolean hasPathTo(int v) {
        return distTo[v] < Double.MAX_VALUE;
    }

    // 查询最短路径
    public Iterable<DirectedEdge> pathTo(int v) {
        if (!hasPathTo(v)) return null;
        LinkedList<DirectedEdge> pathStack = new LinkedList<>();
        for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from])
            pathStack.push(e);
        return pathStack;
    }
}

/**
 * AcyclicSP算法(仅适用于加权DAG);
 * 相比Dijkstra算法,只需要按照拓扑顺序依次松弛每个点,每个点只会被松弛一次,因为当from被放松后,一定成立 distTo[from] + edge.getWeight() >= distTo[to];
 * 在算法结束前都成立,因为from松弛后,distTo[from]就不会变了,而distTo[to]只会变小,所以当按照拓扑顺序松弛完所有的点后,最短路径就生成了。
 */
class AcyclicSP {
    private DirectedEdge[] edgeTo; // 到此点路径(指向此点的边)
    private double[] distTo;       // 到此点的路径权重

    /*
    这里是寻找最短路径的实现,寻找最长路径只需要把distTo[]初始化为Double.MIN_VALUE,并改变relax()中不等式的方向即可
     */
    public AcyclicSP(DirectedGraph graph, int s) {
        edgeTo = new DirectedEdge[graph.getV()];
        distTo = new double[graph.getV()];
        for (int v = 0; v < graph.getV(); v++)
            distTo[v] = Double.MAX_VALUE; // 先将最短距离置为最大值
        distTo[s] = 0.0; // 设置源点距离为0

        graph.depthFirstSearch();
        LinkedList<Integer> topological = graph.reversePostStack; // 获取拓扑排列

        for (int v : topological) // 按照拓扑顺序遍历所有的点,每个点只遍历一次
            relax(graph,v);
    }

    // 工具函数:松弛操作
    private void relax(DirectedGraph graph, int v) {
        for (DirectedEdge edge : graph.adj(v)) { // 遍历所有邻接边
            int w = edge.to;
            if (distTo[edge.from] + edge.getWeight() < distTo[w]) { // 找到非树上的点离源点更近的点,更新或添加
                edgeTo[w] = edge;
                distTo[w] = distTo[edge.from] + edge.getWeight();
            }
        }
    }

    // 查询最短路径长度
    public double distTo(int v) {
        return distTo[v];
    }

    // 查询是否存在最短路径
    public boolean hasPathTo(int v) {
        return distTo[v] < Double.MAX_VALUE;
    }

    // 查询最短路径
    public Iterable<DirectedEdge> pathTo(int v) {
        if (!hasPathTo(v)) return null;
        LinkedList<DirectedEdge> pathStack = new LinkedList<>();
        for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from])
            pathStack.push(e);
        return pathStack;
    }
}

/**
 * BF算法
 */
class BellmanFordSP {
    private DirectedEdge[] edgeTo;        // 到此点路径(指向此点的边)
    private double[] distTo;              // 到此点的路径权重
    private LinkedList<Integer> queue;    // 正在被放松的顶点队列
    private boolean[] onQueue;            // 该顶点是否在队列中
    private int cost;                     // relax()被调用的次数
    private Iterable<DirectedEdge> cycle; // edgeTo[]中的边是否形成负权重环

    public BellmanFordSP(DirectedGraph graph, int s) {
        edgeTo = new DirectedEdge[graph.getV()];
        distTo = new double[graph.getV()];
        onQueue = new boolean[graph.getV()];

        for (int v = 0; v < graph.getV(); v++)
            distTo[v] = Double.POSITIVE_INFINITY;  // 正无穷
        distTo[s] = 0.0;

        // Bellman-Ford algorithm:按任意顺序松弛有向图的所有边
        queue = new LinkedList<>();
        queue.offer(s);
        onQueue[s] = true;
        while (!queue.isEmpty() && !hasNegativeCycle()) { // 队列为空代表成功找到了最短路径,如果在V轮后队列非空则一定存在负权环,循环终止
            int v = queue.poll();
            onQueue[v] = false;
            relax(graph, v);
        }
    }

    // 松弛操作
    private void relax(DirectedGraph graph, int v) {
        for (DirectedEdge e : graph.adj(v)) {
            int w = e.to;
            if (distTo[w] > distTo[v] + e.getWeight()) {
                distTo[w] = distTo[v] + e.getWeight();
                edgeTo[w] = e;
                if (!onQueue[w]) { // 保证队列中不含重复的点,并且只将成功放松的边指向的所有顶点加入队列
                    queue.offer(w);
                    onQueue[w] = true;
                }
            }
            if (++cost % graph.getV() == 0) {  // 周期性的检查edgeTo[]表示的子图中是否存在负权重环
                findNegativeCycle();
                if (hasNegativeCycle()) return; // 如果存在环直接返回
            }
        }
    }

    // 查询最短路径长度
    public double distTo(int v) {
        return distTo[v];
    }

    // 查询是否存在最短路径
    public boolean hasPathTo(int v) {
        return distTo[v] < Double.MAX_VALUE;
    }

    // 查询最短路径
    public Iterable<DirectedEdge> pathTo(int v) {
        if (!hasPathTo(v)) return null;
        LinkedList<DirectedEdge> pathStack = new LinkedList<>();
        for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from])
            pathStack.push(e);
        return pathStack;
    }

    public boolean hasNegativeCycle() {
        return cycle != null;
    }

    public Iterable<DirectedEdge> negativeCycle() {
        return cycle;
    }

    // 用edgeTo[]中的边表示一个有向加权图,检查edgeTo[]表示的子图中是否存在负权重环
    private void findNegativeCycle() {
        int V = edgeTo.length;
        DirectedGraph spt = new DirectedGraph(V);
        for (int v = 0; v < V; v++) // 构造一个图
            if (edgeTo[v] != null)
                spt.addEdge(edgeTo[v]);

        EdgeWeightedDirectedCycle finder = new EdgeWeightedDirectedCycle(spt);
        cycle = finder.cycle();
    }
}

/**
 * 暴力 Dijkstra 算法也可以求任意两点间的最短路径
 */
class ViolentDijkstra {
    private DirectedEdge[][] edgeTo; // edgeTo[w][v]: w指向v经过的最后一条边,为了记录路径
    private double[][] distTo;       // distTo[w][v]: w到v点的路径权重

    public ViolentDijkstra(DirectedGraph graph) {
        edgeTo = new DirectedEdge[graph.getV()][graph.getV()];
        distTo = new double[graph.getV()][graph.getV()];
        for (int w = 0; w < graph.getV(); w++)
            for (int v = 0; v < graph.getV(); v++) {
                if (w == v)
                    distTo[w][v] = 0;  // 自己指向自己设置为0,为了下面relax()中初始化
                else
                    distTo[w][v] = Double.MAX_VALUE;
            }

        // Violent Dijkstra algorithm
        for (int w = 0; w < graph.getV(); w++)
            for (int v = 0; v < graph.getV(); v++) {
                relax(graph,w,v);
            }
    }

    // 工具函数:松弛操作
    private void relax(DirectedGraph graph, int w, int v) {
        for (DirectedEdge edge : graph.adj(v)) { // 遍历所有邻接边
            int to = edge.to;
            if (distTo[w][v] + edge.getWeight() < distTo[w][to]) {
                edgeTo[w][to] = edge;
                distTo[w][to] = distTo[w][v] + edge.getWeight();
            }
        }
    }

    // 查询最短路径长度
    public double distTo(int w, int v) {
        return distTo[w][v];
    }

    // 查询是否存在最短路径
    public boolean hasPathTo(int w,int v) {
        return distTo[w][v] < Double.MAX_VALUE;
    }

    // 查询最短路径
    public Iterable<DirectedEdge> pathTo(int w, int v) {
        if (!hasPathTo(w, v)) return null;
        LinkedList<DirectedEdge> pathStack = new LinkedList<>();
        for (DirectedEdge e = edgeTo[w][v]; e != null; e = edgeTo[w][e.from])
            pathStack.push(e);
        return pathStack;
    }
}

/**
 * Flyod算法: 动态规划的思想,由于不是基于松弛操作的,所以可以处理负权边
 */
class Flyod {
    private DirectedEdge[][] edgeTo; // edgeTo[w][v]: w指向v经过的最后一条边,为了记录路径
    private double[][] distTo;       // distTo[w][v]: w到v点的路径权重

    public Flyod(DirectedGraph graph) {
        edgeTo = new DirectedEdge[graph.getV()][graph.getV()];
        distTo = new double[graph.getV()][graph.getV()];

        // 初始化数组
        for (int i = 0; i < graph.getV(); i++)
            for (int j = 0; j < graph.getV(); j++) {
                if (i == j)
                    distTo[i][j] = 0;
                else
                    distTo[i][j] = Double.MAX_VALUE;
            }

        for (DirectedEdge e : graph.edges()) {
            distTo[e.from][e.to] = e.weight;
            distTo[e.to][e.from] = e.weight;
            edgeTo[e.from][e.to] = e;
        }

        // Flyod algorithm
        for (int k = 0; k < graph.getV(); k++)
            for (int w = 0; w < graph.getV(); w++)
                for (int v = 0; v < graph.getV(); v++)
                    if (distTo[w][v] > distTo[w][k] + distTo[k][v]) {
                        distTo[w][v] = distTo[w][k] + distTo[k][v];
                        edgeTo[w][v] = graph.getEdge(k,v);  // 记录边
                    }
    }

    // 查询最短路径长度
    public double distTo(int w, int v) {
        return distTo[w][v];
    }

    // 查询是否存在最短路径
    public boolean hasPathTo(int w,int v) {
        return distTo[w][v] < Double.MAX_VALUE;
    }

    // 查询最短路径
    public Iterable<DirectedEdge> pathTo(int w, int v) {
        if (!hasPathTo(w, v)) return null;
        LinkedList<DirectedEdge> pathStack = new LinkedList<>();
        for (DirectedEdge e = edgeTo[w][v]; e != null; e = edgeTo[w][e.from])
            pathStack.push(e);
        return pathStack;
    }
}

// 测试类
class TestDirectedGraph {
    // test UndirectedGraph
    public static void main(String[] args) {
        DirectedGraph directedGraph = new DirectedGraph(8); // 添加6个点,ID:0-5
        directedGraph.addEdge(new DirectedEdge(0,1,10));
        directedGraph.addEdge(new DirectedEdge(0,2,30));
        directedGraph.addEdge(new DirectedEdge(0,5,100));
        directedGraph.addEdge(new DirectedEdge(1,2,50));
        directedGraph.addEdge(new DirectedEdge(1,3,20));
        directedGraph.addEdge(new DirectedEdge(2,4,10));
        directedGraph.addEdge(new DirectedEdge(3,4,60));
        directedGraph.addEdge(new DirectedEdge(4,5,10));

//        System.out.println(directedGraph);

        // test Dfs(只添加了6个点)
        directedGraph.depthFirstSearch();
        System.out.println(directedGraph.preQueue);
        System.out.println(directedGraph.postQueue);
        System.out.println(directedGraph.reversePostStack);

        // test Dijkstra
        Dijkstra dijkstra = new Dijkstra(directedGraph,0);
        System.out.println(dijkstra.pathTo(5));

        // test AcyclicSP
        AcyclicSP acyclicSP = new AcyclicSP(directedGraph,0);
        System.out.println(acyclicSP.pathTo(5));

        // test Violent Dijkstra
        ViolentDijkstra violentDijkstra = new ViolentDijkstra(directedGraph);
        System.out.println(violentDijkstra.pathTo(0,5));

        // test Flyod
        Flyod flyod = new Flyod(directedGraph);
        System.out.println(flyod.pathTo(0,5));

        // test BF
        directedGraph.addEdge(new DirectedEdge(2,6,10));
        directedGraph.addEdge(new DirectedEdge(6,7,-60));
        directedGraph.addEdge(new DirectedEdge(7,6,10));
        BellmanFordSP bellmanFordSP = new BellmanFordSP(directedGraph,0);
        System.out.println(bellmanFordSP.pathTo(5));
    }
}
/* Output:
[0, 1, 3, 4, 5, 2]
[5, 4, 3, 2, 1, 0]
[0, 1, 2, 3, 4, 5]

[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]

[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]

[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]

[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]

[DirectedEdge{from=0, to=2, weight=30.0}, DirectedEdge{from=2, to=4, weight=10.0}, DirectedEdge{from=4, to=5, weight=10.0}]
 */



// 工具类
/**
 * 索引优先队列
 * @param <T>
 */
class IndexMinPriorityQueue<T extends Comparable<T>> {
    private T[] elements;
    private int[] indexPq;
    private int[] reIndexQp;
    private int N = 0;

    public IndexMinPriorityQueue(int maxN) {
        elements = (T[]) new Comparable[maxN + 1]; // elements这里可以不加1,没影响,但可能有其他用途
        indexPq = new int[maxN + 1];
        reIndexQp = new int[maxN +1];
        for (int i = 0; i <= maxN; i++)
            reIndexQp[i] = -1;
    }

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

    public int size() {
        return N;
    }

    public boolean contains(int k) {
        return reIndexQp[k] != -1;
    }

    // 插入:在k位置插入元素,位置k并不代表任何含义,只是存储在elements数组的索引位置;注意:这里k可以从索引0开始,没有任何影响
    public void insert(int k, T value) {
        N++;
        elements[k] = value; // 放入索引k
        indexPq[N] = k;      // 记录此元素所在索引位置(k)
        reIndexQp[k] = N;    // 记录indexPq数组中哪个位置(N)存储着此元素的索引
        swim(N);             // 从堆底加入并上浮,维护indexPq 和 reIndexQp
    }

    public T min() {
        return elements[indexPq[1]];
    }

    public int minIndex() {
        return indexPq[1];
    }

    // 删除最小值,并返回其索引
    public int delMin() {
        int indexOfMax = indexPq[1];
        if (elements[indexOfMax] == null) // 已为空,返回-1
            return -1;
        exch(1,N--);                 // 把最后一个元素(最小元素)放在顶端,然后N--(堆的大小-1)
        sink(1);                    // 让“最后”一个元素下沉
        elements[indexPq[N+1]] = null; // 将垃圾(删除的最小值)清空
        reIndexQp[indexPq[N+1]] = -1;  // 更新对应reIndexQp为-1
        indexPq[N+1] = 0;              // 更新最后一位删除的indexPq为0
        return indexOfMax;
    }

    // 删除索引k位置的元素,与删除最小值类似
    public void delete(int k) {
        int indexOfPq = reIndexQp[k];
        exch(indexOfPq,N--);
        swim(indexOfPq);
        sink(indexOfPq);
        elements[k] = null;
        reIndexQp[k] = -1;
        indexPq[N+1] = 0;
    }

    // 更新值
    public void change(int k, T newValue) {
        elements[k] = newValue;
        // 更新值后,可能出现三种情况:
        //    1. 比父节点小:需要上浮
        //    2. 比子节点大:需要下沉
        //    3. 大小在父节点和子节点之间:不执行任何操作
        // 所以此处采取的策略是先上浮在下沉(或先下沉再上浮)
        swim(reIndexQp[k]); // 上浮
        sink(reIndexQp[k]); // 下沉
    }

    // 用于堆实现的比较方法:这里怎么设计关乎着是大堆顶(<0)还是小堆顶(>0)
    private boolean greater(int i, int j) {
        return elements[indexPq[i]].compareTo(elements[indexPq[j]]) > 0;
    }

    // 用于堆实现的交换方法:交换indexPq[i]、indexPq[j] 和 reIndexPq[i]、reIndexPq[j]
    private void exch(int i, int j) {
        int tempPq = indexPq[i];
        indexPq[i] = indexPq[j];
        indexPq[j] = tempPq;
        reIndexQp[indexPq[i]] = i;
        reIndexQp[indexPq[j]] = j;
    }

    // 上浮
    private void swim(int k) {
        while(k > 1 && greater(k/2,k)) {
            exch(k/2,k); // k/2默认向下取整
            k = k/2;
        }
    }

    // 下沉
    private void sink(int k) {
        while (2*k <= N) {
            int j = 2*k;
            if (j < N && greater(j,j+1)) // 找到较小的子节点,并将j指向它
                j++;
            if (!greater(k,j)) // 此时j一定指向较小的子节点,如果elements[indexPq[k]] <= elements[indexPq[j]],则下沉结束
                break;
            exch(k,j); // 如果没有break则说明elements[indexPq[k]] > elements[indexPq[j]],交换indexPq 和 reIndexQp
            k = j;     // 交换k、j,让k始终指向下沉的元素
        }
    }

    @Override
    public String toString() {
        return "      indexPq " +
                Arrays.toString(indexPq) + "\n" +
                "    reIndexQp " +
                Arrays.toString(reIndexQp) + "\n" +
                "PriorityQueue " +
                Arrays.toString(elements);
    }
}

/**
 * 负权重环检测(环检测只需要把dfs里的weight删除)
 */
class EdgeWeightedDirectedCycle {
    private boolean[] marked;             // 是否已经被访问
    private DirectedEdge[] edgeTo;        // 到达edgeTo[v]代表达到v的边
    private boolean[] onStack;            // 递归调用栈上的所有顶点
    private Stack<DirectedEdge> cycle;    // 有向环上的所有顶点(如果存在)

    /*
     判断edgeTo[]表示的加权有向图graph是否有环,如果是有,返回环。
     */
    public EdgeWeightedDirectedCycle(DirectedGraph graph) {
        marked  = new boolean[graph.getV()];
        onStack = new boolean[graph.getV()];
        edgeTo  = new DirectedEdge[graph.getV()];
        for (int v = 0; v < graph.getV(); v++)
            if (!marked[v]) dfs(graph, v);
    }

    // dfs查找环
    private void dfs(DirectedGraph graph, int v) {
        onStack[v] = true;  // 标记已经在栈上了
        marked[v] = true;
        for (DirectedEdge e : graph.adj(v)) {
            int w = e.to;

            if (cycle != null) return; // 已经有环了,返回
            else if (!marked[w]) {     // 找到没有被访问的点,递归 dfs
                edgeTo[w] = e;
                dfs(graph, w);
            }else if (onStack[w]) {    // 如果相邻点已经在同一个栈上了,则说明存在环,并记录这个环
                cycle = new Stack<>();
                double weight = 0; // 计算环的权值(前面如果是拓扑排序请把这部分删除)

                DirectedEdge f = e;
                while (f.from != w) { // 循环遍历这个环,加入cycle中
                    cycle.push(f);
                    weight += f.getWeight();
                    f = edgeTo[f.from];
                }
                cycle.push(f);
                weight = weight + f.getWeight();

                if (weight > 0) // 正权值环
                    cycle = null;

                return; // 添加完环返回
            }
        }

        onStack[v] = false; // 递归结束要出栈,将入栈标记标为 false
    }

    public boolean hasCycle() {
        return cycle != null;
    }

    public Iterable<DirectedEdge> cycle() {
        return cycle;
    }

}