图
图的定义和术语
![](http://img-my.csdn.net/uploads/201303/19/1363700148_8834.jpg)
图中数据元素通常称为顶点(Vertex),V是顶点的有穷非空集合
VR是两个顶点之间的关系的集合
- 若<v,w>属于VR,则<v,w>表示从v到w的一条弧(Arc),且称v为弧尾(Tail)或起始点(Inital node),称w为弧头(Head)或终端点(Terminal node),此图称为有向图(Digraph)
- 若<v,w>属于VR必有<w,v>属于VR,即VR是对称的,则以无序对(v,w)代替这两个有序对,表示v和w之间的一条边(Edge),此时图称为无向图(Undigraph)
我们用n表示图中顶点的数目,用e表示边或弧的数目,那么
- 对于无向图,e的取值范围是0到1/2*n*(n-1).有1/2*n*(n-1)条边的无向图称为完全图(completed graph)
- 对于有向图,e的取值范围是0到n*(n-1).具有n(n-1)条弧的有向图称为有向完全图。有很少条边或弧的图称为稀疏图(sparse graph),反之,称为稠密图(dense graph)
对于无向图,G=(V, {E}):
- 如果边(v,v')属于E,则称顶点v和v‘互为邻接点,即v和v'相邻接
- 边(v,v')依附于顶点v和v’,或者说(v,v')和顶点v和v‘相关联
- 顶点v的度(degree)是和v相关联的边的数目,记为TD(v)
对于有向图,G=(V,{A}):
- 如果弧<v,v'>属于A,则称顶点v邻接到顶点v‘,顶点v’邻接自顶点v。弧<v,v'>和顶点v,v'相关联
- 以顶点v为头的弧的数目称为v的入度(indegree),记为ID(v)
- 以v为尾的弧的数目称为v的出度(outdegree),记为OD(v).顶点v的度记为TD(v) = ID(v)+OD(v)
在无向图G中:
- 如果顶点v到顶点v‘有路径,则称v和v’是连通的
- 如果对于图中任意两个顶点vi,vj属于V,vi和vj都是连通的,则称G是连通图(Connected Graph)
- 连通分量(Connected Component),无向图中的极大连通子图
在有向图G中:
- 如果对于每一对vi,vj属于V,并且vi != vj,从vi到vj和从vj到vi都存在路径,则称G是强连通图
- 有向图的极大强连通子图称作有向图的强连通分量
图的存储结构
数组表示法
用两个数组分别存储数据元素(顶点)的信息和数据元素之间的关系(边或弧)的信息。
#define INFINITY INT_MAX // 最大值为无穷 #define MAX_VERTEX_NUM 20 // 最大顶点个数 typedef enum {DG, DN, AG, AN} GraphKind; // {有向图,有向网,无向图,无向网} typedef struct ArcCell { int adj; }ArcCell, AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; struct MGraph { AdjMatrix arcs; // 邻接矩阵 int vexnum, arcnum; // 图当前顶点数和弧数 GraphKind kind; // 图的种类 };
邻接表
邻接表是图的一种链式存储结构。在邻接表中,对图中每个顶点建立一个单链表,第i个单链表中的结点表示依附于顶点vi的边(对有向图是以顶点vi为尾的弧)。每个结点由三个域组成,其中
- 邻接点域(adjvex)指示与顶点vi邻接的点在图中的位置
- 链域(nextarc)指示下一条边或弧的结点
- 数据域(info)存储和边或弧相关的信息,如权值
每个链表上附设一个表头结点。在表头结点中,除了设有链域(firstarc)指向链表中的第一个结点外,还设有存储顶点vi的名和其它有关信息的数据域(data)
图示
![](http://img-my.csdn.net/uploads/201303/28/1364476446_8355.jpg)
数据结构
#define MAX_VERTEX_NUM 20 // 最大顶点个数 /*定义表结点*/ struct ArcNode { int adjvex; // 该弧所指向的顶点的位置 struct ArcNode *nextarc; // 指向下一条弧的指针 InfoType *info; // 该弧相关信息的指针 }; /*定义头结点*/ typedef struct VNode { VertexType data; // 顶点信息 struct ArcNode *firstarc; // 指向第一条依附该顶点的弧的指针 }VNode, AdjList[MAX_VERTEX_NUM]; struct ALGraph { AdjList vertices; int vexnum, arcnum; // 图的当前顶点数和弧数 int kind; // 图的种类标志 };
图的遍历
图的遍历分为广度优先遍历(breadth-first-search)和深度优先遍历(depth-first-search)
广度优先遍历
广度优先搜索是很多重要的图算法的原型。在prime最小生成树和Dijkstra单源最短路径算法中,都采用了与广度优先搜索类似的思想
在给定图G=(V,E)和一个特定的源顶点s的情况下,广度优先搜索系统地探索G的边,以期“发现”可从s到达的所有顶点,并计算s到所有这些可达顶点之间的距离(即最小的边数)。BFS同时还能生成一颗根为s,且包括所有s的可达顶点的广度优先树。对从s可达的任意顶点v,广度优先树中从s到v的路径对应于图G中从s到v的一条最短路径,即包含最少边数的路径。BFS之所以 称之为广度优先搜索,是因为,它自始至终都是从一个结点沿其广度方向向外扩展。BFS算法自始至终一直通过已找到和未找到顶点之间的边界向外扩展,就是说,算法首先搜索和s距离为k的所有顶点,然后再去搜索和s距离为k+1的其他顶点
为了记录搜索的轨迹,广度优先搜索将每个顶点都着色为白色,灰色或黑色。算法开始前,所有的顶点都是白色;随着搜索的进行,各顶点会逐渐变成灰色,然后成为黑色。在搜索中第一次碰到一个顶点时,称该顶点被发现了,此时该顶点变为非白色。因此,灰色和黑色顶点都是已被发现的,但是广度优先搜索还是对它们加以区分,以确保搜索是以一种广度优先的顺序进行的。若(u,v)属于E,且顶点u为黑色,那么顶点v要么是灰色,要么是黑色。也就是说,与黑色顶点相邻的所有顶点都是已被发现的,灰色顶点可能会有一些白色的相邻顶点,它们代表了已被发现与未被发现顶点之间的边界。
伪代码
BFS(G, s) for each vertex u (- V[G] - {s} do color[u] <- WHITE d[u] <- 00 parent[u] <- NULL color[s] <- GRAY d[s] <- 0 parent[s] <- NULL Q <- 空集 ENQUEUE(Q, s); while Q != 空集 do u <- DEQUEUE(Q) for each v (- Adj[u] do if color[v] == WHITE then color[v] <- GRAY d[v] <- d[u] + 1 parent[v] <- u ENQUEUE(Q, v); color[u] <- BLACK
参考示例
深度优先搜索(DFS)
深度优先搜索所遵循的搜索策略是尽可能“深”地搜索一个图,对于新发现的顶点,如果它还有以此为起点而未探测到的边,就沿此边继续探测下去。当顶点v的所有边都已被探寻过后,搜索将回溯到发现顶点v有起始点的那些边,这一过程一直进行到已发现从源点可达的所有顶点为止。如果还存在未被发现的顶点,则选择其中一个作为源点,并重复以上过程。整个过程反复进行,知道所有的顶点都被已发现为止
伪代码
图示过程,当各条边被算法探索到时,他们或者被加以阴影(如果它们是树边),或者被标以虚线。对于非树边,根据他们是否反向,交叉或前向边等情况,将它们标记为B/C/F。对于各个顶点,根据它们的发现/完成时间,给它们加上时间戳。
参考示例
最小生成树
假设要在n个城市之间建立通讯联络网,则连通n个城市只需要修建n-1条线路,如何在最省经费的前提下建立这个通讯网呢?答案就是最小生成树。
术语描述是:在e条带权的边中选取n-1条边(不构成回路),使权值之和最小。
如下图是一个无向连通图,图中显示各条边的权值,带阴影的边为最小生成树的边,树中各边的权值之和为37.最小生成树不是唯一的,用边(a,h)替代边(b,c)得到的是另外一颗最小生成树,各边权值之和也是37.
![](http://img-my.csdn.net/uploads/201304/12/1365761030_6966.png)
解决最小生成树问题有两种算法:kruskal算法和prim算法。在了解这两种算法之前,先得了解一下MST性质(最小生成树性质):
设G = (V, E)是一个连通网络,U是顶点集V的一个真子集。若(u,v)是G中一条“一个端点在U中(例如u属于U),另一个端点不在U中的边(例如v属于V-U),且(u,v)具有最小权值,则一定存在G的一颗最小生成树包括此边(u,v).”通俗的讲,就是最小权值的边必定在最小生成树上,前提是,边的一个顶点在生成树上,另一个顶点不在(这里可以参考贪心算法的实现,而且kruskal和prim算法都是利用了贪心的思想)
kruskal算法
考虑问题的出发点:为使生成树上的边的权值之和达到最小,则应使生成树中每一条边的权值尽可能的小。具体做法:先构造一个只含n个顶点的子图SG(sub-graph),然后从权值最小的边开始,若它的添加不使SG中产生回路,则在SG上加上这条边,如此重复,直至加上n-1条边为止。图示如下:
![](http://img-my.csdn.net/uploads/201304/12/1365769617_8679.png)
简单的说,kruskal算法过程就是,每次寻找最小权值的边,然后加入到最小生成树中,这个过程用到的操作,就是并查集,find-set(u)返回最小包含u的集合中的一个代表元素(最小权值边的顶点),通过测试find-set(u)是否等价于find-set(v)判断顶点u和v是否属于同一棵树。通过union-set(u,v)实现树与树的合并。
伪代码如下:
MST-KRUSKAL(G, w) A <- 空集 for each vertex v (- V[G] do MAKE-SET(v) sort the edges of E into nondecreasing order by weight w for each edge(u, v) (- E, taken in nondecreasing order by weight do if FIND-SET(u) != FIND-SET(v) then A <- A U {(u, v)} UNION-SET(u, v) return A
参考示例
prim算法
取图中任意一个顶点v作为生成树的根,之后往生成树上添加新的顶点u。在添加的顶点u和已经在生成树的顶点v之间必定存在一条边,并且改边的权值在所有连通顶点v和u之间的边中取值最小。之后继续往生成树上添加顶点,直至生成树上含有n-1个顶点为止。也就是,每次添加树中的边,都是树的权尽可能小的边。图示:
![](http://img-my.csdn.net/uploads/201304/12/1365770678_6012.png)
伪代码
MST-PRIME(G, w, r) for each u (- V[G] do key[u] <- 无穷 parent[u] <- null key[r] <- 0 Q <- V[G] while Q != 空集 do u <- EXTRACT-MIN(Q) for each v (- Adj[u] do if v(- Q and w(u, v) < key[v] then parent[v] <- u key[v] <- w(u, v)
参考链接
kruskal和prime对比
过程简单对比
kruskal算法:所有的顶点放那,每次从所有的边中找一条代价最小的
prim算法:在U,(V-U)之间的边,每次找一条代价最小的
效率对比
效率上:
- 稠密图 prim > kruskal
- 稀疏图 kruskal > prim
拓扑排序
拓扑排序(Toplogical Sort),简单地说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称为拓扑排序
若集合X上的关系R是自反的、反对称的和传递的,则称R是集合X上的偏序关系
设R是集合X上的偏序,如果对每个x,y∈X,必有xRy或yRx,则称R是集合X上的全序关系
图示:
![](http://img-my.csdn.net/uploads/201304/26/1366940593_5462.png)
用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的网(Activity On Vertex Network),简称AOV-网。在网中,若从顶点i到顶点j有一条有向路径,则i是j的直接前驱,j是i的直接后继
拓扑排序算法
- 在有向图中选一个没有前驱的顶点输出
- 从图中删除该顶点和所有以它为尾的弧
- 重复1、2步骤,直至全部顶点均已输出,或者当图中不存在无前驱的顶点为止(说明有向图中有环)
伪代码实现
采用邻接矩阵作为存储图的数据结构,同时加上了一个入度数组indegree数组查找入度为0节点的辅助数组
#include <stdio.h> #include <stdlib.h> #include <string.h> // 图最大节点总数 #define MAX 502 // 入度数组 int indegree[MAX]; // 图的邻接矩阵 int matrix[MAX][MAX]; /** * Description:邻接矩阵+入度数组实现拓扑排序 */ void topological_sort(int n) { int i, j, k; for (i = 0; i < n; i ++) { // 每次循环确定全序序列的一个元素 for (j = 1; j <= n; j ++) { if (indegree[j] == 0) { // 选取一个没有前驱的节点输出 indegree[j] --; if (i == n - 1) { printf("%d\n", j); }else { printf("%d ", j); } // 删除该节点和所有以它为尾的弧 for (k = 1; k <= n; k ++) { if (matrix[j][k]) { matrix[j][k] = 0; indegree[k] -= 1; } } // 进行下一个循环 break; } } } }