最短路径:学习总结

最短路径

最小生成树是以无向带权图为基准,而最短路径则是以加权有向图为基准

最短路径树

  • 给定一幅加权有向图和一个顶点s。
  • 以s为起点的一棵最短路径树是图的一幅子图,它包含s和从s可达的所有顶点。
  • 根节点为s,到叶子结点的每条路径和都是有向图中的一条最短路径

基本数据结构:

public class DirectedEdge {
    // 定义的是起始节点,互相不一定互通
    private final int v;
    private final int w;
    private final double weight;

    public DirectedEdge(int v, int w, double weight) {
        this.v = v;
        this.w = w;
        this.weight = weight;
    }

    public double weight(){
        return weight;
    }

    public int from(){
        return v;
    }

    public int to(){
        return w;
    }

}
public class EdgeWeightedDigraph {

    // 通过始点来记录边
    private final int V;
    private int E;
    private Bag<DirectedEdge>[] adj;

    public EdgeWeightedDigraph(int V) {
        this.V = V;
        this.E = 0;
        adj = (Bag<DirectedEdge>[]) new Bag[V];
        for (int v = 0; v < V; v++) {
            adj[v] = new Bag<>();
        }
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    public void addEdge(DirectedEdge e){
        adj[e.from()].add(e);
        E++;
    }

    public Iterable<DirectedEdge> adj(int v){
        return adj[v];
    }

    public Iterable<DirectedEdge> edges(){
        Bag<DirectedEdge> bag = new Bag<>();
        for (int v = 0; v < V; v++) {
            for (DirectedEdge edge : adj[v]) {
                bag.add(edge);
            }
        }
        return bag;
    }
}

数据结构记录:

  • 最短路径树中的边:edgeTo[],edgeTo[v]的值为树中连接v和它的父节点的边(也是从s到v的最后一条边)

  • 到达起点的距离。distTo[],其中distTo[],为从s到v的已知最短路径的长度

  • 边的松弛:

    • 检查从s→w的最短路径是否先从s→v,然后再由v→w
    • 检查distTo[v]与e.weight()的和,如果这个值不小于distTo[w],则称这条边失效了,并将它忽略
    • 如果这个值更小,就更新内容
private void relax(DirectedEdge e){
    int v = e.from(), w = e.to();
    if(distTo[w] > distTo[v] + e.weight()){
        // 相当于s→v的距离和v到w的边的权值和更小 -- 比之前的直接从s→w更小
        distTo[w] = distTo[v] + e.weight();
        // 将w的边替换为当前的e
        edgeTo[w] = e;
    }
}

通用算法:

  • 放松G中的任意边,知道不存在有效边为止。对于任意从s可达的顶点w,在进行这些操作后,distTo[w]的值即为从s到w的最短路径长度

Dijkstra算法

算法流程:

  • 首先将distTo[s]初始化为0,distTo[]中的其他元素初始化为正无穷,然后将distTo[]最小的非树顶点放松并加入树中
  • 知道所有的顶点都在树中或者所有的非树顶点的distTo[]值均为无穷大

准备条件

  • distTo[] edgeTo[]数组
  • 索引优先队列pq;临时结点 – 保存需要被放松的顶点或者确认下一个被放松的节点
public class DijkstraSP {

    private DirectedEdge[] edgeTo;
    private double[] distTo;
    private IndexMinPQ<Double> pq;

    public DijkstraSP(EdgeWeightedDigraph G, int s) {

        edgeTo = new DirectedEdge[G.V()];
        distTo = new double[G.V()];
        pq = new IndexMinPQ<>(G.V());

        for (int v = 0; v < G.V(); v++) {
            // 每一个都要赋初值
            distTo[v] = Double.POSITIVE_INFINITY;
            // 每一次都需要添加树的顶点进队列,相当于每次找最短路径都会从顶点开始走一遍
            distTo[s] = 0.0;
            pq.insert(s, 0.0);
            while (!pq.isEmpty()) {
                // 放松pq权值最小的点
                relax(G, pq.delMin());
            }
        }
    }

    private void relax(EdgeWeightedDigraph G, int v) {
        for (DirectedEdge e : G.adj(v)) {
            int w = e.to();
            if (distTo[w] > distTo[v] + e.weight()) {
                // 松弛边
                distTo[w] = distTo[v] + e.weight();
                edgeTo[w] = e;
                // 修改优先队列中对应的值
                if (pq.contains(w)) pq.changeKey(w, distTo[w]);
                else pq.insert(w, distTo[w]);
            }
        }
    }

    public double distTo(int v) {
        validateVertex(v);
        return distTo[v];
    }

    public boolean hasPathTo(int v) {
        validateVertex(v);
        return distTo[v] < Double.POSITIVE_INFINITY;
    }

    public Iterable<DirectedEdge> pathTo(int v) {
        validateVertex(v);
        if (!hasPathTo(v)) return null;
        Stack<DirectedEdge> path = new Stack<>();
        for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
            path.push(e);
        }
        return path;
    }

    // 验证点是否在树中
    private void validateVertex(int v) {
        int V = distTo.length;
        if (v < 0 || v >= V)
            throw new IllegalArgumentException("vertex " + v + " is not between 0 and " + (V - 1));
    }
}

无环加权有向图的最短路径算法

问题变化:要求有权无向图的最短路径,相当于创建一个有向图,每个结点对于相邻结点都有一个双向箭头

  • 核心:
    • 在有向图的拓扑排序(保证前面的点都指向后面的点)中,对每一条边进行一次relax松弛操作。
    • 松弛不改变distTo[v],即s→v的值不再改变,保证v→w的是最小值
public class AcyclicSP {

    private DirectedEdge[] edgeTo;
    private double[] distTo;

    public AcyclicSP(EdgeWeightedDigraph G, int s) {
        edgeTo = new DirectedEdge[G.V()];
        distTo = new double[G.V()];

        for (int v = 0; v < G.V(); v++) 
            // distTo[v] = Double.POSITIVE_INFINITY就等价于marked[v] = false
            distTo[v] = Double.POSITIVE_INFINITY;
       
        distTo[s] = 0.0;
		//构建拓扑排序
        Topological top = new Topological(G);
        // 对于先序序列进行松弛
        for (int v : top.order())
            relax(G, v);
    }

    private void relax(EdgeWeightedDigraph G, int v) {
        for (DirectedEdge e : G.adj(v)) {
            int w = e.to();
            if (distTo[w] > distTo[v] + e.weight()) {
                // 松弛边
                distTo[w] = distTo[v] + e.weight();
                edgeTo[w] = e;
            }
        }
    }
}

拓展:求最长路径

  • 所有的double最开始记为最小
  • 松弛的判定变为distTo[w] < distTo[v] + e.weight(),由原来的大于变成小于

一般加权有向图中的最短路径问题

Bell–ford算法过程:

  • 在任意含有V个顶点的加权有向图中给定起点s,s不可达到负权环(各边权重总和为负)

  • 将distTo[s]设置为0,distTo[v]设置为无穷大,进行v轮边的放松,得到的即是环的最短路径

  • 边的总数为V-1,所以通过V轮迭代后,再进行迭代,总会得到一个相同的结果。结果从第V轮开始收敛

  • 只有上一轮中distTo[v]发生变化的边,下一轮才能改变其他元素的distTo[]值。通过一个队列来保存这样的点

  • 辅助数据结构:

    • 队列:保存即将被放松的顶点的队列
    • 数组onQ[]:用来指示顶点是否已经存在于队列中,防止将顶点重复插入队列
    • 可以保证:
      • 队列不出现重复的点
      • 在某一轮中,改变了distTo[]和edgeTo[]的值的所有顶点都会在下一轮处理
    • 注意需要在检测到负权重环的时候退出
public class BellmanFordSP {
    private double[] distTo;
    private DirectedEdge[] edgeTo;
    private boolean[] onQ;
    // 正在被放松的顶点
    private Queue<Integer> queue;
    // relax的调用次数
    private int cost;
    // 检测是否有负权重环
    private Iterable<DirectedEdge> cycle;

    public BellmanFordSP(EdgeWeightedDigraph G, int s) {
        distTo = new double[G.V()];
        edgeTo = new DirectedEdge[G.V()];
        onQ = new boolean[G.V()];
        queue = new LinkedList<>();
        //初始化各边,以进行放松操作
        for (int v = 0; v < G.V(); v++) {
            distTo[v] = Double.POSITIVE_INFINITY;
        }
        distTo[s] = 0.0;
        // 将起点入队,只要入队,设置onQ为true
        queue.offer(s);
        onQ[s] = true;
        // 队列非空,且不含有负权重环 -- 不然会陷入无限循环
        while (!queue.isEmpty() && !hasNegativeCycle()){
            int v = queue.poll();
            onQ[v] = false;
            // 每次循环,将顶点的出边进行放松
            relax(G, v);
        }

    }

    private void relax(EdgeWeightedDigraph G, int v) {
        for (DirectedEdge e : G.adj(v)) {
            int w = e.to();
            if(distTo[w] > distTo[v] + e.weight()){
                distTo[w] = distTo[v] + e.weight();
                edgeTo[w] = e;
                // 如果发生了变化,那么将这个结点入队
                // 其他distTo不发生变化的结点以后也不会改变
                if(!onQ[w]){
                    queue.offer(w);
                    onQ[w] = true;
                }
            }
            if(cost++ % G.V() == 0){
                // 当遍历的次数达到V时,进行负权重的检测
                findNegativeCycle();
            }
        }
    }

    private void findNegativeCycle() {
        int V = edgeTo.length;
        EdgeWeightedDigraph spt = new EdgeWeightedDigraph(V);
        for (int v = 0; v < V; v++) {
            if(edgeTo[v] != null)
                spt.addEdge(edgeTo[v]);
        }
        // 进行环的检测,只要有环,都是不可接受的,因为有可能会产生负权重环
        // 所以这里用的一般的环检测算法
        EdgeWeightedDirectedCycle cf = new EdgeWeightedDirectedCycle(spt);
        cycle = cf.cycle();
    }

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

    public Iterable<DirectedEdge> negativeCycle(){
        return cycle;
    }
}
// 环检测算法补充、
public EdgeWeightedDirectedCycle(EdgeWeightedDigraph G) {
    marked  = new boolean[G.V()];
    onStack = new boolean[G.V()];
    edgeTo  = new DirectedEdge[G.V()];
    for (int v = 0; v < G.V(); v++)
        if (!marked[v]) dfs(G, v);

    // check that digraph has a cycle
    assert check();
}

// check that algorithm computes either the topological order or finds a directed cycle
private void dfs(EdgeWeightedDigraph G, int v) {
    onStack[v] = true;
    marked[v] = true;
    for (DirectedEdge e : G.adj(v)) {
        int w = e.to();

        // short circuit if directed cycle found
        if (cycle != null) return;

        // found new vertex, so recur
        else if (!marked[w]) {
            edgeTo[w] = e;
            dfs(G, w);
        }

        // trace back directed cycle
        // 到这里检测到了环,开始入栈进行回溯
        else if (onStack[w]) {
            cycle = new Stack<>();

            DirectedEdge f = e;
            while (f.from() != w) {
                // 将点的出边先入栈
                cycle.push(f);
                // 再获取点的入边,直到没有入边为止,即到了环的起点
                f = edgeTo[f.from()];
            }
            cycle.push(f);

            return;
        }
    }

    onStack[v] = false;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章