算法导论第24章 单源最短路径

来源:互联网 发布:视频 嗅探软件 编辑:程序博客网 时间:2024/05/09 06:35

最短路径问题有最优子结构:最短路径的子路径也是最短路径。

如果图中不包含从源节点s可以到达的权重为负值的环路,则对于所有的结点v,最短路径权重有精确定义。

不存在最短路径的两种情况:

1、从结点s到v的某条路径上存在权重为负值的环路,则δ(s,v) = -∞

2、从结点s到v不存在路径,,则δ(s,v) = ∞

最短路径都是简单路径,且不存在环


本章中有三个最短路径算法

1、Bellman-Ford算法:解决的是一班情况下的单源最短路径问题,可适用于边的权重为负值,且有环路的情况,算法返回一个bool值,表明是否存在一个从源结点可以到达的权重为负值的环路。如果存在,则返回false,否则,可以求出最短路径和这条路径的权重。

2、Dag_Shortest_Paths: 解决有向无环图的单源最短路径问题,算法根据结点的拓扑排序对带权重的有向无环图G= (V, E)进行边的松弛操作,则可以在θ(V+E)的时间内计算出从单个源节点到所有结点之间的最短路径,允许有负权值的边存在。

3、Dijkstra算法:解决的是带权重的有向图,算法要去所有的权重大于0,运行时间小于Bellman-Ford算法,算法在运行过程中维持一组结点集合S,从源节点s到该集合中的每个结点之间的最短距离已经找到。算法重复从结点集V-S中选择最短路径估计中最小的结点u,将u加入集合S,然后对从u出发的边进松弛操作。


松弛操作:

在每个结点中维持一个属性v.d :记录从源节点s到结点v的最短路径权重的上界,称为最短路径估计

首先对最短路径估计和前驱结点进行初始化,即InitializeSingleSource,即将所有结点v.p = NULL, v.d = ∞ ,  然后源点s.d = 0

对每条边(u, v)的松弛操作是:如果源结点s到u的最短路径加上边(u, v)的权重大于s到v的最短路径估计,则对v.d,v.p进行更新,松弛操作可以降低路径的最短路径估计。

三角不等式性质:对于任何边(u, v)属于E,有 δ(s,v)<=  δ(s, u) + w(u, v)

上界性质:对于所有结点有v.d >=  δ(s,v)。一旦v.d的值达到(s,v),则其值不再变化。


BellmanFord算法通过对边进行松弛操作来渐进的降低从源节点s到每个结点v的最短路径估计值v.d,直到最短路径估计与最短路径相同为止。对每条边总共松弛V-1次操作。

求出最短路径后判断是否有负值的环路,判断方法是对每条边(u, v)测试是否v.d > u.d + w(u, v),如果是,则不符合三角不等式性质,则说明存在负环。

运行时间:因为总共有E条边,而每条边都进行V-1次操作,所以时间复杂度为O(V*E)

以下为代码:

//对每个结点的最短路径估计和前驱结点进行初始化,最短路径初始化为INT_MAX, p初始化为NULL//并将源节点的key初始化为0void InitalizeSingleSource(int index){for (int i = 0; i < MAX_VERTEX_NUM; i++){vertices[i].key = INT_MAX>>2;vertices[i].p = NULL;}vertices[index].key = 0;}//对边(u, v)进行松弛,将目前s到v的最短路径v.key与s到u的最短路径加上w(u, v)的值进行比较//如果比后面的值还大,则进行更新,将v.key缩短,并且将p置为uvoid relax(ArcNode *arc){//竟然溢出了!!if (vertices[arc->adjvex].key > vertices[arc->source].key + arc->weight){vertices[arc->adjvex].key = vertices[arc->source].key + arc->weight;vertices[arc->adjvex].p = &vertices[arc->source];}}//BellmanFordbool BellmanFord(int index){InitalizeSingleSource(index-1);for (int i = 1; i < vexnum; i++)     //循环共进行vexnum-1次{//遍历所有的边,并对每个边进行一次松弛for (int j = 0; j < vexnum; j++){for (ArcNode *arc = vertices[j].firstarc; arc != NULL; arc = arc->nextarc)relax(arc);}}//再次遍历所有的边,检查图中是否存在权重为负值的环路,如果存在,则返回falsefor (int j = 0; j < vexnum; j++){for (ArcNode *arc = vertices[0].firstarc; arc != NULL; arc = arc->nextarc){if (vertices[arc->adjvex].key > vertices[arc->source].key + arc->weight)return false;}}cout << "BellmanFord求出的单源最短路径:" << endl;for (int i = 1; i < vexnum; i++){printPath(index-1, i);}cout << "**************************************************" << endl;return true;}
代码的运行过程和图如下:


DagShortestPaths算法开始时对图进行拓扑排序,以便获得顶点的先行序列,然后按照这个顺序依次对结点求最短路径:

//用一个栈记录入度为0的结点,当栈不为空时,一个结点出栈,然后依次遍历这个结点//的邻接表,对每个邻接的结点的入度减一,邻接的结点入度变为0时进栈,循环直到栈为空//用count记录出栈的结点,如果等于vexnum,则图无环,拓扑排序正确,否则,图有环pair<bool, vector<int> > TopologicalSort(){stack<VNode> stk;vector<int> ivec;for (int i = 0; i < vexnum; i++){if (vertices[i].indegree == 0)stk.push(vertices[i]);}cout << "图的拓扑排序是:" << endl;int count = 0;while (stk.empty() == false){cout << stk.top().data << "->";ArcNode *arc = stk.top().firstarc;if (arc != NULL)ivec.push_back(arc->source);stk.pop();count++;for (; arc != NULL; arc = arc->nextarc){if (!--(vertices[arc->adjvex].indegree))stk.push(vertices[arc->adjvex]);}}cout << endl;if (count < vexnum)return make_pair(false, ivec);elsereturn make_pair(true, ivec);}//在有向无环图中,可以按照结点的拓扑顺序来对图的边进行松弛操作,可以带有负的权值//按照拓扑排序遍历结点,可以保证边全部被遍历一遍,因为如果有向无环图中有从u到v的一条路径,//则u在拓扑排序中的次序位于结点v的前面,void DagShortestPaths(int index){vector<int> vv = TopologicalSort().second;InitalizeSingleSource(index-1);for (int i = 0; i < vv.size(); i++){ArcNode* arc = vertices[vv[i]].firstarc;for (; arc != NULL; arc = arc->nextarc)relax(arc);}cout << "DagShortestPaths求出的单源最短路径:" << endl;for (int i = index; i < vexnum; i++){printPath(index-1, i);}cout << "**************************************************" << endl;}

算法的时间分析:拓扑排序需要O(V+E)时间,initializeSingleSource需要O(V)时间,for循环对每个顶点只有一次迭代,对每个顶点,其邻接的边依次被relax,即将所有的边遍历了一遍,所以运行时间为要O(V+E)

以下为程序运行图:




Dijkstra算法:权值非负,与Prim类似,与BFS也类似,用到了优先队列,运行时间取决于优先队列的实现方式,

队列的操作有extractmin, insert, decreasekey,代码:

void Dijkstra(int index){InitalizeSingleSource(index-1);vector<VNode> snode;       //保存已经找到最短路径的结点vector<VNode *> que;       //保存结点的指针的数组,用这个数组执行堆的算法//将结点指针进队列,形成以key为关键值的最小堆for (int i = 0; i < vexnum; i++)que.push_back(&(vertices[i]));//使que按照pvnodecompare准则构成一个最小堆make_heap(que.begin(), que.end(), PVNodeCompare());   while (que.empty() == false){//将队列中拥有最小key的结点出队VNode *node = que.front();pop_heap(que.begin(), que.end(), PVNodeCompare());   //从堆中删除最小的结点,只是放到了vector的最后que.pop_back();      //将vector中的这个结点彻底删除,因为后面还要再排序一次,以免影响后面的堆排序,pop算法。snode.push_back(*node);for (ArcNode *arc = node->firstarc; arc != NULL; arc = arc->nextarc)relax(arc);make_heap(que.begin(), que.end(), PVNodeCompare());}cout << "Dijkstra求出的单源最短路径:" << endl;for (int i = 1; i < vexnum; i++){if (i != index-1)   printPath(index-1, i);}cout << "**************************************************" << endl;}

关于运行时间:extractmin 、insert运行V次,decreasekey运行E次

程序运行情况:


1、队列仅仅用简单数组,每次选择数组中最小的结点extractmin需要O(V),总共出选择V次,所以总共O(V*V),

而insert和decreasekey需要O(1)时间,总共运行E次,所以总运行时间O(V*V+E)

2、用二叉堆实现,则extractmin,decreasekey需要O(lgV)时间,所以运行时间为O((V+E)lgV)

3、用斐波那契堆实现,运行时间提升到O(VlgV+E),extract-min平摊代价为O(lgV),而decreaseKey平摊时间为O(1)

对于该算法,decreasekey的调用要比extractmin调用一般要多得多,所以不增加extractmin的运行时间,而减小decreasekey的运行时间,都会加快算法的运行时间。


以上为对于本章内容自己的总结~~~


0 0
原创粉丝点击