查看原文
其他

图也有权重,你们知道吗?

倪升武 程序员私房菜 2018-12-04


阅读本文大概需要8分钟


这是一篇技术文,继续提升一下我们的内功。

从之前一篇文章:我敢说,这图绝对跟你想象中的不太一样!。可以看到,图的边可以有方向,那么这一篇文章,我们来探讨一下边的另一个特性:权值。例如,如果带权图的顶点代表城市,边的权可能代表城市之间的距离,或者城市之间的路费,或者之间的车流量等等。

带权图归根究底还是图,前一篇文章中那些图的基本操作,例如广度优先搜索和深度优先搜索等都是一样的,本文主要来探讨一下带权图的最小生成树最短路径问题。

1. 最小生成树

首先来探讨下最小生成树问题,它与前一篇文章中所提到的最小生成树不同。前一节的最小生成树是个特例,即所有边的权值都一样。那么算法如何设计呢?建议用优先级队列来实现这个反复选择最小的路径,而不是链表或数组,这是解决最小生成树的有效方式。在正式的程序中,优先级队列可能基于堆来实现(堆其实是个很简单的数据结构),这会加快在较大的优先级队列中的操作。但是在本例中,我们使用数组实现优先级队列,仅仅为了说明算法。算法要点如下:

从一个顶点开始,把它放入树的集合中,然后重复做下面的事情:

  • 1.找到从最新的顶点到其他顶点的所有边,这些顶点不能在树的集合中,把这些边放入优先级队列中。

  • 2.找出权值最小的边,把它和它所达到的顶点放入树的集合中。


重复这些步骤,直到所有的顶点都在树的集合中,这时工作完成。下面先看一下最小生成树的代码,然后再解释一些细节上的问题:

代码有点长,建议收藏本文,先看个美女压压惊~ 


  1. //边界路径类,主要记录了边的始末顶点,以及边的权值

  2. class Edge {

  3.    public int srcVert; //index of vertex starting edge

  4.    public int destVert; //index of vertex ending edge

  5.    public int distance; //distance from src to dest

  6.    public Edge(int sv, int dv, int d) {

  7.        srcVert = sv;

  8.        destVert = dv;

  9.        distance = d;

  10.    }

  11. }

  12. //自定义优先队列,用来存储边

  13. class PriorityQ {

  14.    private final int SIZE = 20;

  15.    private Edge[] queArray; //存储边界的数组

  16.    private int size;

  17.    public PriorityQ() {

  18.        queArray = new Edge[SIZE];

  19.        size = 0;

  20.    }

  21.    public void insert(Edge item) { //有序的插入边界

  22.        int j;

  23.        for(j = 0; j < size; j++) { //找到插入的位置,从0到size-1,逐渐减小

  24.            if(item.distance >= queArray[j].distance)

  25.                break;

  26.        }

  27.        //比item.distance小的往后挪一位,给腾出个空间

  28.        for(int k = size-1; k >= j; k--) {

  29.            queArray[k+1] = queArray[k];

  30.        }

  31.        queArray[j] = item; //插入item

  32.        size++;

  33.    }

  34.    public Edge removeMin() { //删除最小的边界并返回

  35.        return queArray[--size];

  36.    }

  37.    public void removeN(int n) { //删除n位置的边界

  38.        for(int j = n; j < size-1; j++) {

  39.            queArray[j] = queArray[j+1];

  40.        }

  41.        size--;

  42.    }

  43.    public Edge peekMin() { //返回最小边界,不删除

  44.        return queArray[size-1];

  45.    }

  46.    public Edge peekN(int n) { //返回n位置的边界

  47.        return queArray[n];

  48.    }

  49.    public int size() {

  50.        return size;

  51.    }

  52.    public boolean isEmpty() {

  53.        return (size == 0);

  54.    }

  55.    public int find(int findDex) { //寻找特定disVert的边界索引

  56.        for(int j = 0; j < size; j++) {

  57.            if(queArray[j].destVert == findDex)

  58.                return j;

  59.        }

  60.        return -1;

  61.    }

  62. }

  63. //带权图类

  64. public class WeightedGraph {

  65.    private final int MAX_VERTS = 20; //最大顶点数

  66.    private final int INFINITY = 100000; //最远距离...表示无法达到

  67.    private Vertex[] vertexArray; //存储顶点的数组

  68.    private int adjMat[][]; //存储顶点之间的边界

  69.    private int nVerts; //顶点数量

  70.    private int currentVert; //当前顶点索引

  71.    private PriorityQ thePQ; //存储边的优先级队列

  72.    private int nTree; //最小生成树中的顶点数量

  73.    public WeightedGraph() {

  74.        vertexArray = new Vertex[MAX_VERTS];

  75.        adjMat = new int[MAX_VERTS][MAX_VERTS];

  76.        for(int i = 0; i < MAX_VERTS; i++) {

  77.            for(int j = 0; j < MAX_VERTS; j++) {

  78.                adjMat[i][j] = INFINITY; //初始化所有边界无穷远

  79.            }

  80.        }

  81.        thePQ = new PriorityQ();

  82.    }

  83.    public void addVertex(char lab) { //添加顶点

  84.        vertexArray[nVerts++] = new Vertex(lab);

  85.    }

  86.    public void addEdge(int start, int end, int weight) {//添加带权边

  87.        adjMat[start][end] = weight;

  88.        adjMat[end][start] = weight;

  89.    }

  90.    public void displayVertex(int v) {

  91.        System.out.print(vertexArray[v].label);

  92.    }

  93.    /*

  94.     * 带权图的最小生成树,要选择一条最优的路径

  95.     */

  96.    public void MinSpanningTree() {

  97.        currentVert = 0; //从0开始

  98.        while(nTree < nVerts-1) { //当不是所有节点都在最小生成树中时

  99.            //isInTree是上一节Vertex类中新添加的成员变量 private boolean isInTree;

  100.               //表示有没有加入到树中,初始化为false

  101. vertexArray[currentVert].isInTree = true; //将当前顶点加到树中

  102.            nTree++;

  103.            //往PQ中插入与当前顶点相邻的一些边界

  104.            for(int i = 0; i < nVerts; i++) {

  105.                if(i == currentVert) //如果是本顶点,跳出

  106.                    continue;

  107.                if(vertexArray[i].isInTree) //如果顶点i已经在树中,跳出

  108.                    continue;

  109.                int distance = adjMat[currentVert][i]; //计算当前顶点到i顶点的距离

  110.                if(distance == INFINITY)

  111.                    continue; //如果当前顶点与i顶点无穷远,跳出

  112.                putInPQ(i, distance); //将i节点加入PQ中

  113.            }

  114.            if(thePQ.size() == 0) { //如果PQ为空,表示图不连接

  115.                System.out.println("Graph not connected!");

  116.                return;

  117.            }

  118.            Edge theEdge = thePQ.removeMin();

  119.            int sourceVert = theEdge.srcVert;

  120.            currentVert = theEdge.destVert;

  121.            System.out.print(vertexArray[sourceVert].label);

  122.            System.out.print(vertexArray[currentVert].label);

  123.            System.out.print(" ");

  124.        }

  125.    }

  126.    private void putInPQ(int newVert, int newDist) {

  127.        int queueIndex = thePQ.find(newVert);//判断PQ中是否已经有到相同目的顶点的边界

  128.        if(queueIndex != -1) { //如果有则与当前顶点到目的顶点的距离作比较,保留短的那个

  129.            Edge tempEdge = thePQ.peekN(queueIndex);//get edge

  130.            int oldDist = tempEdge.distance;

  131.            if(oldDist > newDist) { //如果新的边界更短

  132.                thePQ.removeN(queueIndex); //删除旧边界

  133.                Edge theEdge = new Edge(currentVert, newVert, newDist);

  134.                thePQ.insert(theEdge);

  135.            }

  136.        }

  137.        else { //如果PQ中没有到相同目的顶点的边界

  138.            Edge theEdge = new Edge(currentVert, newVert, newDist);

  139.            thePQ.insert(theEdge);//直接添加到PQ

  140.        }

  141.    }

  142. }


算法在while循环中执行,循环结束条件是所有顶点都已在树中。

  • 1.当前顶点放在树中。

  • 2.连接这个顶点的边放到优先级队列中(如果合适)。

  • 3.从优先级队列中删除权值最小的边,这条边的目的顶点变成当前顶点。


再看看这些步骤的细节:1中,通过标记 currentVert 所指顶点的 isInTree 字段来表示该顶点放入树中,2中,连接这个顶点的边插入优先级队列。通过在邻接矩阵中扫描行号是 currentVert 的行寻找需要的边。只要下面任意一个条件为真,这条边就不能放入队列中:

  • 1.源点和终点相同;

  • 2.终点在树中;

  • 3.源点和终点之间没有边(邻接矩阵中对应的值等于无穷大)。


如果没有一个条件为真,调用 putInPQ() 方法把这条边放入队列中。实际上并不一定会将这条边放入队列中,还得进行判断。步骤3中,将最小权值的边从优先级队列中删除。把这条边和该边的重点加入树,并显示源点和终点。

最后,所有顶点的 isInTree 变量被重置,即从树中删除。在该程序这样做,是因为根据这些数据只能创建一棵树。然后在完成一项工作后,最好把数据恢复到原始的形态。

2. 最短路径问题

在带权图中最常遇到的问题就是寻找两点间的最短路径问题,这个问题的解决方法可应用于现实生活中的很多地方。但是它比前面遇到的问题更加复杂一些。为了解决最短路径问题而提出的方法叫做Djikstra算法。这个算法的实现基于图的邻接矩阵表示法,它不仅能够找到任意两点间的最短路径,还可以找到某个指定点到其他所有顶点的最短路径。

为了实现这个算法,首先得建一个辅助类DistPar类,这个类中封装了到初始顶点的距离以及父顶点的信息。

  1. //DistPar类记录了当前顶点到起始顶点点的距离和当前顶点的父顶点

  2. class DistPar {

  3.    public int distance; //distance from start to this vertex

  4.    public int parentVert; //current parent of this vertex

  5.    public DistPar(int pv, int d) {

  6.        distance = d;

  7.        parentVert = pv;

  8.    }

  9. }


另外还得有个数组,这是最短路径算法中的一个关键数据结构,它保持了从源点到其他顶点(终点)的最短路径。在算法的执行过程中这个距离是变化的,知道最后,它存储了从源点开始的真正最短距离。这个数组定义为 WeightedGraph 的一个私有成员变量:

  1. private DistPar[] sPath; //存储最短路径数据,存储的是上面的DistPar对象

  2. private int startToCurrent; //到当前顶点的距离


另外需要在构造函数中将其初始化: sPath=newDistPar[MAX_VERTS];

下面详细分析最短路径算法中涉及的几个方法,这都是 WeightedGraph 类中的方法,在这里我抽出来分析的,最后会附上完整的 WeightedGraph 类代码

  1. /************************** 最短路径问题 ****************************/

  2. /**

  3. * path()方法执行真正的最短路径算法。

  4. */

  5. public void path() { //寻找所有最短路径

  6.    /*

  7.     * 源点总在vertexArray[]数组下标为0的位置,path()方法的第一个任务就是把这个顶点放入树中。

  8.     * 算法执行过程中,将会把其他顶点也逐一放入树中。把顶点放入树中的操作是设置一下标志位即可。

  9.     * 并把nTree变量增1,这个变量记录了树中有多少个顶点。

  10.     */

  11.    int startTree = 0; //从vertex 0开始

  12.    vertexArray[startTree].isInTree = true;

  13.    nTree = 1;

  14.    /*

  15.     * path()方法把邻接矩阵的对应行表达的距离复制到sPath[]中,实际总是先从第0行复制

  16.     * 为了简单,假定源点的下标总为0。最开始,所有sPath[]数组中的父节点字段为A,即源点。

  17.     */

  18.    for(int i = 0; i < nVerts; i++) {

  19.        int tempDist = adjMat[startTree][i];

  20.        //sPath中保存的都是到初始顶点的距离,所以父顶点默认都是初始顶点,后面程序中会将其修改

  21.        sPath[i] = new DistPar(startTree, tempDist);

  22.    }

  23.    /*

  24.     * 现在进入主循环,等到所有的顶点都放入树中,这个循环就结束,这个循环有三个基本动作:

  25.     * 1. 选择sPath[]数组中的最小距离

  26.     * 2. 把对应的顶点(这个最小距离所在列的题头)放入树中,这个顶点变成“当前顶点”currentVert

  27.     * 3. 根据currentVert的变化,更新所有的sPath[]数组内容

  28.     */

  29.    while(nTree < nVerts) {

  30.        //1. 选择sPath[]数组中的最小距离

  31.        int indexMin = getMin(); //获得sPath中的最小路径值索引

  32.        int minDist = sPath[indexMin].distance; //获得最小路径

  33.        if(minDist == INFINITY) {

  34.            System.out.println("There are unreachable vertices");

  35.            break;

  36.        }

  37.        //2. 把对应的顶点(这个最小距离所在列的题头)放入树中,这个顶点变成“当前顶点”currentVert

  38.        else { //reset currentVert

  39.            currentVert = indexMin;

  40.            startToCurrent = sPath[indexMin].distance;

  41.        }

  42.        vertexArray[currentVert].isInTree = true;

  43.        nTree++;

  44.        //3. 根据currentVert的变化,更新所有的sPath[]数组内容

  45.        adjust_sPath();

  46.    }

  47.    displayPaths();

  48.    nTree = 0;

  49.    for(int i = 0; i < nVerts; i++) {

  50.        vertexArray[i].isInTree = false;

  51.    }

  52. }

  53. //获取sPath中最小路径的索引

  54. private int getMin() {

  55.    int minDist = INFINITY;

  56.    int indexMin = 0;

  57.    for(int i = 0; i < nVerts; i++) {

  58.        if(!vertexArray[i].isInTree && sPath[i].distance < minDist) {

  59.            minDist = sPath[i].distance;

  60.            indexMin = i;

  61.        }

  62.    }

  63.    return indexMin;

  64. }

  65. /*调整sPath中存储的对象的值,即顶点到初始顶点的距离,和顶点的父顶点

  66. * 这是Dijkstra算法的核心

  67. */

  68. private void adjust_sPath() {

  69.    int column = 1;

  70.    while(column < nVerts) {

  71.        if(vertexArray[column].isInTree) {

  72.            column++;

  73.            continue;

  74.        }

  75.        int currentToFringe = adjMat[currentVert][column]; //获得当前顶点到其他顶点的距离,其他顶点不满足isInTree

  76.        int startToFringe = startToCurrent + currentToFringe; //计算其他顶点到初始顶点的距离=当前顶点到初始顶点距离+当前顶点到其他顶点的距离

  77.        int sPathDist = sPath[column].distance; //获得column处顶点到起始顶点的距离,如果不与初始顶点相邻,默认值都是无穷大

  78.        if(startToFringe < sPathDist) {

  79.            sPath[column].parentVert = currentVert; //修改其父顶点

  80.            sPath[column].distance = startToFringe; //以及到初始顶点的距离

  81.        }

  82.        column++;

  83.    }

  84. }

  85. //显示路径

  86. private void displayPaths() {

  87.    for(int i = 0; i < nVerts; i++) {

  88.        System.out.print(vertexArray[i].label + "=");

  89.        if(sPath[i].distance == INFINITY)

  90.            System.out.print("infinity");

  91.        else

  92.            System.out.print(sPath[i].distance);

  93.        char parent = vertexArray[sPath[i].parentVert].label;

  94.        System.out.print("(" + parent + ") ");

  95.    }

  96.    System.out.println("");

  97. }


由于图的表示法有两种,邻接矩阵和邻接表。是的图的算法效率问题变得相对复杂。如果使用邻接矩阵,前面讨论的算法大多需要O(V2)的时间级,V表示顶点数量。因为这些算法几乎都检查了一遍所有的顶点,具体方法是在邻接矩阵中扫描每一行,一次查看每一条边。换句话说,邻接矩阵的V2个单元都被扫描过。

对于大规模的矩阵,O(V2)的时间基本是非常好的性能。如果图是密集的,那就没什么提高性能的余地了(密集意味着图有很多边,而它的邻接矩阵的许多或大部分单元被占)。然而,许多图是稀疏的,其实并没有一个确定数量的定义说明多少条边的图才是密集的或稀疏的,但如果在一个大图中每个顶点只有很少几条边相连,那么这个图通常被认为是稀疏的。

在稀疏图中,使用邻接表的表示方法代替邻接矩阵,可以改善运行时间,因为不必浪费时间来检索邻接矩阵中没有边的单元。对于无权图,邻接表的深度优先搜索需要O(V+E)的时间级,V是顶点数量,E是边数。对于带权图,最小生成树算法和最短路径算法都需要O(E+V)logV)的时间级,在大型的稀疏图中,与邻接矩阵方法的时间级O(V2)相比,这样的时间级可使性能大幅提升,但是算法会复杂一些。

终于写完了,文章比较长,如果觉得对你有帮助,建议先收藏,在等公交的时候、排队等饭的时候可以拿出来看看,利用碎片化时间学习。

END


这里不仅仅有技术

相关推荐阅读:

如果让你手写个栈和队列,你还会写吗?

开发了那么多项目,你能自己手写个健壮的链表出来吗?

下次面试若再被问到二叉树,希望你能对答如流!

面试还在被红-黑树虐?看完这篇轻松搞定面试官

2-3-4树是如何解决二叉树中非平衡问题的?

读完这篇,希望你能真正理解什么是哈希表

我敢说,这图绝对跟你想象中的不太一样!

堆其实是个很简单的数据结构


测试代码请点击左下角“阅读原文”查看。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存