最短路径问题有最优子结构:最短路径的子路径也是最短路径。
如果图中不包含从源节点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初始化为0 void 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置为u void 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]; } } //BellmanFord bool 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); } } //再次遍历所有的边,检查图中是否存在权重为负值的环路,如果存在,则返回false for (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); else return 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; }
以下为程序运行图:
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; }
程序运行情况:
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的运行时间,都会加快算法的运行时间。
以上为对于本章内容自己的总结~~~