• 单源赋权最短路径


    单源赋权最短路径指给定一个赋权图G=(V,E)和一个输入顶点s,从s出发到图中其他顶点的最短路径就称为单源赋权最短路径。

    Djkstra算法

    解决单源赋权最短路径问题的一般方法叫作Djkstra算法(迪杰斯特拉算法)。迪杰斯特拉算法是一种贪婪算法,它是分阶段进行的,在每个阶段都认为选择是最好的。迪杰斯特拉算法只能解决非负权值的最短路径问题。

    在该方法中,图中的每个顶点保留与无权最短路径中同样的信息。known表示是否该顶点是否已知,d表示该顶点到输入顶点的距离,path表示引起该顶点距离变化的最后一个顶点,在顶点v的邻接表中,除了要保存邻接顶点的编号外,还要额外保存该顶点到其邻接顶点的边的权值dvw。

    迪杰斯特拉算法正是基于这些信息来实现的。首先我们选择d最小的未知顶点v,因为这是我们在这个阶段所能做的最好选择,而且事实上,从输入顶点s开始,首先找到其邻接顶点并改变邻接顶点的d,并标记s为已知,接下来从d最小的未知顶点v开始,显然这个未知顶点的最小距离就是d,因为从s开始,不论从任何其他顶点回到v,都需要经过(s,v)以外的边,显然这会导致距离大于d,所以每当我们选择到d最小的未知顶点时,它的最短距离就是d,并且我们可以将它标记为已知。接下来,我们考察v的未知邻接顶点。当顶点v的距离d加上其与邻接顶点之间边的权值dvw小于邻接顶点当前的距离d时,就更新邻接顶点的d,并改变邻接顶点的path,重复上述操作直到所有顶点都变为已知,就解决了单源赋权最短路径问题。

    以下图为例,选择输入顶点sv_{1},通过表格来说明Djkstra算法的具体过程,首先将图中顶点(除v_{1}外)的距离d都设置为inf(无穷),显然v_{1}的距离d为0.

    vKnowndpath
    v_{1}00-1
    v_{2}0inf-1
    v_{3}0inf-1
    v_{4}0inf-1
    v_{5}0inf-1
    v_{6}0inf-1
    v_{7}0inf-1

    下一步,将v_{1}标记为已知(用 * 表示),并处理v_{1}的邻接顶点:

     

    vKnowndpath
    v_{1}10-1
    v_{2}02v_{1}
    v_{3}0inf-1
    v_{4}01v_{1}
    v_{5}0inf-1
    v_{6}0inf-1
    v_{7}0inf-1

     下一步,找到d最小的未知顶点v_{4},做和v_{1}相同的处理:

     

    vKnowndpath
    v_{1}10-1
    v_{2}02v_{1}
    v_{3}03v_{4}
    v_{4}11v_{1}
    v_{5}03v_{4}
    v_{6}09v_{4}
    v_{7}05v_{4}

    接下来处理v_{2},在处理邻接顶点时,v_{4}已知,不做处理,处理v_{5}时,因为v_{5}当前的d=3,而v_{2}的d=2,再加上dvw=10,所以v_{5}的d保持不变,path也保持不变。

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_{3}03v_{4}
    v_{4}11v_{1}
    v_{5}03v_{4}
    v_{6}09v_{4}
    v_{7}05v_{4}

    接下来处理v_{3},同v_{2}的处理方法一样,显然v_{6}的d和path需要更新:

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_{3}13v_{4}
    v_{4}11v_{1}
    v_{5}03v_{4}
    v_{6}08v_{3}
    v_{7}05v_{4}

    下一步,处理v_{5}

     

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_{3}13v_{4}
    v_{4}11v_{1}
    v_{5}13v_{4}
    v_{6}08v_{3}
    v_{7}05v_{4}

    下一步处理v_{7}

     

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_{3}13v_{4}
    v_{4}11v_{1}
    v_{5}13v_{4}
    v_{6}08v_{7}
    v_{7}15v_{4}

    最后处理v_{6}

    vKnowndpath
    v_{1}10-1
    v_{2}12v_{1}
    v_{3}13v_{4}
    v_{4}11v_{1}
    v_{5}13v_{4}
    v_{6}16v_{7}
    v_{7}15v_{4}

    到此就得到了所有顶点的赋权最短路径。

    代码实现:

     

    1. void Djkstra(g* p) {
    2. int min, dmin;
    3. for (;;) {
    4. dmin = inf;
    5. int i;
    6. for (i = 0; i < 7; i++) {//找到d最小的未知顶点
    7. if (p->v[i]->known == 0 && p->v[i]->d < dmin) {
    8. dmin = p->v[i]->d;
    9. min = i;
    10. }
    11. }
    12. if (dmin == inf) {//dmin没有改变说明要么所有顶点都是已知,要么虽然有的顶点未知,但是从输入顶点出发不能到达,这两种情况都应该退出循环
    13. break;
    14. }
    15. p->v[min]->known = 1;
    16. l* tmp = p->v[min]->next;
    17. while (tmp != NULL) {//处理min顶点的邻接顶点
    18. if (p->v[tmp->val]->known == 0) {
    19. if (p->v[min]->d + tmp->dvw < p->v[tmp->val]->d) {
    20. p->v[tmp->val]->d = p->v[min]->d + tmp->dvw;
    21. p->v[tmp->val]->path = min;
    22. }
    23. }
    24. tmp = tmp->next;
    25. }
    26. }
    27. }

    显然,Djkstra算法包含两层for循环,所以它的时间复杂度为O(|V|^2)

    Djkstra算法能够处理没有负边的赋权最短路径问题,但是它并不能解决有负边的最短路径问题,这是因为Djkstra算法对已知的顶点不会再做处理,但是有负边时,已知的顶点有可能邻接于其之后处理的顶点,而从这个顶点返回已知顶点的路径更短,这个时候Djkstra算法就不能正确的更新d以及path。 

    带有负值边的赋权最短路径:

    一种处理具有负边图的办法是:给所有边都加上一个常数C,使得所有边都变为正值,这种方法看似可行,实际上那些具有较多边的路径的权重会大于那些较少边的路径,即使之前它们之间的关系不是这样。

    另一种方法是:将赋权和无权的方法结合起来,但是舍弃掉Known这个信息。这种方法使用一个队列来存放将要处理的顶点,首先将s入队,当队列不为空时,队头顶点出队,然后对该顶点的邻接顶点的信息进行更新,并将更新过且不在队列中的顶点入队,重复操作直到队列为空。这个算法基于这样一个事实,如果存在负边,那么所有顶点当前的d都不一定是最小的,那么它们可能仍然需要被更新,所以当处理完当前顶点后,如果该顶点的邻接顶点信息被改变,那么该邻接顶点的邻接顶点信息也要被改变。

    但是如果负值边指向的顶点能够到达图中的任何一个其他顶点(如图),那么这种方法就可能陷入死循环,这个时候就必须设置终止条件,这个条件可以是在任意顶点已经出队|V|+1次后。在图中,每个顶点的最小d实际上应该是负无穷(如果一直循环下去)。但如果将-10改为-2或是-1,那么这条边又会变得没有意义,因为经过这条边到达v_{2}并不会比v_{2}本来的d更小。实际上可以总结为,如果能通过负边减小d的顶点,就可以通过不断地经过负边来使它的d越来越小,而不能通过负边改变d的顶点,它们的信息也就与没有负边时相同。并且在现实生活中,负的权值是很少见的,我们一般处理的权值,比如价格、距离、时间等因素,都是非负的。

     使用队列的赋权最短路径算法:

    1. void WeightedNegative(g* p) {
    2. q* pq = CreatQueue();//建立队列
    3. enqueue(pq, 0);//将输入顶点s入队
    4. p->v[0]->known = 1;//将该顶点标记为已在队列中
    5. while (!isempty(pq)) {//当队列不为空时
    6. int n = dequeue(pq);//出队
    7. p->v[n]->known = 0;//将出队顶点标记为不在队中
    8. if (p->v[n]->dequeuenum == 8)//循环结束条件
    9. break;
    10. p->v[n]->dequeuenum++;//出队后,出队次数+1
    11. l* tmp = p->v[n]->next;
    12. while (tmp != NULL) {//对出队顶点的邻接顶点更新,如果邻接顶点更新了就入队
    13. if (p->v[n]->d + tmp->dvw < p->v[tmp->val]->d) {
    14. p->v[tmp->val]->d = p->v[n]->d + tmp->dvw;
    15. p->v[tmp->val]->path = n;
    16. if (p->v[tmp->val]->known == 0) {
    17. enqueue(pq, tmp->val);
    18. p->v[tmp->val]->known = 1;
    19. }
    20. }
    21. tmp = tmp->next;
    22. }
    23. }
    24. }

    运行结果:

    将循环结束条件改为 p->v[n]->dequeuenum == 80

    将循环结束条件改为 p->v[n]->dequeuenum == 100000

    将图中的-10改为-2,运行结果为:

     

    可以看到此时的运行结果和没有负边的结果是相同的。 

    图的建立和测试代码:

    1. #define inf 99999999
    2. //
    3. typedef struct list {//邻接表
    4. int val;
    5. int dvw;
    6. struct list* next;
    7. }l;
    8. typedef struct table {//图中顶点的信息
    9. int known;
    10. int d;
    11. int path;
    12. int dequeuenum;
    13. l* next;//指向邻接表的指针
    14. }t;
    15. typedef struct graph {//指向定点信息的指针
    16. t* v[7];
    17. }g;
    18. g* CreatGraph() {
    19. g* pg = (g*)malloc(sizeof(g));
    20. for (int i = 0; i < 7; i++) {
    21. t* p = (t*)malloc(sizeof(t));
    22. p->next = NULL;
    23. p->d = inf;
    24. p->known = 0;
    25. p->path = -1;
    26. p->dequeuenum = 0;
    27. pg->v[i] = p;
    28. }
    29. l* p = (l*)malloc(sizeof(l));
    30. p->val = 1;
    31. p->dvw = 2;
    32. p->next = pg->v[0]->next;
    33. pg->v[0]->next = p;
    34. p = (l*)malloc(sizeof(l));
    35. p->val = 3;
    36. p->dvw = 1;
    37. p->next = pg->v[0]->next;
    38. pg->v[0]->next = p;
    39. p = (l*)malloc(sizeof(l));
    40. p->val = 3;
    41. p->dvw = 3;
    42. p->next = pg->v[1]->next;
    43. pg->v[1]->next = p;
    44. /*p = (l*)malloc(sizeof(l));
    45. p->val = 4;
    46. p->dvw = 10;
    47. p->next = pg->v[1]->next;
    48. pg->v[1]->next = p;*/
    49. p = (l*)malloc(sizeof(l));
    50. p->val = 0;
    51. p->dvw = 4;
    52. p->next = pg->v[2]->next;
    53. pg->v[2]->next = p;
    54. p = (l*)malloc(sizeof(l));
    55. p->val = 5;
    56. p->dvw = 5;
    57. p->next = pg->v[2]->next;
    58. pg->v[2]->next = p;
    59. p = (l*)malloc(sizeof(l));
    60. p->val = 2;
    61. p->dvw = 2;
    62. p->next = pg->v[3]->next;
    63. pg->v[3]->next = p;
    64. p = (l*)malloc(sizeof(l));
    65. p->val = 4;
    66. p->dvw = 2;
    67. p->next = pg->v[3]->next;
    68. pg->v[3]->next = p;
    69. p = (l*)malloc(sizeof(l));
    70. p->val = 5;
    71. p->dvw = 8;
    72. p->next = pg->v[3]->next;
    73. pg->v[3]->next = p;
    74. p = (l*)malloc(sizeof(l));
    75. p->val = 6;
    76. p->dvw = 4;
    77. p->next = pg->v[3]->next;
    78. pg->v[3]->next = p;
    79. p = (l*)malloc(sizeof(l));
    80. p->val = 6;
    81. p->dvw = 6;
    82. p->next = pg->v[4]->next;
    83. pg->v[4]->next = p;
    84. p = (l*)malloc(sizeof(l));
    85. p->val = 1;
    86. p->dvw = -10;
    87. p->next = pg->v[4]->next;
    88. pg->v[4]->next = p;
    89. p = (l*)malloc(sizeof(l));
    90. p->val = 5;
    91. p->dvw = 1;
    92. p->next = pg->v[6]->next;
    93. pg->v[6]->next = p;
    94. return pg;
    95. }
    96. //队列
    97. typedef struct queue {
    98. int arr[50];
    99. int front;
    100. int rear;
    101. int num;
    102. }q;
    103. int dequeue(q* p) {
    104. p->num--;
    105. return p->arr[p->front++ % 50];
    106. }
    107. void enqueue(q* p, int x) {
    108. p->num++;
    109. p->arr[(++p->rear) % 50] = x;
    110. }
    111. q* CreatQueue() {
    112. q* p = (q*)malloc(sizeof(q));
    113. p->front = 0;
    114. p->rear = -1;
    115. p->num = 0;
    116. return p;
    117. }
    118. int isempty(q* p) {
    119. return p->num == 0;
    120. }
    121. //打印函数
    122. void print(g* p) {
    123. for (int i = 0; i < 7; i++) {
    124. printf("v%d path = v%d know = %d distance = %d\n", i + 1, p->v[i]->path + 1, p->v[i]->known, p->v[i]->d);
    125. }
    126. }
    127. //测试函数
    128. int main() {
    129. g* pg = CreatGraph();
    130. pg->v[0]->d = 0;
    131. WeightedNegative(pg);
    132. print(pg);
    133. return 0;
    134. }

  • 相关阅读:
    简单工厂模式、工厂模式、抽象工厂模式和加入反射、配置优化后的抽象工厂模式之间的关系和区别
    python opencv 读取文件夹下所有MP4文件并解析成jpg图像
    想拿高薪?先避开这几个坑!
    python execute() 使用%s 拼接sql 避免sql注入攻击 好于.format
    物联网技术解析以及其组件简介
    SpringCloud——Gateway(Predicate断言工厂、Filter过滤器工厂、token校验)
    使用echarts如何实现双y轴且实现指定数据使用y轴呢?
    省级森林防火应急指挥系统
    带头双向循环链表的增删查改(C语言实现)
    纯CSS自定义滚动条样式
  • 原文地址:https://blog.csdn.net/thdwx/article/details/127442385