首页投稿(暂停使用,暂停投稿)程序员

单源最短路径

2017-11-04  本文已影响135人  某昆

前言

给定一个带权有向图G=(V,E),其中每条边的权是一个实数。另外,还给定V中的一个顶点,称为源。现在要计算从源到其他所有各顶点的最短路径长度。这里的长度就是指路上各边权之和。这个问题通常称为单源最短路径问题。

求取单源最短路径一般有两种算法,Bellman ford算法以及Dijkstra算法。Bellman ford算法可适应有负权重的图,而Dijkstra算法则只能用于正权重无环图。

环路

如上图,一条最短路径是不可能包含环路的。环路包含两种情况,一种是负环路,如上图最下边的路径,它的最短路径为无限小,因为s、e、f、g的环路中,可以无限循环e、f,那么权重值可以无限降低。

最短路径也不可能包含正环路的,比如s、c、d、g的路径,不可能重复c、d,因为这样会增加路径的距离,完全可以减去正环路。

所以,最小路径中是不包含环路的。

松驰操作

求取单源最短路径需要用到松驰技术。对于每个节点来说,我们维持一个属性 v.d ,用来记录从源节点s到结点v的最短路径权重的上界。在算法开始时,使用如下方式进行初始化。

public void init_single_source(MatrixGraph graph, Vertex s){
    List<Vertex> list = graph.mList;
    for (Vertex vertex : list) {
        vertex.d = Integer.MAX_VALUE;
        vertex.pre = null;
    }
    s.d = 0;
}

设置源节点s.d等于0,其它节点v.d为无穷大,任意节点的前驱节点为空。

public void relax(Vertex u, Vertex v, int w){
    if (v.d > u.d + w) {
        v.d = u.d + w;
        v.pre = u;
    }
}

松驰操作,就是比较当前节点和前驱节点。如果前驱节点 d 加上边的权重值少于本节点的 d 值,则更新本节点 d 值。

Bellman ford算法

首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。

其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按每个点实际的最短路径(虽然我们还不知道,但它一定存在)的层次,逐层生成这棵最短路径树的过程。

注意,每一次遍历,都可以从前一次遍历的基础上,找到此次遍历的部分点的单源最短路径。如:这是第i次遍历,那么,通过数学归纳法,若前面单源最短路径层次为1~(i-1)的点全部已经得到,而单源最短路径层次为i的点,必定可由单源最短路径层次为i-1的点集得到,从而在下一次遍历中充当前一次的点集,如此往复迭代,[v]-1次后,若无负权回路,则我们已经达到了所需的目的--得到每个点的单源最短路径。(注意:这棵树的每一次更新,可以将其中的某一个子树接到另一个点下)

最后,我们在第[v]次更新中若没有新的松弛,则输出结果,若依然存在松弛,则输出 false 表示无解。

public boolean bellman_ford(){
    MatrixGraph graph = initDigraph();
    Vertex s = graph.mList.get(0);
    init_single_source(graph, s);
    List<Vertex> list = graph.mList;
    MatrixEdge[][] edges = graph.mEdges;
    List<MatrixEdge> edgeList = new ArrayList<>();
    int arrayLength = graph.maxLength;
    MatrixEdge edge = null;
    for (int i = 0; i < arrayLength; i++) {
        for (int j = 0; j < arrayLength; j++) {
            edge = edges[i][j];
            if (edge != null) {
                edgeList.add(edge);
            }
        }
    }
    //每条边都要循环 list .size()-1 次,才能计算出正确答案。
    //因为s到最远的端点最多要经过 list .size()-1 条边,如果松驰次数少了
    //某个前顶点的d值之前改变后,就不会得到反馈。
    for (int i = 1; i < list.size(); i++) {
        for (int j = 0; j < edgeList.size(); j++) {
            edge = edgeList.get(j);
            relax(edge.v1, edge.v2, edge.weight);
        }
    }
    //第i个点经过i-1次数松驰就能得到最短距离
    //最远的点经过 edgeList.size()-1 次松驰也会得到最短距离。
    //所以,再次遍历,如果还存在能松驰的情况,那一定是有环路,这样的情况不存在最短距离。
    for (int i = 0; i < edgeList.size(); i++) {
        edge = edgeList.get(i);
        if (edge.v2.d > edge.v1.d + edge.weight) {
            return false;
        }
    }
    for (Vertex vertex : list) {
        System.out.println(vertex);
    }
    return true;
}

为什么一定要松驰 V-1 次,因为一个顶点到另一个顶点最多需要经过 V-1 条边。而且,我们在做松驰操作的时候,选取的第一条边未必就经过初始顶点,每次松驰所有边之后,顶点的d值均有可能减少,所以要松驰这么多次,以便得到最后正确答案。另外本算法的复杂度是 VE

Dijkstra算法

Dijkstra算法在运行过程中维持的关键信息是一组结点集合q,且q是最小优先队列,图中的所有结点均被保存在q中,不停地从q中取出第一个节点,也是d值最小的节点,将与d节点有边相连的节点进行松驰操作。

因为Dijkstra算法总是选择图中d值最小的点进行松驰,使用的是贪心策略。因为最短路径也一定是各条子路径都是最短的,所以此算法是有效的。

代码如下:

public void dijkstra(){
    MatrixGraph graph = initDigraph();
    Vertex s = graph.mList.get(0);
    init_single_source(graph, s);
    Vertex[] datas = new Vertex[graph.mList.size()];
    for (int i = 0; i < graph.mList.size(); i++) {
        datas[i] = graph.mList.get(i);
    }
    MinPriorityQueue queue = new MinPriorityQueue(datas);
    Vertex u = null;
    MatrixEdge edge = null;
    while (queue.size() > 0) {
        u = (Vertex) queue.heapExtractMin();
        int index = graph.mList.indexOf(u);
        for (int i = 0; i < VERTEX_NUM; i++) {
            edge = graph.mEdges[index][i];
            if (edge != null) {
                relax(edge.v1, edge.v2, edge.weight);
                //上一步进行了松驰操作,所以此处需要重新检测最小优先队列的属性
                //以维持queue仍是最小优先队列
                //此算法的关键就是正确设计最小优先队列
                queue.checkMinHeap();
            }
        }
    }
  for (Vertex vertex : graph.mList) {
      System.out.println(vertex);
  }
}

最小优先队列使用最小堆技术,代码详见本人github

如果不用最小堆,还有另一种实现:

/**
 * dijkstra算法的另一种实现
 * 核心原理都是顶点分成两部分,一部分是已经查验过的,一部分是未查验过的
 * 已检查的顶点集合已得到的最小距离,它们必然是其它顶点最短距离的一个环节
 * 每次在未查验顶点集合中,选取d值最小顶点,松驰d相关的所有边
 */
public void dijkstra2(){
    MatrixGraph graph = initDigraph();
    Vertex s = graph.mList.get(0);
    init_single_source(graph, s);
    List<Vertex> notCheckList = new ArrayList<>();
    notCheckList.addAll(graph.mList);
    List<Vertex> checkList = new ArrayList<>();
    Vertex u = null;
    MatrixEdge edge = null;
    while (notCheckList.size() > 0) {
        //找出最小的d值所在顶点
        int small = 0;
        for (int i = 0; i < notCheckList.size(); i++) {
            if (notCheckList.get(i).d < notCheckList.get(small).d) {
                small = i;
            }
        }
        u = notCheckList.get(small);
        int index = graph.mList.indexOf(u);
        for (int i = 0; i < VERTEX_NUM; i++) {
            edge = graph.mEdges[index][i];
            if (edge != null) {
                relax(edge.v1, edge.v2, edge.weight);
            }
        }
        notCheckList.remove(u);
        checkList.add(u);
    }
    for (Vertex vertex : graph.mList) {
        System.out.println(vertex);
    }
}

这两种写法其实背后的原理一样。

上一篇下一篇

猜你喜欢

热点阅读