550 likes | 704 Views
第八章 图. 图的基本概念 图的存储结构 图的遍历 最小生成树 最短路径 拓扑排序 关键路径. 一、图的基本概念. 图定义 图是由顶点集合 (vertex) 及顶点间的关系 集合组成的一种数据结构 : Graph = ( V , E ) 其中 V = { x | x 某个数据对象 } 是顶点的有穷 非空集合; E = {( x , y ) | x , y V } 是顶点之间关系的有穷集合,也叫做边 (edge) 集 合。.
E N D
第八章 图 • 图的基本概念 • 图的存储结构 • 图的遍历 • 最小生成树 • 最短路径 • 拓扑排序 • 关键路径
一、图的基本概念 图定义 图是由顶点集合(vertex)及顶点间的关系 集合组成的一种数据结构: Graph=( V, E ) 其中 V = { x | x 某个数据对象}是顶点的有穷 非空集合; E = {(x, y) | x, y V } 是顶点之间关系的有穷集合,也叫做边(edge)集 合。
有向图与无向图 若图G中的每条边都是有方向的,则称G为有向图。有向边也称为弧。若图G中的每条边都是没有方向的,则称G为无向图。 • 完全图 对有n个顶点的图,若为无向图且边数为n(n-1)/2,则称其为无向完全图;若为有向图且边数为n(n-1) ,则称其为有向完全图。 • 邻接顶点 若(vi,vj)是一条无向边,则称顶点vi和vj互为邻接点,或称vi和vj相邻接,并称边(vi,vj)关联于顶点vi和vj,或称(vi,vj)与顶点vi和vj相关联。
顶点的度 一个顶点v的度是与它相关联的边的条数。 记作TD(v)。 • 顶点 v 的入度 是以 v 为终点的有向边的条数, 记作 ID(v); 顶点 v的出度是以 v 为始点的有向边的条数, 记作 OD(v)。 • 子图 设有两个图 G=(V, E) 和 G‘=(V’, E‘)。若 V’ V 且 E‘E, 则称 图G’ 是 图G 的子图。
路径 在图 G=(V, E) 中, 若存在一个顶点序列 • vp1, vp2, …, vpm,使得(vi, vp1)、(vp1, vp2)、 • ...、(vpm, vj)均属于E,则称顶点vi到vj存在一 • 条路径。若一条路径上除了vi和vj可以相同外, • 其余顶点均不相同,则称此路径为一条简单路径 • 。起点和终点相同的路径称为简单回路或简单环。
图的连通 在无向图G中,若两个顶点vi和vj之间有 • 路径存在,则称vi和vj是连通的。若G中任意两 • 个顶点都是连通的,则称G为连通图。非连通图的 • 极大连通子图叫做连通分量。 • 强连通图与强连通分量 在有向图中, 若对于每一 对顶点vi和vj, 都存在一条从vi到vj和从vj到vi的路径, 则称此图是强连通图。非强连通图的极大强连通 子图叫做强连通分量。 • 权 某些图的边具有与它相关的数, 称之为权。这种 带权图叫做网络。
7 2 5 B 10 9 60 40 1 12 80 8 A C 6 3 7 75 30 6 35 15 6 3 D E 4 7 45 16
生成树 一个连通图的生成树是它的极小 连通子图,在n个顶点的情形下,有n-1条 边。 • 不予讨论的图包含顶点到其自身的边;一条边在图中重复出现
二、图的存储结构 • 邻接矩阵 (Adjacency Matrix) • 在图的邻接矩阵表示中,有一个记录各个顶点信息的顶点表,还有一个表示各个顶点之间关系的邻接矩阵。 • 设图 A = (V, E)是一个有 n 个顶点的图,则图的邻接矩阵是一个二维数组 A .Edge[n][n],定义: • 无向图的邻接矩阵是对称的,有向图的邻接矩阵可能是不对称的。
在有向图中, 统计第 i行 1 的个数可得顶点 i的出度,统计第 j 列 1 的个数可得顶点 j的入度。 • 在无向图中, 统计第 i行 (列) 1 的个数可得顶点i的度。
邻接矩阵表示法中图的描述 #define n 6 /*图的顶点数*/ #define e 8 /*图的边数*/ typedef char vextype; /*顶点的数据类型*/ typedef float adjtype; /*权值类型*/ typedef struct { vextype vexs[n]; adjtype arcs[n][n]; } graph;
2 1 4 3 5
B A D C E
2 5 20 40 70 1 4 50 80 30 6 3
邻接矩阵表示法中无向网络的建立算法 CREATEGRAPH(graph *ga) { int i,j,k; float w; for (i=0;i<n;i++) ga->vexs[i]=getchar(); /*读入顶点信息,建立顶点表*/ for (i=0;i<n;i++) for (j=0;j<n;j++) ga->arcs[i][j]=0; /*邻接矩阵初始化*/ for (k=0;k<e;k++) { scanf(“%d%d%f”,&i,&j,&w); /*读入e条边上的权*/ ga->arcs[i][j]=w; ga->arcs[j][i]=w; } }
邻接表 (Adjacency List) • 无向图的邻接表 把同一个顶点发出的边链接在同一个边链表中,链表的每一个结点代表一条边,叫做边结点,结点中保存有与该边相关联的另一顶点的顶点下标 dest和指向同一链表中下一个边结点的指针 link。
有向图的邻接表和逆邻接表 • 在有向图的邻接表中,第 i个边链表链接的边都是顶点 i发出的边。也叫做出边表。 • 在有向图的逆邻接表中,第 i个边链表链接的边都是进入顶点i的边。也叫做入边表。
带权图的边结点中保存该边上的权值 cost。 • 顶点 i的边链表的表头指针adj在顶点表的下标为 i 的顶点记录中,该记录还保存了该顶点的其它信息。 • 在邻接表的边链表中,各个边结点的链入顺序任意,视边结点输入次序而定。 • 设图中有 n 个顶点,e 条边,则用邻接表表示无向图时,需要 n 个顶点结点,2e 个边结点;用邻接表表示有向图时,若不考虑逆邻接表,只需 n 个顶点结点,e 个边结点。
邻接表的形式说明和建立算法 typedef struct node /*边表结点定义*/ { int adjvex; struct node *next; } edgenode; typedef struct /*顶点表结点定义*/ { vextype vertex; edgenode *link; } vexnode; vexnode ga[n];
CREATADJLIST(vexnode ga[]) { int i,j,k; edgenode *s; for (i=0;i<n;i++) /*读入顶点信息并初始化*/ { ga[i].vertex=getchar(); ga[i].link=NULL; } for (k=0;k<e;k++) /*建立边表*/ { scanf(“%d%d”,&i,&j); s=malloc(sizeof(edgenode)); s->adjvex=j; s->next=ga[i].link ga[i].link=s; s=malloc(sizeof(edgenode)); s->adjvex=i; s->next=ga[j].link; ga[i].link=s; } }
三、图的遍历性 • 从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历 ( Graph Traversal )。 • 图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。 • 为了避免重复访问,可设置一个标志顶点是否被访问过的辅助数组 visited [ ],它的初始状态为 0,在图的遍历过程中,一旦某一个顶点 i被访问,就立即让 visited [i]为 1,防止它被多次访问。
深度优先搜索DFS ( Depth First Search ) 深度优先搜索的示例
DFS 在访问图中某一起始顶点 v 后,由 v 出发, 访问它的任一邻接顶点 w1;再从 w1 出发,访问与 w1邻接但还没有访问过的顶点 w2;然后再从 w2 出 发,进行类似的访问,… 如此进行下去,直至到达 所有的邻接顶点都被访问过的顶点 u 为止。接着, 退回一步,退到前一次刚访问过的顶点,看是否还 有其它没有被访问的邻接顶点。如果有,则访问此 顶点,之后再从此顶点出发,进行与前述类似的访 问;如果没有,就再退回一步进行搜索。重复上述 过程,直到连通图中所有顶点都被访问过为止。
深度优先搜索算法 int visited[n]; graph g; DFS(int i) /*图用邻接矩阵表示*/ { int j; printf(“node:%c\n”,g.vexs[i]); visited[i]=TRUE; for (j=0;j<n;j++) if ((g.arcs[i][j]==1)&&(!visited[j])) DFS(j); }
vexnode g1[n]; DFSL(int i) /*图用邻接表表示*/ { int j; edgenode *p; printf(“node:%c\n”,g1[i].vertex); visited[i]=TRUE; p=g1[i].link; while (p!=NULL) { if (!visited[p->adjvex]) DFSL(p->adjvex); p=p->next; } }
例子 遍历结果:A、C、B、D
算法分析 • 图中有 n 个顶点,e 条边。 • 如果用邻接表表示图,沿Firstout link 链可以找到某个顶点 v 的所有邻接顶点 w。由于总共有 2e 个边结点,所以扫描边的时间为O(e)。而且对所有顶点递归访问1次,所以遍历图的时间复杂性为O(n+e)。 • 如果用邻接矩阵表示图,则查找每一个顶点的所有的边,所需时间为O(n),则遍历图中所有的顶点所需的时间为O(n2)。
广度优先搜索BFS ( Breadth First Search) 广度优先搜索的示例 广度优先搜索过程 广度优先生成树
使用广度优先搜索在访问了起始顶点 v 之后,由 v 出发,依次访问 v 的各个未曾被访问过的邻接顶点 w1, w2, …, wt,然后再顺序访问 w1, w2, …, wt 的所有还未被访问过的邻接顶点。再从这些访问过的顶点出发,再访问它们的所有还未被访问过的邻接顶点,… 如此做下去,直到图中所有顶点都被访问到为止。 • 广度优先搜索是一种分层的搜索过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况。因此,广度优先搜索不是一个递归的过程,其算法也不是递归的。
为了实现逐层访问,算法中使用了一个队列,以记忆正在访问的这一层和上一层的顶点,以便于向下一层访问。为了实现逐层访问,算法中使用了一个队列,以记忆正在访问的这一层和上一层的顶点,以便于向下一层访问。 • 与深度优先搜索过程一样,为避免重复访问,需要一个辅助数组 visited [ ],给被访问过的顶点加标记。
广度优先搜索算法 BSF(int k) /*图用邻接矩阵表示*/ { int i,j; SETNULL(Q); printf(“%c\n”,g.vexs[k]); visited[k]=TRUE; ENQUEUE(Q,K); while (!EMPTY(Q)) { i=DEQUEUE(Q); for (j=0;j<n;j++) if ((g.arcs[i][j]==1)&&(!visited[j])) { printf(“%c\n”,g.vexs[j]); visited[j]=TRUE; ENQUEUE(Q,j); } } }
BFSL(int k) /*图用邻接表表示*/ { int i; edgenode *p; SETNULL(Q); printf(“%c\n”,g1[k].vertex); visited[k]=TRUE; ENQUEUE(Q,k); while (!EMPTY(Q)) { i=DEQUEUE(Q); p=g1[i].link; while (p!=NULL) { if (!visited[p->adjvex]) { printf(“%c\n”,g1[p->adjvex].vertex); visited[p->adjvex]=TRUE; ENQUEUE(Q,p->adjvex); } p=p->next; } } }
例子 遍历结果:A、 B 、 C 、D
算法分析 • 如果使用邻接表表示图,则循环的总时间代价为 d0 + d1 + … + dn-1 = O(e),其中的 di 是顶点 i 的度。 • 如果使用邻接矩阵,则对于每一个被访问过的顶点,循环要检测矩阵中的 n 个元素,总的时间代价为O(n2)。
连通分量 (Connected component) • 当无向图为非连通图时,从图中某一顶点出发,利用深度优先搜索算法或广度优先搜索算法不可能遍历到图中的所有顶点,只能访问到该顶点所在的最大连通子图(连通分量)的所有顶点。 • 若从无向图的每一个连通分量中的一个顶点出发进行遍历,可求得无向图的所有连通分量。 • 在算法中,需要对图的每一个顶点进行检测:若已被访问过,则该顶点一定是落在图中已求得的连通分量上;若还未被访问,则从该顶点出发遍历图,可求得图的另一个连通分量。
对于非连通的无向图,所有连通分量的生成树组成了非连通图的生成森林。对于非连通的无向图,所有连通分量的生成树组成了非连通图的生成森林。
非连通图的遍历 非连通图的遍历必须多次调用深度优先搜索或广度优先搜索算法,以DFS为例: TRAVER() /* 遍历用邻接矩阵表示的非连通图*/ { int i; for ( i = 0; i < n; i++) visited[i] = FALSE; /* 标志数组初始化 */ for ( i = 0; i < n; i++) { if ( !visited[i]) DFS(i); /* 从顶点出发遍历一个连 通分量 */ printf( “comp end\n”); } }
四、最小生成树 ( minimum cost spanning tree ) 连通图G的一个子图如果是一棵包含G的所有顶点的树,则该子图称为G的生成树。 生成树是连通图的极小连通子图。所谓极小是指:若在树中任意增加一条边,则将出现一个回路;若去掉一条边,将会使之变成非连通图。 生成树各边的权值总和称为生成树的权。权最小的生成树称为最小生成树
用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。 按照生成树的定义,n 个顶点的连通网络的生成树有 n 个顶点、n-1 条边。 构造最小生成树的准则:必须只使用该网络中的边来构造最小生成树;必须使用且仅使用 n-1 条边来联结网络中的 n 个顶点;不能使用产生回路的边。
最小生成树的重要性质: 设 G =(V,E)是一个连通网络,U 是顶点集 V 的一个真子集。若(u,v)是 G 中所有的一个顶点在 U,另一个顶点不在 U 的边中,具有最小权值的一条边,则一定存在 G 的一棵最小生成树包括此边。 U V—U u v
证明(反证法): 假设 G 中任何一棵最小生成树中都不包含(u,v)。设T是一棵最小生成树但不包含(u,v)。由于T是最小生成树,所以 T 是连通的,因此有一条从u到v的路径,且该路径上必有一条连接两个顶点集 U、V 的边(u,v),其中u∈U,v∈V-U。当把边(u,v)加入到 T 中后,得到一个含有边(u,v)的回路。删除边(u,v),上述回路即被消除。由此得到另一棵生成树 T, T 和 T的区别仅在于用边(u,v)代替了(u,v)。由于(u,v)的权<=(u,v)的全权,所以, T的权<=T的权,与假设矛盾。
V—U U u v u v
普里姆(Prim)算法 普里姆算法的基本思想: 从连通网络 N = { V, E }中的某一顶点 u0出 发,选择与它关联的具有最小权值的边(u0, v), 将其顶点加入到生成树的顶点集合U中。以后每 一步从一个顶点在U中,而另一个顶点不在U中 的各条边中选择权值最小的边(u, v),把它的顶点 加入到集合U中。如此继续下去,直到网络中的 所有顶点都加入到生成树顶点集合U中为止。
1 1 5 6 1 2 4 1 2 4 5 7 5 3 3 3 2 3 2 5 4 4 5 6 5 6 6 用普里姆算法构造最小生成树的过程
普里姆算法构造的基本思想 为直观解释方便,设想在构造过程中,T的 顶点集U和边集均被涂成红色,U之外的顶点涂 成蓝色,连接红点和蓝点的边被涂成紫色。因 此,最短紫边就是连接U和V-U的最短边。 设当前生成的T有k个顶点,则当前紫边数目 是k(n-k),紫边集过大。为了构造一个较小的 侯选紫边集,可以这样处理:对每一个蓝点, 从该蓝点到红点的紫边中,必有一条是最短的 ,我们只要将所有n-k个蓝点所关联的最短紫边 作为侯选集,就必定能保证所有紫边中最短的 紫边属于该侯选集。
侯选集的调整方法: 当最短紫边(u,v)被涂成红色被加入T中后,v由蓝点变为红点,对每一个剩余的蓝点j,边(v,j)就由非紫边变成了紫边,这就使得我们必须对侯选集做如下调整:若侯选集中蓝点j所关联的原最短紫边长度大于新紫边(v,j)的长度,则以(v,j)作为j所关联的新的最短紫边来代替j的原最短紫边,否则j的原最短紫边不变。
Prim 算法的结构如下: (1) 置T为任意一个顶点,置初始侯选紫边集; (2) while ( T中顶点数目<n) (3) { 从侯选紫边集中选取最短紫边(u,v); (4) 将 (u,v) 及蓝点 v 涂成红色,扩充到 T 中; (5) 调整侯选紫边集; (6) }