560 likes | 684 Views
第八章 图. 图的基本概念 图的存储结构 图的遍历 最小生成树 最短路径 拓扑排序 关键路径. 最短路径 (Shortest Path). 最短路径问题: 如果从图中某一顶点 ( 称为源点 ) 到达另一顶点 ( 称为终点 ) 的路径可能不止一条,如何找到一条路径,使得沿此路径各边上的权值总和达到最小。 问题解法 单源最短路径 — Dijkstra 算法 任意顶点对之间的最短路径 — Floyd 算法. 单源最短路径问题.
E N D
第八章 图 • 图的基本概念 • 图的存储结构 • 图的遍历 • 最小生成树 • 最短路径 • 拓扑排序 • 关键路径
最短路径 (Shortest Path) • 最短路径问题:如果从图中某一顶点(称为源点)到达另一顶点(称为终点)的路径可能不止一条,如何找到一条路径,使得沿此路径各边上的权值总和达到最小。 • 问题解法 • 单源最短路径 — Dijkstra算法 • 任意顶点对之间的最短路径 — Floyd算法
单源最短路径问题 • 问题的提出: 给定一个带权有向图G与源点v,求从v到G中其它顶点的最短路径。限定各边上的权值大于或等于0。 • 为求得这些最短路径,Dijkstra提出按路径长度的递增次序,逐步产生最短路径的算法。首先求出长度最短的一条最短路径,再参照它求出长度次短的一条最短路径,依次类推,直到从顶点v到其它各顶点的最短路径全部求出为止。 • 举例说明
算法的基本思想 设置并逐步扩充一个集合S,存放已求出的最短路径的顶点,则尚未确定最短路径的顶点集合是V-S, 为了直观起见,我们设想S中顶点均被涂成红色,V-S中的顶点均被涂成蓝色。算法初始化时,红点集中仅有一个源点,以后每一步都是按最短路径长度递增的顺序,逐个地把蓝点集中的顶点涂成红色后,加入到红点集中。
算法粗框: while ( S 中的红点数 < n ) 在当前蓝点集中选择一个最短路径长度最短的 蓝点扩充到蓝点集中; 那么,如何在蓝点集中选择一个最短路径长度最短的蓝点呢? 注意:这种蓝点所对应的最短路径上,除终点外,其余顶点都是红点。为此,对于图中每一个顶点 i ,都必须记住从v 到i 、且中间只经过红点的最短路径的长度,并将此长度记作 i 的距离值。
开始时,红点集只有一个源点v,初始蓝点集中的蓝点j的距离值D[j]均为有向边<v,j>上的权值。开始时,红点集只有一个源点v,初始蓝点集中的蓝点j的距离值D[j]均为有向边<v,j>上的权值。 用数组D(n)来存放n个顶点的距离值。若当前蓝点集中具有最小距离值的蓝点是k,则其距离值D(k)是k的最短路径长度,并且k是蓝点集中最短路径长度最短的顶点。
证明1: (距离值D(k)是k的最短路径长度) 若D(k)不是k的最短路径长度,则必存在另外一条从源点v到k的路径P,其长度小于D(k)。由距离值的定义可知,路径P上必然包含一个或多个蓝点作为中间点。假设从源点沿P向前第一次碰到的蓝点是x,则P上从源点v到x的这一段路径的长度,显然不小于x的距离值D(x)。而P上从x到终点k所经过的边上,其权值均为非负实数,所以D(x)<=P的长度。又因为P的长度<D(k)(这是假设前提),于是有下述不等式: D(x) <= P的长度 < D(k) 由此可知:D(x) < D(k),这与k是蓝点集中距离值最小的蓝点产生矛盾。所以D(k)是k的最短路径长度。
蓝点k 源点v 红点集S 蓝点x
证明2: (k是蓝点集中最短路径长度最短的顶点) 设i是蓝点集中任何一个异于k的顶点,若i的最短路径只经过红点,则该最短路径长度是i的距离值D(i) ,因为D(k)是当前蓝点集距离值最小的顶点,所以D(i) >= D(k)。若i的最短路径P包含其它蓝点作为中间点,设P上第一个蓝点是j,则P上从v到j的路径长度就是j的距离值D(j)。显然,D(j)>=D(k),而P的长度=D(j)+j到i的长度,因为j到i的长度为非负实数,所以 P的长度>=D(k)。由此可知蓝点集中任意顶点i的最短路径长度都不会小于k的最短路径长度D(k)。
扩充红点集的方法:每一步只要在当前蓝点集中选择一个具有最小的距离值的蓝点k扩充到红点集合中,k被涂成红色之后,剩余的蓝点的距离值可能由于增加了新红点k而发生变化(即减少)。因此必须调整当前蓝点集中各蓝点的距离值。扩充红点集的方法:每一步只要在当前蓝点集中选择一个具有最小的距离值的蓝点k扩充到红点集合中,k被涂成红色之后,剩余的蓝点的距离值可能由于增加了新红点k而发生变化(即减少)。因此必须调整当前蓝点集中各蓝点的距离值。 算法框架: S = {v}; 置初始蓝点集中各蓝点的距离值; while ( S中红点数 < n ) { 在当前蓝点集中选择距离值最小的顶点k; S = S + {k}; /* 将k涂成红色加入红点集 */ 调整剩余蓝点的距离值; }
如何调整剩余蓝点的距离值呢? 若新红点k加入红点集S后,使得某个蓝点j的距离值D(j)减少,则必定是由于存在一条从源点v途径新红点k最终到达蓝点j且中间只经过红点的、新的最短路径Pvkj,它的长度小于从源点v到达j且中间只经过老红点(即步包含k)的原最短路径Pvj的长度D(j)。由于Pvkj是一条中间只经过红点的最短路径,所以,它的前一段从v到k的路径必定是k的最短路径,其长度为D(k);它的后一段从k到j的路径Pkj只可能有两种情形:其一是由k经过边<k,j>直达蓝点j;其二是从k出发再经过S中若干老红点后到达j。
用反正法证明后一种情形是不可能的。 假设从源点v出发,经过红点k、老红点x,最后到达蓝点j的新最短路径Pvkxj的长度小于原D(j)。因为x比k先加入红点集S,故D(x)<=D(k),又因为权值非负,所以从v到x的路径Pvx的长度不大于从v经k到x的路径Pvkx的长度,即 length(Pvk)=D(x)<=D(k)+length(Pkx)=length(Pvkx) 因此从v经x到j的路径长度: length(Pvxj)=length(Pvk)+length(Pxj) 不大于从v经k、x到j的新路径长度 length(Pvkxj)=length(Pvkx)+length(Pxj) 又因为Pvxj中间只经过老红点,所以Pvxj的长度不大于D(j)的值,由此可得: D(j)<=length(Pvxj)<=length(Pvkxj)
这与新路径Pvkxj的长度小于原D(j)值的假设相矛盾!因此,使得D(j)值减小的新路径比必是先经过老红点到达k,然后经过边<k,j>直达蓝点j的,它得长度是D(k)+边<k,j>上的权。这与新路径Pvkxj的长度小于原D(j)值的假设相矛盾!因此,使得D(j)值减小的新路径比必是先经过老红点到达k,然后经过边<k,j>直达蓝点j的,它得长度是D(k)+边<k,j>上的权。 由此得到调整距离值的方法:当顶点k从蓝点集转移到红点集时,对蓝点集扫描检查,若某蓝点j的原距离值D(j)大于新路径的长度D(k)+边<k,j>上的权,则将D(j)修改成此长度值。 为了同时得到路径,设置一个路径向量P[n],其中P[i]表示从源点到达i点的最短路径上该点的前驱顶点。
蓝点j x 源点v 红点集S 蓝点k
1 10 100 2 5 30 50 10 60 3 4 20 max 1 max 2 20 3 <- 4 0 4 30 5 <- 3 <- 4
1 10 2 5 30 10 3 4 20
float D[n]; int P[n],S[n]; DIJKSTRA(float C[][n], int v) { int i,j,k,v1,pre; int min,max=60,inf=80; v1=v-1; for (i=0;i<n;i++) { D[i]=C[v1][i]; if (D[i]!=max) P[i]=v; else P[i]=0; } for (i=0;i<n;i++) S[i]=0; S[v1]=1; D[v1]=0; for (i=0;i<n-1;i++) { min=inf; for (j=0;j<n;j++) if ((!S[j])&&(D[j]<min)) { min=D[j]; k=j; } S[k]=1; for (j=0;j<n;j+=) if ((!S[j])&&(D[j]>D[k]+C[k][j])) { D[j]=D[k]+C[k][j]; P[j]=k+1; } } for (i=0;i<n;i++) { printf(“%f\n%d”,D[i],i+1); pre=p[i]; while (pre!=0) { printf(“<--%d”,pre); pre=P[pre-1]; } } }
所有顶点对之间的最短路径 算法说明 对于顶点i和j: 1、首先,考虑从i到j是否有以顶点1为中间点的路径,:i,1,j,即考虑图中是否有边<i,1>和<1,j>,若有,则新路径i,1,j的长度是C[i][1]+C[1][j],比较路径i,j和i,1,j,的长度,并以较短者为当前所求得的最短路径,。该路径是中间点序号不大于1的最短路径。
2、其次,考虑从i到j是否包含顶点2为中间点的路径:i,...,2,...,j,若没有,则从i到j的最短路径仍然是第一步中求出的,即从i到j的中间点序号不大于1的最短路径;若有,则i,...,2,...,j可分解成两条路径i,...,2和2,...,j,而这两条路径是前一次找到的中间点序号不大于1的最短路径,将这两条路径相加就得到路径i,...,2,...,j的长度,将该长度与前一次求出的从i到j的中间点序号不大于1的最短路径长度比较,取其较短者作为当前求得的从i到j的中间点序号不大于2的最短路径。2、其次,考虑从i到j是否包含顶点2为中间点的路径:i,...,2,...,j,若没有,则从i到j的最短路径仍然是第一步中求出的,即从i到j的中间点序号不大于1的最短路径;若有,则i,...,2,...,j可分解成两条路径i,...,2和2,...,j,而这两条路径是前一次找到的中间点序号不大于1的最短路径,将这两条路径相加就得到路径i,...,2,...,j的长度,将该长度与前一次求出的从i到j的中间点序号不大于1的最短路径长度比较,取其较短者作为当前求得的从i到j的中间点序号不大于2的最短路径。
3、然后,再选择顶点3加入当前求得的从i到j中间点序号不大于2的最短路径中,按上述步骤进行比较,从未加入顶点3作中间点的最短路径和加入顶点3作中间点的新路径中选取较小者,作为当前求得的从i到j的中间点序号不大于3的最短路径。依次类推,直到考虑了顶点n加入当前从i到j的最短路径后,选出从i到j的中间点序号不大于n的最短路径为止。由于图中顶点序号不大于n,所以从i到j的中间点序号不大于n的最短路径,已考虑了所有顶点作为中间点的可能性。因而它必是从i到j的最短路径。3、然后,再选择顶点3加入当前求得的从i到j中间点序号不大于2的最短路径中,按上述步骤进行比较,从未加入顶点3作中间点的最短路径和加入顶点3作中间点的新路径中选取较小者,作为当前求得的从i到j的中间点序号不大于3的最短路径。依次类推,直到考虑了顶点n加入当前从i到j的最短路径后,选出从i到j的中间点序号不大于n的最短路径为止。由于图中顶点序号不大于n,所以从i到j的中间点序号不大于n的最短路径,已考虑了所有顶点作为中间点的可能性。因而它必是从i到j的最短路径。
算法的基本思想就是: 从初始的邻接矩阵A0开始,递推地生成矩阵序列A1,A2,...,An 显然,A中记录了所有顶点对之间的最短路径长度。若要求得到最短路径本身,还必须设置一个路径矩阵P[n][n],在第k次迭代中求得的path[i][j],是从i到j的中间点序号不大于k的最短路径上顶点i的后继顶点。算法结束时,由path[i][j]的值就可以得到从i到j的最短路径上的各个顶点。
Floyd算法 int path[n][n]; FLOYD(float A[][n],float C[][n]) { int i,j,k,next; int max=160; for (i=0,i<n,i++) for (j=0;j<n;j++) { if (C[i][j]!=max) path[i][j]=j; else path[i][j]=0; A[i][j]=C[i][j]; } for (k=0;k<n;k++) for (i=0;i<n;i++) for (j=0;j<n;j++) if (a[i][j]>(A[i][k]+A[k][j])) { A[i][j]=A[i][k]+A[k][j]; path[i][j]=path[i][k]; } for (i=0;i<n;i++) for (j=0;j<n;j++) { printf(“%f”,A[i][j]); next=path[i][j]; if (next==0) printf(“%d to %d no path.\n”,i+1;j+1); else { printf(“%d”,i+1); while (next!=j) { printf(“-->%d”,next+1); next=path[next][j]; } printf(“-->%d”,j+1); } } }
1 10 100 2 5 30 50 10 60 3 4 20
拓扑排序 • 计划、施工过程、生产流程、程序流程等都是“工程”。除了很小的工程外,一般都把工程分为若干个叫做“活动”的子工程。完成了这些活动,这个工程就可以完成了。 • 例如,计算机专业学生的学习就是一个工程,每一门课程的学习就是整个工程的一些活动。其中有些课程要求先修课程,有些则不要求。这样在有的课程之间有先后关系,有的课程可以并行地学习。
课程名称 先修课程 课程代号 C1高等数学 C2程序设计基础 C3离散数学 C1, C2 C4数据结构 C3, C2 C5高级语言程序设计 C2 C6编译方法 C5, C4 C7操作系统 C4, C9 C8普通物理 C1 C9计算机原理 C8
可以用有向图表示一个工程。在这种有向 图中,用顶点表示活动,用有向边<Vi, Vj> 表示活动的前后次序。Vi 必须先于活动Vj 进行。这种有向图叫做顶点表示活动的 AOV网络(Activity On Vertices)。 • 在AOV网络中,如果活动Vi 必须在活动Vj 之前进行,则存在有向边<Vi, Vj>, AOV 网络中不能出现有向回路,即有向环。在 AOV网络中如果出现了有向环,则意味着 某项活动应以自己作为先决条件。 • 因此,对给定的AOV网络,必须先判断它 是否存在有向环。
检测有向环的一种方法是对AOV网络构造它的拓 扑有序序列。即将各个顶点 (代表各个活动) 排列 成一个线性有序的序列,使得AOV网络中所有应 存在的前驱和后继关系都能得到满足。 • 这种构造AOV网络全部顶点的拓扑有序序列的运 算就叫做拓扑排序。 • 如果通过拓扑排序能将AOV网络的所有顶点都排 入一个拓扑有序的序列中,则该AOV网络中必定 不会出现有向环;相反,如果得不到满足要求的 拓扑有序序列,则说明AOV网络中存在有向环, 此AOV网络所代表的工程是不可行的。
例如,对学生选课工程图进行拓扑排序,得到的拓扑有序序列为例如,对学生选课工程图进行拓扑排序,得到的拓扑有序序列为 C1 , C2 , C3 , C4 , C5 , C6 , C8 , C9 , C7或 C1 , C8 , C9 , C2 , C5 , C3 , C4 , C7 , C6
进行拓扑排序的方法 • 输入AOV网络。令 n 为顶点个数。 • 1、在AOV网络中选一个没有直接前驱的顶 • 点(即此顶点入度为0), 并输出之; 2、从图中删去该顶点, 同时删去所有它发出 的有向边; 重复以上两步, 直到 • 全部顶点均已输出,拓扑有序序列形成, 拓扑排序完成;或 • 图中还有未输出的顶点,但已跳出处理循 环。这说明图中还剩下一些顶点,它们都 有直接前驱,再也找不到没有前驱的顶点 了。这时AOV网络中必定存在有向环。
a b c d e f
1 2 4 3 6 5 为了便于考察每个顶点的入度,在顶点表中增加一个入度域,同时设置一个栈来存储所有入度为0 的顶点。在进行拓扑排序之前,只要对顶点表扫描一遍,将所有入度为0 的顶点都推入栈中,一旦排序过程中出现新的入度为0 的顶点,也同样将其推入栈中。 出边表 。。。 。。。 。。。 。。。
拓扑排序算法框架 1、扫描顶点表,将入度为0 的顶点入栈; 2、while ( 栈非空 ) { 将栈顶顶点v弹出并输出之; 检查v的出边,将每条出边<v,u>终点u的入度减1, 若u的入度变为0,则把u推入栈; } 3、若输出的顶点数小于n,则输出“有回路”;否则拓扑 排序正常结束。
在算法具体实现时,上述链栈无须占有额外的空间,而是利用顶点表中值为0 的入度域来存放链栈的指针(用下标值模拟)。因为顶点域中已经存入有相应的顶点,故入栈时只需修改相应的指针。
1 2 4 3 6 5 top 0 0 0 0 2 2 2 1 top 1 1 1 4 2 2 1 0 3 3 2 2 top 0 1 1 1 top=0
1 2 4 3 6 5 0 0 0 0 top 4 4 4 4 4 4 4 4 0 top 0 0 0 1 1 top 0 0 1 1 1 1
拓扑排序算法 typedef int datatype; typedef int vextype; typedef struct node /*边表结点定义*/ { int adjvex; struct node *next; } edgenode; typedef struct /*顶点表结点定义*/ { vextype vertex int id; edgenode *link } vexnode; vexnode dig[n];
while (p) { k=p->adjvex; dig[k].id--; if (dig[k].id==0) { dig[k].id=top; top=k; } p=p->next; } } if (m<n) printf(“\nThe network has a cycle\n”); } TOPOSORT(vexnode dig[]) { int i,j,k,m=0,top=-1; edgenode *p; for (i=0;i<n;i++) if (dig[i].id==0) { dig[i].id=top; top=i; } while (top!=-1) { j=top; top=dog[top].id; printf(“%d\t”, dig[j].vertex+1); m++; p=dig[j].link;
算法分析 设AOV网有n个顶点,e条边。初始建立入度为0 的顶点栈,要检查所有顶点一次,执行时间为O(n);排序中,若AOV网无回路,则每个顶点入、出栈各一次,每个边表结点被检查一次,执行时间为O(n+e),所以总的时间复杂度为O(n+e)。
关键路径 • 如果在无有向环的带权有向图中 • 用有向边表示一个工程中的各项活动(Activity) • 用边上的权值表示活动的持续时间(Duration) • 用顶点表示事件(Event) • 则这样的有向图叫做用边表示活动的网络,简称AOE (Activity On Edges)网络。 • AOE网络在某些工程估算方面非常有用。例如,可以使人们了解: • (1) 完成整个工程至少需要多少时间(假设网络中没有环). • (2) 为缩短完成工程所需的时间, 应当加快哪些活动.
在AOE网络中, 有些活动顺序进行,有些活动并 行进行。 • 从源点到各个顶点,以至从源点到汇点的有向路 径可能不止一条。这些路径的长度也可能不同。 完成不同路径的活动所需的时间虽然不同,但只 有各条路径上所有活动都完成了,整个工程才算 完成。 • 因此,完成整个工程所需的时间取决于从源点到 汇点的最长路径长度,即在这条路径上所有活动 的持续时间之和。这条路径长度最长的路径就叫 做关键路径(Critical Path)。
定义几个与计算关键活动有关的量: • 事件Vi的最早可能开始时间Ve(i) 是从源点V0到顶点Vi 的最长路径长度。 • 事件Vi 的最迟允许开始时间Vl[i] 是在保证汇点Vn-1在Ve[n-1] 时刻完成的前提 下,事件Vi的允许的最迟开始时间。 • 活动ak 的最早可能开始时间 e[k] 设活动ak 在边< Vi , Vj >上,则e[k]是从源点 V0到顶点Vi的最长路径长度。因此, e[k] = Ve[i]。 • 活动ak 的最迟允许开始时间 l[k] l[k]是在不会引起时间延误的前提下,该活动允 许的最迟开始时间。
l[k] = Vl[j] - dur(<i, j>)。 其中,dur(<i, j>)是完成ak 所需的时间。 • 时间余量 l[k] - e[k] 表示活动ak 的最早可能开始时间和最迟允许开始时间的时间余量。l[k] == e[k]表示活动ak 是没有时间余量的关键活动。显然,关键路径上的所有活动都是关键活动。 • 为找出关键活动, 需要求各个活动的 e[k] 与 l[k],以判别是否 l[k] == e[k]. • 为求得e[k]与 l[k],需要先求得从源点V0到各个顶点Vi 的 Ve[i] 和 Vl[i]。 • 求Ve[i]的递推公式
从Ve[0] = 0开始,向前递推 < Vi, Vj > S2, i = 1, 2, , n-1 其中, S2是所有从Vi指向顶点Vj 的有向边< Vi, Vj>的集合。 • 从Vl[n-1] = Ve[n-1]开始,反向递推 < Vi, Vj > S1, i = n-2, n-3, , 0 其中, S1是所有从顶点Vi发出的有向边< Vi, Vj>的集合。
这两个递推公式的计算必须分别在拓扑有序及逆拓扑有这两个递推公式的计算必须分别在拓扑有序及逆拓扑有 序的前提下进行。 • 设活动ak (k = 1, 2, …, e)在带权有向边< Vi, Vj> 上, 它的 持续时间用dur (<Vi, Vj>) 表示,则有 e[k] = Ve[i]; l[k] = Vl[j] -dur(<Vi, Vj>);k = 1, 2, …, e。 这样就得到计算关键路径的算法。 • 计算关键路径时,可以一边进行拓扑排序一边计算各顶 点的Ve[i],并按拓扑有序的顺序对各顶点重新进行了编 号。算法在求Ve[i], i=0, 1, …, n-1时按拓扑有序的顺序计 算,在求Vl[i], i=n-1, n-2, …, 0时按逆拓扑有序的顺序计 算。
v2 v7 2 1 9 6 v9 v5 v1 7 4 1 4 v8 v3 5 4 2 v4 v6 6 16 6 16 18 7 0 18 7 14 4 0 14 6 5 7 8 10
6 16 v2 v7 2 1 9 6 16 6 18 7 0 v9 v5 v1 7 4 1 18 7 14 4 0 4 v8 v3 14 6 5 4 5 7 2 v4 v6 8 10
6 16 v2 v7 2 1 9 6 16 6 18 7 0 v9 v5 v1 7 18 7 14 0 4 v8 14