Chapter 8 - Graph-Shortest Paths
1 Shortest Paths 如果图中存在负权环(环上所有边的代价之和为负数),则最短路径不存在 —— 因为可以绕环无限次,让路径长度无限减小。 若图中无负权环,定义源点到自身的最短路径长度为 0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void Unweighted(Table T) { int CurrDist; Vertex V, W; // 按距离从小到大处理所有顶点 for (CurrDist = 0; CurrDist < NumVertex; CurrDist++) { // 找到所有距离为CurrDist且未处理的顶点 for (each vertex V) { if (!T[V].Known && T[V].Dist == CurrDist) { T[V].Known = true; // 更新邻接点的距离 for (each W adjacent to V) { if (T[W].Dist == Infinity) { T[W].Dist = CurrDist + 1; T[W].Path = V; } } } } } }
初始状态:T[v3].Dist = 0,其他顶点的 Dist = Infinity,所有 Known = false。 CurrDist = 0: 中间循环扫所有顶点,找到 Dist=0 且 Known=false 的 v3。 把 T[v3].Known 设为 true。 看 v3 的邻接点(比如 v1、v6),它们的 Dist 是 Infinity,所以: T[v1].Dist = 0+1 = 1,T[v1].Path = v3; T[v6].Dist = 0+1 = 1,T[v6].Path = v3。 CurrDist = 1: 中间循环扫所有顶点,找到 Dist=1 且 Known=false 的 v1 和 v6。 先处理 v1:标记为 Known=true,看它的邻接点 v2、v4,把它们的 Dist 设为 1+1=2,Path 设为 v1。 再处理 v6:同理标记并更新它的邻接点。 CurrDist = 2: 找到 v2、v4,处理它们的邻接点 v5、v7,把距离设为 3。
CurrDist 其实就代表了是第几层 ,本质上就是广度优先搜索
但是对于一条链,比如 找dist为0的点,遍历一遍,找到 找dist为1的点,遍历一遍,找到 找dist为2的点,遍历一遍,找到 … 所以复杂度是 ,太大了
优化:使用队列,对于每一层的每个顶点,把它的下一层,即它邻接的点入队
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void Unweighted(Table T) { Queue Q; Vertex V, W; Q = CreateQueue(NumVertex); MakeEmpty(Q); Enqueue(S, Q); // 源点入队,源点的Dist是0 while (!IsEmpty(Q)) { V = Dequeue(Q); T[V].Known = true; // 此标记非必需,因为每个顶点仅入队一次 // 更新所有未访问的邻接点 for (each W adjacent to V) { if (T[W].Dist == Infinity) { T[W].Dist = T[V].Dist + 1; T[W].Path = V; Enqueue(W, Q); } } } DisposeQueue(Q); }
时间复杂度为 (每个顶点入队 / 出队 1 次,每条边被处理 1 次)
2 Dijkstra’s Algorithm 这是最经典的带权图最短路径算法,基于贪心策略 ,要求所有边的权值非负 。
定义集合 :包含源点 和所有已经找到最短路径的顶点。
对于不在 中的顶点 ,定义 distance[u]:从 出发,只经过 中的顶点 到达 的最短路径长度。
算法步骤:
初始化 ,distance[s]=0,其他顶点为无穷大。
重复直到 包含所有顶点:
选择不在 中且 distance 最小的顶点 ,加入 (贪心选择)。
对 的每个邻接点 ,更新 distance[v] = min(distance[v], distance[u]+c(u,v))。
证明 假设选择 加入 时,distance[u] 其实不是 到 的最短路径长度。那么存在一条更短的路径 ,其中 不在 中,否则这条路径长度肯定小于等于 distance[u]。此时 distance[w] < distance[u],与” 是不在 中距离最小的顶点”矛盾。因此贪心选择是正确的。
伪代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void Dijkstra (Table T) { Vertex V, W; for (;;) { V = smallest unknown distance vertex; if (V == NotAVertex) break ; T[V].Known = true ; for (each W adjacent to V) { if (!T[W].Known) { if (T[V].Dist + Cvw < T[W].Dist) { T[W].Dist = T[V].Dist + Cvw; T[W].Path = V; } } } } }
例如 带权图结构:
算法执行过程:
初始:v1.Dist=0,其他为∞。选v1加入S,更新v2.Dist=2,v4.Dist=1。
选v4(最小未处理)加入S,更新v3.Dist=3,v5.Dist=3,v6.Dist=9,v7.Dist=5。
选v2(最小未处理)加入S,v5当前Dist=3 < 2+10=12,不更新。
选v3(最小未处理)加入S,更新v6.Dist=min(9,3+5)=8。
选v5(最小未处理)加入S,v7当前Dist=5 < 3+6=9,不更新。
选v7(最小未处理)加入S,更新v6.Dist=min(8,5+1)=6。
选v6加入S,算法结束。
2.1 Implementation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 #include <stdio.h> #include <stdlib.h> #include <limits.h> #define Infinity INT_MAX #define NotAVertex -1 #define MaxVertexNum 100 typedef struct { int Dist; int Known; int Path; } TableEntry; typedef TableEntry Table[MaxVertexNum]; typedef int Vertex; typedef int Weight; typedef struct AdjNode { Vertex adjvex; Weight weight; struct AdjNode *next ; } AdjNode; typedef struct { AdjNode *head[MaxVertexNum]; int vertexNum; int edgeNum; } Graph; void InitTable (Vertex start, Graph *G, Table T) { for (int i = 0 ; i < G->vertexNum; i++) { T[i].Dist = Infinity; T[i].Known = 0 ; T[i].Path = NotAVertex; } T[start].Dist = 0 ; } Vertex FindMinDist (Table T, Graph *G) { int minDist = Infinity; Vertex minV = NotAVertex; for (int i = 0 ; i < G->vertexNum; i++) { if (!T[i].Known && T[i].Dist < minDist) { minDist = T[i].Dist; minV = i; } } return minV; } void Dijkstra_Basic (Graph *G, Vertex start, Table T) { InitTable(start, G, T); Vertex V, W; AdjNode *p; for (;;) { V = FindMinDist(T, G); if (V == NotAVertex) break ; T[V].Known = 1 ; p = G->head[V]; while (p != NULL ) { W = p->adjvex; if (!T[W].Known) { if (T[V].Dist + p->weight < T[W].Dist) { T[W].Dist = T[V].Dist + p->weight; T[W].Path = V; } } p = p->next; } } }
外层循环执行 | V | 次(每个顶点处理 1 次) 每次FindMinDist是 O (|V|) 所有边总共被处理 1 次(O (|E|)) 总时间: (稠密图中 | E|≈|V|²,这个复杂度是最优的)
可以用堆来优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 typedef struct { int dist; Vertex v; } PQNode; typedef struct { PQNode data[MaxVertexNum * 10 ]; int size; } MinHeap; void InsertHeap (MinHeap *heap, int dist, Vertex v) { int i = ++heap->size; while (i > 1 && dist < heap->data[i/2 ].dist) { heap->data[i] = heap->data[i/2 ]; i /= 2 ; } heap->data[i].dist = dist; heap->data[i].v = v; } PQNode DeleteMin (MinHeap *heap) { PQNode minNode = heap->data[1 ]; PQNode lastNode = heap->data[heap->size--]; int i = 1 , child; while (i * 2 <= heap->size) { child = i * 2 ; if (child != heap->size && heap->data[child+1 ].dist < heap->data[child].dist) child++; if (lastNode.dist <= heap->data[child].dist) break ; heap->data[i] = heap->data[child]; i = child; } heap->data[i] = lastNode; return minNode; } void Dijkstra_PriorityQueue (Graph *G, Vertex start, Table T) { InitTable(start, G, T); MinHeap heap; heap.size = 0 ; InsertHeap(&heap, 0 , start); Vertex V, W; AdjNode *p; PQNode node; while (heap.size > 0 ) { node = DeleteMin(&heap); V = node.v; if (T[V].Known) continue ; T[V].Known = 1 ; p = G->head[V]; while (p != NULL ) { W = p->adjvex; if (!T[W].Known && T[V].Dist + p->weight < T[W].Dist) { T[W].Dist = T[V].Dist + p->weight; T[W].Path = V; InsertHeap(&heap, T[W].Dist, W); } p = p->next; } } }
更新时,我们把更新后的元素插入到堆中,最多可能要更新 次,每次插入需要 为堆中元素个数,由于Dijkstra算法对于每条边只会插入堆一次,所以堆中元素最多 个,而² ,故总时间复杂度为
3 Graph With Negative Edge Costs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void WeightedNegative (Table T) { Queue Q; Vertex V, W; Q = CreateQueue(NumVertex); MakeEmpty(Q); Enqueue(S, Q); while (!IsEmpty(Q)) { V = Dequeue(Q); for (each W adjacent to V) { if (T[V].Dist + Cvw < T[W].Dist) { T[W].Dist = T[V].Dist + Cvw; T[W].Path = V; if (W不在队列中) { Enqueue(W, Q); } } } } DisposeQueue(Q); }
该算法(SPFA)的思路是,从源点开始,每次检查所有边需不需要更新,核心就在于负权边并不是像Dijsktra算法一样当前最短就能直接视为最短路径了,每次更新都要检查,SPFA 的队列允许一个人反复进出。只要你拿到了更短的距离,不管你之前进过几次队列,你都可以重新排队
如上图,左边的图中,A先入队,然后出队,更新A到C是3,A到B是2,按Dijkstra算法,B就会被认为已经找到了最短路径。 接下来B,C入队,B出队,没有能到达的,C出队,D入队,A到D更新成-3,D出队,更新A到B是-5,B入队;B出队,没有更新的,队空,结束。这时A到B,C,D的最短路是-5,3,-3,正确!
而上图右边的图,A先入队,然后出队,C入队;C出队,D入队;D出队,B入队;B出队,A入队……循环不会停止
怎么避免这种循环不停的情况呢?一条非环的最短路径最多V-1条边,对于一个顶点B,从A到B最多V-1条边,每次更新相当于在原有的路上又多加了一条边,所以一个顶点最多入队V-1次,若入队V次说明有环,如果是0或正的环,不会入队的,所以一定是负权环,所以只需要记录每个顶点入队次数就可以检验
每个顶点最多入队V次(为了检验负权环情况,最多V次),时间复杂度为
4 Acyclic Graphs & AOE If the graph is directed and acyclic, vertices may be selected in topological order since when a vertex is selected, its distance can no longer be lowered without any incoming edges from unknown nodes.
对于有向无环图,按拓扑排序去遍历,到某个顶点时,指向这个顶点的路径已经“没”了,所以此时的dist就是最小的了
计算所有顶点的入度。
将入度为 0 的顶点放入队列。
取出顶点 ,遍历其邻居 :更新距离:if (dist[V] + Cvw < dist[W]) dist[W] = dist[V] + Cvw;
将 的入度减 1,若减为 0 则将 入队。
4.1 AOE 假设有这么一个项目,边称作活动,顶点称作事件,每个事件必须在前一个指向它的事件做完后才可以开始做,求完成这个项目的最短时间 活动可以同时进行,所以时间取决于最耗时的那条路,称作关键路径 关键活动 :不能拖延的活动,即该活动的最早开始时间和最晚开始时间一样 我们先看0到4,上面那条路要7,下面是5,所以上面那条路是关键路径,而活动1的最早开始时间当然是第0天,最晚开始时间是第2天,因为这样2+4+1=7刚好跟上面的路同步完成。或者说,事件2的最早完成时间是第4天,最晚完成时间是第6天