DSAA之图论Dijkstra(贪婪算法)(五)

1. 前言

  有网友把Dijkstra归为BFS算法,笔者觉得是不准确的,两者的差别在于每个阶段是否是最优选择,也就是是否达到局部最优,当然不保证全局最优。

2. 定义

  • We keep all of the same information as before. Thus, each vertex is marked as either known or unknown. A tentative distance dv is kept for each vertex, as before.
  • This distance turns out to be the shortest path length from s to v using only known vertices as intermediates. As before, we record pv, which is the last vertex to cause a change to dv.
  • The general method to solve the single-source shortest-path problem is known as Dijkstra’s algorithm. This thirty-year-old solution is a prime example of a greedy algorithm. Greedy algorithms generally solve a problem in stages by doing what appears to be the best thing at each stage.Dijkstra更加准确的归类为贪婪算法,也就是BFS的每阶段进行最优选择

  第二点是最关键的区分BFS和Dijkstra的地方,前者对已经更新过的dv,不再进行更新。而后者会根据当前处理的节点更新它周边的节点路径信息(如果当前节点提供的加权路径更短),也就是dv。我想画图或者代码能够比较清楚的理解这点,在后文再提及这点。

2. 实现原理

  • Dijkstra’s algorithm proceeds in stages, just like the unweighted shortest-path algorithm. At each stage, Dijkstra’s algorithm selects a vertex v, which has the smallest dv among all the unknown vertices, and declares that the shortest path from s to v is known.
  • The remainder of a stage consists of updating the values of dw In the unweighted case, we set dw= dv+ 1 if dw = . Thus, we essentially lowered the value of dw if vertex v offered a shorter path. If we apply the same logic to the weighted case, then we should set dw = dv+ cv,w if this new value for dw would be an improvement.
  • Put simply, the algorithm decides whether or not it is a good idea to use v on the path to w. The original cost, dw , is the cost without using v; the cost calculated above is the cheapest path using v (and only known vertices)

这里写图片描述
  为了理解这三点,笔者画了个简单的图。假设起始节点为1,上图为无权图,BFS最后的结果为1-2-4,1-3。而实际上我们通过观察图发现1-2,1-3-4也是可以的。因为BFS算法的特点,4在2节点是当前处理节点而被更新,所以在3节点被处理的时候直接忽略。对于无权图来说,这是合理的。所以BFS的结果只是正确答案之一,但一定是正确的。再看看有权图的情况:
  这里写图片描述
  经过Dijkstra算法得出的结果为1-3-4,1-2。上图该结果是唯一的,当处理3的时候,因为1-3-4的dv值更小,所以更新了4的dv值。Dijkstra算法认为1-3-4的路径要优于1-2-4的路径,所以将4的前一个节点变更为3。我相信看到这里就已经很清楚了,Dijkstra因为每一步都做出最优选择,所以为贪婪算法。特别的当修改1-3的权重为10的时候,Dijkstra将不再按照BFS的遍历方式遍历。所以把Dijkstra归类为BFS,严格上说是不正确的。

3. 第一种实现

  同样还是伪代码,这里直接给出DSAA的实现,(当然这很虚,比如昨天笔者做网易的算法题,就是BFS。如果自己动手实现过一遍,肯定不虚的。)

void dijkstra( TABLE T ){
    vertex v, w;
    for( ; ; ){
        //选择当前未处理节点中距离开始节点最短的那个
     /*1*/v = smallest unknown distance vertex;
        if   dont find 
            break;
        T[v].known = TRUE;
        for each w adjacent to v
            if( !T[w].known )
                //如果当前known节点让邻接节点拥有更小的路径,则更新该邻接节点
                /*2*/if( T[v].dist + cv,w < T[w].dist ){ /* update w */
                    decrease( T[w].dist to T[v].dist + cv,w);
                    T[w].path = v;
                }
    }
}

  1,2语句让Dijkstra区别于BFS而完全是贪婪算法。

时间复杂度分析:

  • The running time depends on how the table is manipulated, which we have yet to consider. If we use the obvious algorithm of scanning down the table to find the minimum dv, each phase will take O ( | V | ) time to find the minimum, and thus O ( | V | 2 ) time will be spent finding the minimum over the course of the algorithm.
  • The time for updating dw is constant per update, and there is at most one update per edge for a total of O ( | E | ) . Thus, the total running time is O ( | E | + | V | 2 ) = O ( | V | 2 ) .
    • If the graph is dense, with | E | = ( | V | 2 ) , this algorithm is not only simple but essentially optimal, since it runs in time linear in the number of edges.
    • If the graph is sparse, with | E | = ( | V | ) , this algorithm is too slow.

  上面解释的大部分比较清楚,有几点理解困难的地方稍微强调下:

  • 每个边最多更新一次,虽然从算法上感觉好像每个顶点会被多次更新,但是注意顶点更新的是对应的边,比如:更新v2点,上一次是v1-v2,这次v3-v2。
  • 密集型图是指(最极端情况)任意一个顶点都和其他顶点连通,所以 | E | = ( | V | 2 ) ,这种情况下算法时间复杂度为边的线性函数。所以叫快速算法。
  • 但是稀疏型的图, | E | = ( | V | ) ,此时 O ( | V | 2 ) 的时间复杂度让该算法成为较慢的选择。

4. 第二种实现(优先堆)

  这里有两种选择,笔者只记录第二种插入的方法:

  • The alternate method is to insert w and the new value dw into the priority queue every time line 9 is executed. Thus, there may be more than one representative for each vertex in the priority queue. When the delete_min operation removes the smallest vertex from the priority queue, it must be checked to make sure that it is not already known.
  • Thus, line 2 becomes a loop performing delete_mins until an unknown vertex emerges.
      

  DSAA没有给出伪代码,但是根据上面的原理,我们自己也能很快写出来伪代码:

void dijkstra( TABLE T )
{
    vertex v, w;
    //初始情况下,除了起始顶点dv为0,其他都是负无穷
    //特别的T应为
    /*struct cell{
       int dv;
       int pv;
       int v;
    }的数组
    */
    p_q.inital(T);
    for( ;! p_q.empty(); ){
        v = p_q.delete_min()
        if(T[v].known == TRUE)
            continue;
        T[v].known = TRUE;
        for each w adjacent to v
            if( !T[w].known )
                //如果当前known节点让邻接节点拥有更小的路径,则更新该邻接节点
                /*9*/if( T[v].dist + cv,w < T[w].dist ){ /* update w */
                    decrease( T[w].dist to T[v].dist + cv,w);
                    T[w].path = v;
                    //插入优先堆中
                    p_q.insert(T[w]) 
                }
    }
}

  上面算法的难理解的地方就是优先堆中可能存在重复的顶点,但是这不是问题,因为我们使用know标记每个顶点,又根据优先堆的特点,即使真的存在重复的顶点,也会被忽略。特别注意优先堆中最多可能存在 | E | 个顶点。

时间复杂度

  • Since | E | | V | 2 implies that l o g | E | 2 l o g | V | . Thus, we still get an O ( | E | l o g | V | ) algorithm.
  • However, the space requirement does increase, and this could be important in some applications. Moreover, because this method requires | E | delete_mins instead of only | V | , it is likely to be slower in practice.

  理解上面的时间复杂度,还是需要自己亲手推导下: T ( E , V ) = O ( | E | + | E | l o g V ) = O ( 2 l o g | V | + | E | l o g V ) = O ( | E | l o g | V | ) 。如果使用更加简略的计算方式: T ( E , V ) = O ( | E | 2 l o g V ) 。这个上界不好,因为其远远坏于最坏情况。(背包9讲中上界计算方式同理)
  

5. 最后

  我写这篇文章前,查阅很多资料,也看了其他网友的文章(大部分都没有经典书籍写的好)。我觉得理解想要深刻,必须多方面思考,多角度思考。费时间是真的,但是做一件事,就应该尽力做好。

猜你喜欢

转载自blog.csdn.net/lovestackover/article/details/80577508
今日推荐