加权有向图和最短路径

加权有向图

之前学习的加权无向图中,边是没有方向的,并且同一条边会同时出现在该边的两个顶点的邻接表中,为了能够处 理含有方向性的图的问题,我们需要实现以下加权有向图。

加权有向图边的表示

类名 DirectedEdge
构造方法 DirectedEdge(int v,int w,double weight):通过顶点v和w,以及权重weight值构造一个边对象
成员方法 1.public double weight():获取边的权重值
2.public int from():获取有向边的起点
3.public int to():获取有向边的终点
成员变量 1.private final int v:起点
2.private final int w:终点
3.private final double weight:当前边的权重

代码:

/**
 * 有向边
 * @author wen.jie
 * @date 2021/8/30 14:39
 */
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 this.weight;
    }

    //起点
    public int from() {
        return v;
    }

    //终点
    public int to() {
        return w;
    }
    
    @Override
    public String toString() {
        return "DirectedEdge{" +
                "v=" + v +
                ", w=" + w +
                ", weight=" + weight +
                '}';
    }
}

加权有向图的实现

类名 EdgeWeightedDigraph
构造方法 EdgeWeightedDigraph(int V):创建一个含有V个顶点的空加权有向图
成员方法 1.public int V():获取图中顶点的数量
2.public int E():获取图中边的数量
3.public void addEdge(DirectedEdge e):向加权有向图中添加一条边e
4.public Queue adj(int v):获取由顶点v指出的所有的边
5.public Queue edges():获取加权有向图的所有边
成员变量 1.private final int V: 记录顶点数量
.private int E: 记录边数量
3.private Queue[] adj: 邻接表

代码:

/**
 * 加权有向图
 * @author wen.jie
 * @date 2021/8/30 14:42
 */
public class EdgeWeightedDigraph{

    private final int V;

    private int E;

    private Queue<DirectedEdge>[] adj;

    public EdgeWeightedDigraph(int V) {
        this.V = V;
        this.E = E;

        this.adj = new Queue[V];
        for (int i = 0; i < adj.length; i++) {
            adj[i] = new Queue<>();
        }
    }

    public int V() {
        return V;
    }

    public int E() {
        return E;
    }

    //向加权有向图中添加一条边
    public void addEdge(DirectedEdge e) {
        int v = e.from();
        adj[v].enqueue(e);
        E++;
    }

    //获取由顶点v指出的所有的边
    public Queue<DirectedEdge> adj(int v) {
        return adj[v];
    }

    //获取加权有向图中的所有边
    public Queue<DirectedEdge> edges() {
        Queue<DirectedEdge> allEdges = new Queue<>();
        for (int v = 0; v < V; v++) {
            for (DirectedEdge edge : adj[v]) {
                allEdges.enqueue(edge);
            }
        }
        return null;
    }
}

最短路径

最短路径定义及性质

定义: 在一副加权有向图中,从顶点s到顶点t的最短路径是所有从顶点s到顶点t的路径中总权重最小的那条路径。

image-20210830145351225

性质:

1.路径具有方向性;

2.权重不一定等价于距离。权重可以是距离、时间、花费等内容,权重最小指的是成本最低

3.只考虑连通图。一副图中并不是所有的顶点都是可达的,如果s和t不可达,那么它们之间也就不存在最短路径,为了简化问题,这里只考虑连通图。

4.最短路径不一定是唯一的。从一个顶点到达另外一个顶点的权重最小的路径可能会有很多条,这里只需要找出一条即可。

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

最短路径树API设计

计算最短路径树的经典算法是dijstra算法,为了实现它,先设计如下API:

类名 DijkstraSP
构造方法 public DijkstraSP(EdgeWeightedDigraph G, int s):根据一副加权有向图G和顶点s,创建一个计算顶点为s的最短路径树对象
成员方法 1.private void relax(EdgeWeightedDigraph G, int v):松弛图G中的顶点v
2.public double distTo(int v):获取从顶点s到顶点v的最短路径的总权重
3.public boolean hasPathTo(int v):判断从顶点s到顶点v是否可达
4.public Queue pathTo(int v):查询从起点s到顶点v的最短路径中所有的边
成员变量 1.private DirectedEdge[] edgeTo: 索引代表顶点,值表示从顶点s到当前顶点的最短路径上的最后一条边
2.private double[] distTo: 索引代表顶点,值从顶点s到当前顶点的最短路径的总权重
3.private IndexMinPriorityQueue<Double> pq:存放树中顶点与非树中顶点之间的有效横切边

松弛技术

松弛这个词来源于生活:一条橡皮筋沿着两个顶点的某条路径紧紧展开,如果这两个顶点之间的路径不止一条,还有存在更短的路径,那么把皮筋转移到更短的路径上,皮筋就可以放松了。

image-20210830150451451

松弛这种简单的原理刚好可以用来计算最短路径树。

在我们的API中,需要用到两个成员变量edgeTo和distTo,分别存储边和权重。一开始给定一幅图G和顶点s,我们只知道图的边以及这些边的权重,其他的一无所知,此时初始化顶点s到顶点s的最短路径的总权重disto[s]=0;顶点s到其他顶点的总权重默认为无穷大,随着算法的执行,不断的使用松弛技术处理图的边和顶点,并按一定的条件更新edgeTo和distTo中的数据,最终就可以得到最短路径树。

边的松弛:

放松边v->w意味着检查从s到w的最短路径是否先从s到v,然后再从v到w?

如果是,则v-w这条边需要加入到最短路径树中,更新edgeTo和distTo中的内容:edgeTo[w]=表示v->w这条边的DirectedEdge对象,distTo[w]=distTo[v]+v->w这条边的权重;

如果不是,则忽略v->w这条边。

image-20210830150636412

顶点的松弛:

顶点的松弛是基于边的松弛完成的,只需要把某个顶点指出的所有边松弛,那么该顶点就松弛完毕。例如要松弛顶点v,只需要遍历v的邻接表,把每一条边都松弛,那么顶点v就松弛了。

如果把起点设置为顶点0,那么找出起点0到顶点6的最短路径0->2->7>3->6的过程如下:

image-20210830151044608

Dijstra算法实现

Disjstra算法的实现和Prim算法很类似,构造最短路径树的每一步都是向这棵树中添加一条新的边,而这条新的边是有效横切边pq队列中的权重最小的边。

代码:

/**
 * Dijkstra算法
 * @author wen.jie
 * @date 2021/8/30 15:14
 */
public class DijkstraSP {

    //索引代表顶点,值表示从顶点s到当前顶点的最短路径上的最后一条边
    private DirectedEdge[] edgeTo;

    //索引代表顶点,值从顶点s到当前顶点的最短路径的总权重
    private double[] distTo;

    //存放树中顶点与非树中顶点之间的有效横切边
    private IndexMinPriorityQueue<Double> pq;

    public DijkstraSP(EdgeWeightedDigraph G, int s){
        this.edgeTo = new DirectedEdge[G.V()];
        this.distTo = new double[G.V()];
        Arrays.fill(distTo, Double.POSITIVE_INFINITY);
        pq = new IndexMinPriorityQueue<>(G.V());

        distTo[s] = 0.0;
        pq.insert(s, 0.0);
        while (!pq.isEmpty()) {
            relax(G, pq.delMin());
        }
    }

    //松弛图G中的顶点v
    private void relax(EdgeWeightedDigraph G, int v){
        for (DirectedEdge edge : G.adj(v)) {
            int w = edge.to();
            //松弛技术
            if (distTo(v) + edge.weight() < distTo(w)){
                distTo[w] = distTo[v] + edge.weight();
                edgeTo[w] = edge;
                if (pq.contains(w)) {
                    pq.changeItem(w, distTo(w));
                } else {
                    pq.insert(w, distTo(w));
                }
            }
        }
    }

    //获取从顶点s到顶点v的最短路径的总权重
    public double distTo(int v){
        return distTo[v];
    }

    //判断从顶点s到顶点v是否可达
    public boolean hasPathTo(int v){
        return distTo[v] < Double.POSITIVE_INFINITY;
    }

    //查询从起点s到顶点v的最短路径中所有的边
    public Queue<DirectedEdge> pathTo(int v){
        if (!hasPathTo(v)){
            return null;
        }
        Queue<DirectedEdge> allEdges = new Queue<>();
        while (true) {
            DirectedEdge e = edgeTo[v];
            if (e == null)
                break;
            allEdges.enqueue(e);
            v = e.from();
        }
        return allEdges;
    }

}

测试:

    String[] strs = new String[]{
            "4 5 0.35",
            "5 4 0.35",
            "4 7 0.37",
            "5 7 0.28",
            "7 5 0.28",
            "5 1 0.32",
            "0 4 0.38",
            "0 2 0.26",
            "7 3 0.39",
            "1 3 0.29",
            "2 7 0.34",
            "6 2 0.40",
            "3 6 0.52",
            "6 0 0.58",
            "6 4 0.93"
    };

    @Test
    public void test() {
        EdgeWeightedDigraph G = new EdgeWeightedDigraph(8);
        for (String str : strs) {
            String[] split = str.split(" ");
            DirectedEdge e = new DirectedEdge(Integer.parseInt(split[0]), Integer.parseInt(split[1]), Double.parseDouble(split[2]));
            G.addEdge(e);
        }

        DijkstraSP dijkstraSP = new DijkstraSP(G, 0);
        Queue<DirectedEdge> edges = dijkstraSP.pathTo(6);
        for (DirectedEdge edge : edges) {
            System.out.println(edge);
        }
    }

image-20210830154753942

本文所有代码均已上传:https://gitee.com/wj204811/algorithm

原文地址:https://www.cnblogs.com/wwjj4811/p/15206314.html