参考教程: http://www.cdpc.edu.cn/jpkc/shujujiegou1/jiaoxuetiaojian/yangbenjiaoan/7/1.file/1.htm
1.图是由顶点集合(vertex)及顶点间的关系集合组成的一种数据结构: Graph=( V, E )
其中 V = { x | x ∈ 某个数据对象}是顶点的有穷非空集合(简称点集);
E = {(x, y) | x, y ∈ V }
或 E = {<x, y> | x, y ∈ V && Path (x, y)}
是顶点之间关系的有穷集合,也叫做边(edge)集合(简称边集)。Path (x, y)表示从 x 到 y 的一条单向通路, 它是有方向的。
2.图的基本术语
(1)有向图与无向图
在有向图中,顶点对<x, y>是有序的.
有向边称为弧,用<x, y>表示从顶点x到顶点y的一条弧,并称x为弧尾(起始点)称y为弧头(终点)。<x, y>和<y, x>是不相同的两条弧。
在无向图中,顶点对(x, y)是无序的.
(2)完全图
若有 n 个顶点的无向图有 n(n-1)/2 条边(即任意两点之间都有一条无向边), 则此图为无向完全图。
若有 n 个顶点的有向图有n(n-1) 条边(即任意两点之间都有一对有向边), 则此图为有向完全图。
(3)邻接顶点
如果 (u, v) 是 E(G) 中的一条边,则称 u 与 v 互为邻接顶点。
(4)权
某些图的边具有与它相关的数, 称之为权。这种带权图叫做网络。
(5)子图
设有两个图 G=(V, E) 和 G’=(V’, E’)。若 V’包含于 V 且 E’ 包含于E, 则称 图G’ 是 图G 的子图。
(6)顶点的度
一个顶点v的度是与它相关联的边的条数。记作TD(v)。在有向图中, 顶点的度等于该顶点的入度与出度之和。
(7)顶点 v 的入度
以 v 为终点的有向边的条数, 记作 ID(v);顶点 v 的出度是以 v 为始点的有向边的条数, 记作 OD(v)。
(8)路径
在图 G=(V, E) 中, 若从顶点 vi 出发, 沿一些边经过一些顶点 vp1, vp2, …, vpm,到达顶点vj。则称顶点序列 ( vi vp1 vp2 ... vpm vj ) 为从顶点vi 到顶点 vj 的路径。它经过的边(vi, vp1)、(vp1, vp2)、...、(vpm, vj)应是属于E的边。
(9)路径长度
非带权图的路径长度是指此路径上边的条数。
带权图的路径长度是指路径上各边的权之和。
(10)简单路径
若路径上各顶点 v1,v2,...,vm 均不互相重复, 则称这样的路径为简单路径。
(11)回路
若路径上第一个顶点 v1 与最后一个顶点vm 重合, 则称这样的路径为回路或环。
(12)连通图与连通分量
在无向图中, 若从顶点v1到顶点v2有路径, 则称顶点v1与v2是连通的。如果图中任意一对顶点都是连通的, 则称此图是连通图。最少要有n-1条边。
非连通图的极大连通子图叫做连通分量。
(13)强连通图与强连通分量
在有向图中, 若对于每一对顶点vi和vj, 都存在一条从vi到vj和从vj到vi的路径, 则称此图是强连通图。最少要有n条边。
非强连通图的极大强连通子图叫做强连通分量。
(14)生成树
一个连通图的生成树是它的极小连通子图,即在n个顶点的情形下,有n-1条边。但有向图则可能得到它的由若干有向树组成的生成森林。
三、图的抽象数据类型(ADT)
ADT Graph
{ 数据对象V:V是具有相同特性的数据元素的集合,称为顶点集;
数据关系R:R={ VR }
VR={ <v,w> | v,w∈V且P(v,w), <v,w>表示从v到w的弧,
谓词P(v,w)定义了弧<v,w>的意义或信息 =
基本操作P:
GreatGraph(&G,V,VR):建立一个满足V,VR的图G ;
GetVex(G,u):若G中存在顶点u,则返回该顶点的信息,否则返回错误;
InsertVex(&G,v):在图G中添加顶点v ;
DFSTraverse(G,v,Visit()):从顶点v起深度优先遍历图G,并对每个顶点调用函数Visit一次且仅一次。一旦Visit()失败,则操作失败。
BFSTraverse(G,v,Visit()):从顶点v起广度优先遍历图G,并对每个顶点调用函数Visit一次且仅一次。一旦Visit()失败,则操作失败。
··· ··· }
邻接表存储图
邻接表是一种顺序存储与链接存储相结合的存储方法。将图的每个顶点Vi的邻接点连成一个单链表,即vi点边表。
用一个指针类型的数组存储顶点信息和边表的头指针,即顶点表。在邻接表中存在两种结点结构:顶点表结点和边表结点。
邻接矩阵存储图
图的邻接矩阵存储也称数组表示法,其方法是用一个一维数组存储图中的顶点的信息,用一个二维数组存储图中边的信息(即各点之间的邻接关系),存储顶点之间邻接关系的二维数组也称为邻接矩阵。
最小生成树( minimum cost spanning tree )
【算法分析】在构造过程中,还设置了两个辅助数组:
(1)lowcost[ ] 存放生成树顶点集合内顶点到生成树外各顶点的各边上的当前最小权值;
//(2)nearvex[ ] 记录生成树顶点集合外各顶点距离集合内哪个顶点最近(即权值最小)。
5.构造最小生成树算法--克鲁斯卡尔 (Kruskal) 算法
【基本思想】
设有一个有 n 个顶点的连通网络 G= { V, E },最初先构造一个只有 n个顶点,没有边的非连通图 T = { V, φ }, 图中每个顶点自成一个连通分量。当在 E 中选到一条具有最小权值的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到 T 中;否则将此边舍去,重新选择一条权值最小的边。如此重复下去,直到所有顶点在同一个连通分量上为止。
1.生成树
【定义】一个连通图的生成树是它极小连通子图,即在n个顶点的情形下,有n-1条边。
2.网络的最小生成树
【定义】如果连通图是一个网络,则称网络中所有生成树中权值总和最小的生成树为最小生成树(也称最小代价生成树)。
3.构造网络最小生成树的准则
【准则】
(1)必须只使用该网络中的边来构造最小生成树;
(2)必须使用且仅使用 n-1 条边来联结网络中的 n 个顶点;
(3)不能使用产生回路的边。
4.构造最小生成树算法--普里姆(Prim)算法
【算法的基本思想】
设图(或网络)的顶点集为V,下面将V分为两个集合U和V-U,这样该两个集合肯定没有交集,且满足U+(V-U)=V(即整个集合)
。下面是生成过程:(1)从连通网络 G = { V, E }中的某一顶点 u0 出发,选择与它关联的具有最小权值的边(u0, v),将其顶点加入到生成树的顶点集合U中。
(2)以后每一步从一个顶点在U中,而另一个顶点不在U中的各条边中选择权值最小的边(u, v),把它的顶点加入到集合U中。如此继续下去,直到网络中的所有顶点都加入到生成树顶点集合U中为止。
图的最短路径(Shortest Path)
关键路径(Critical Path)
1.AOE网络(Activity On Edge Network)
【定义说明】
(1)如果在无有向环的带权有向图中
用有向边表示一个工程中的活动(Activity)
用边上权值表示活动持续时间(Duration)
用顶点表示事件 (Event)
则这样的有向图叫做用边表示活动的网络,简称 AOE (Activity On Edges) 网络。
(2)AOE网络在某些工程估算方面非常有用。例如,可以使人们了解:
(1) 完成整个工程至少需要多少时间(假设网络中没有环)?
(2) 为缩短完成工程所需的时间, 应当加快哪些活动?
(3)在AOE网络中, 有些活动顺序进行,有些活动并行进行。
(4)从源点到各个顶点,以至从源点到汇点的有向路径可能不止一条。这些路径的长度也可能不同。完成不同路径的活动所需的时间虽然不同,但只有各条路径上所有活动都完成了,整个工程才算完成。
(5)因此,完成整个工程所需的时间取决于从源点到汇点的最长路径长度,即在这条路径上所有活动的持续时间之和。这条路径长度最长的路径就叫做关键路径(Critical Path)
(6)要找出关键路径,必须找出关键活动,不按期完成就会影响整个工程完成的活动。
(7)关键路径上的所有活动都是关键活动。因此,只要找到了关键活动,就可以找到关键路径。
2.定义几个与计算关键活动有关的量:
(1)事件Vi 的最早可能开始时间Ve[i]是从源点V0 到顶点Vi 的最长路径长度。
(2)事件Vi 的最迟允许开始时间Vl[i]是在保证汇点Vn-1 在Ve[n-1] 时刻完成的前提下,事件Vi 的允许的最迟开始时间。
(3)活动ak 的最早可能开始时间 e[k]
设活动ak 在边< Vi , Vj >上,则e[k]是从源点V0到顶点Vi 的最长路径长度。因此, e[k] = Ve[i]。
(4)活动ak 的最迟允许开始时间 l[k]
l[k]是在不会引起时间延误的前提下,该活动允许的最迟开始时间。
l[k] = Vl[j] - dur(<i, j>)。 其中,dur(<i, j>)是完成 ak 所需的时间。
(5)时间余量 l[k] - e[k]表示活动 ak 的最早可能开始时间和最迟允许开始时间的时间余量。l[k] == e[k] 表示活动 ak 是没有时间余量的关键活动。
【说明】
(1)为找出关键活动, 需要求各个活动的 e[k] 与 l[k],以判别是否 l[k] == e[k].
(2)为求得 e[k]与 l[k],需要先求得从源点 V0 到各个顶点 Vi 的 Ve[i] 和 Vl[i]。
(3)求Ve[i]的递推公式
从 Ve[0] = 0开始,向前递推 < Vj, Vi >∈S2, i = 1, 2,… , n-1
其中, S2是所有指向顶点Vi 的有向边< Vj , Vi >的集合。
从Vl[n-1] = Ve[n-1]开始,反向递推
< Vi, Vj >∈ S1, i = n-2, n-3, …, 0
其中, S1是所有从顶点Vi 发出的有向边< Vi , Vj >的集合。
这两个递推公式的计算必须分别在拓扑有序及逆拓扑有序的前提下进行。
(4)设活动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。这样就得到计算关键路径的算法。
(5)计算关键路径时,可以一边进行拓扑排序一边计算各顶点的Ve[i]。为了简化算法,假定在求关键路径之前已经对各顶点实现了拓扑排序,并按拓扑有序的顺序对各顶点重新进行了编号。算法在求Ve[i], i=0, 1, …, n-1时按拓扑有序的顺序计算,
在求Vl[i], i=n-1, n-2, …, 0时按逆拓扑有序的顺序计算。
1.从源点到其它点的最短路径(Shortest Path)
【定义说明】
如果从某网络中的某一顶点(称为源点:Source)到达另一顶点(称为终点:Destina
_tion)的路径可能不止一条,那么一定有一条其边上权值总和最小的路径,则将从源点到终点之间经过的路径称为最短路径。
关键的问题是如何找到一条路径使得沿此路径上各边上的权值总和达到最小。
【问题的提法】
给定一个带权有向图D与源点v,求从v到D中其它顶点的最短路径。限定各边上的权值大于或等于0。
为求得这些最短路径,迪杰斯特拉(Dijkstra)提出按路径长度的递增次序,逐步产生最短路径的算法。首先求出长度最短的一条最短路径,再参照它求出长度次短的一条最短路径,依次类推,直到从顶点v到其它各顶点的最短路径全部求出为止。
Dijkstra逐步求解的过程
【说明】
(1)引入一个辅助数组dist。它的每一个分量dist[i]表示当前找到的从源点v0到终点vi 的最短路径的长度。初始状态:
(i)若从源点v0到顶点vi有边,则dist[i]为该边上的权值;
(ii)若从源点v0到顶点vi 没有边,则dist[i]为+∞ 。
(2)一般情况下,假设 S 是已求得的最短路径的终点的集合,则可证明:下一条最短路径必然是从v0 出发,中间只经过S中的顶点便可到达的那些顶点vx (vxV-S )的路径中的一条。
(3)每次求得一条最短路径之后,其终点vk 加入集合S,然后对所有的vi ∈V-S,修改其dist[i]值。
【Dijkstra算法描述】
(1)初始化: S ← { v0 };
dist[j] ← Edge[0][j], j = 1, 2, …, n-1; /* n为图中顶点个数 */
(2)求出最短路径的长度:
dist[k] ← min{ dist[i] }, i ∈ V- S ; S ← S U { k };
(3)修改:
dist[i] ← min{ dist[i], dist[k] + Edge[k][i] }, 对于每一个 i∈ V- S ;
(4)判断: 若S = V, 则算法结束,否则转(2)。
【程序实现】
void Dijkstra(Mgraph Gn,int v0,int path[],int dist[])
{
int s[VEX_NUM];
for(v=0;v<VEX_NUM;v++)
{
s[v]=0;dist[v]=Gn.arcs[v0][v];
if(dist[v]<MAXSIZE) path[v]=v0;
else path[v]=-1;
}
dist[v0]=0;s[v0]=1;
for(i=1;i<VEX_NUM-1;i++)
{
min=MAXSIZE ;
for(w=1;w<VEX_NUM ;w++)
if(!s[w]&&dist[w]<min)
{
v=w ;min=dist[w] ;
}
s[v]=1 ;
for(j=1;j<VEX_NUM ;j++)
if(!s[j]&&(min+Gn.arcs[v][j]<dist[j]))
{
dist[j]=min+Gn.arcs[k][j] ;
path[j]=v ;
}
}
} /* Dijkstra */
void putpath(int v0,int p[],int d[])
{ for(i=0 ;i<VEX_NUM ;i++)
if(d[i]<MAXSIZE && i!=v0)
{ printf(“v%d< --”,i);
next=p[i];
while(next!=v0)
{ printf(“v%d< --”,next);
next=p[next]; }
printf(“v%d:%d/n”,v0,d[i]);