图
图的定义和术语
一、图的定义和术语
-
图(Graph)——图G是由两个集合V(G)和E(G)组成的。记为G=(V,E)
-
其中:V(G)是顶点的非空有限集
-
E(G)是边的有限集合,边是顶点的无序对或有序对
-
权——与图的边或弧相关的树叫权
-
网)带权的图叫做网
-
子图——如果图G(V,E)和图G1(v1,E1),满足:
-
弧或边带权的图分别称为有向网或无向网
-
对无向图来讲,假若顶点v和顶点w之间存在一条边,则称顶点v和w互为邻接点,边(v,w)和顶点v和w相关联
-
顶点的度
在无向图中,顶点的度为与每个顶点相连的边数
有向图中,顶点的度分为入度和出度
入度:以该顶点为头的弧的数目
出度:以该顶点为尾的弧的数目
-
路径——路径是顶点的序列,顶点V到顶点V’的路径{V=Vi,0,Vi,1,……Vi,n=V’},
-
其中<Vi,j-1,Vi,j>属于E,(1<j<=n)
-
路径长度—— 路径上的边或弧的数目
-
回路——第一个顶点和最后一个顶点相同的路径
-
简单路径——序列中的顶点不重复出现的路径
-
简单回路——除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路
-
连通——从顶点V到顶点W有一条路径,则说V和W是连通的
-
连通图——图中任意两个顶点都是连通的
-
连通分量——非连通图的每一个连通部分
-
强连通图——有向图中,如果对每一对Vi,Vj属于V,Vi不等于Vj.从Vi到Vj和从Vj到Vi都存在路径,则称G是强连通图
-
强连通分量——有向图中的极大连通子图
例:
-
假设设一个连通图有n 个顶点和e 条边,其中 n-1 条边和n 个顶点构成一个极小连通子图, 称该极小连通子图为此连通图的生成树
-
对非连通图,则称由各个连通分量的生成树 的集合为此非连通图的生成森林
有向图
- 有向图——有向图G是由两个集合V(G)和E(G)组成的
- 其中:V(G)是顶点的非空有限集
- E(G)是有向边(也称弧)的有限集合,弧是顶点的有序对,记为<v,w>,vw是顶点,v是弧尾w是弧头
由于"弧“是有方向的,因此称有顶点集和弧集构成的图称为有向图
- 对有向图来说
顶点的出度:以顶点v为弧尾的弧的数目
顶点的入度:以顶点v为弧头的弧的数目
例如:
无向图
- 无向图——无向图G是由两个集合V(G)和E(G)组成的
- 其中:V(G)是顶点的非空有限集
- E(G)是边的有限集合,边是顶点的无序对,记为(v,w)或(w,v),并且(v,w)=(w,v)
若<v, w>属于E(G)必有<w, v>属于E(G), 则称(v,w) 为顶点v 和顶点w 之间存在一条边
由顶点集和边集构成的图称作无向图
有向完全图和无向完全图
假设图中有n个顶点,e条边:
- 有向完全图——含e=n(n-1)条弧的有向图
- 无向完全图——含e=n(n-1)/2条边的无向完全图
稀疏图和稠密图
假设图中有n个顶点,e条边:
若边或弧的个数e<nlogn,则称作稀疏图,否则为稠密图
二、基本操作
CreateGraph(&G,V,VR); //按定义(V,VR)构造图
DestroyGraph(&G); //销毁图
LocateVex(G,u); //若G中存在顶点u 则返回该顶点在图中位置,否则返回其他信息
GetVex(G,V); 返回V的值
PutVex(&G,V,value); //对v赋值value
// 在一个图中,顶点是没有先后次序的,
// 但当采用某一种确定的存储方式存储后,
// 存储结构中的顶点存储次序构成顶点之间的相对次序
FirstAdjVex(G,v); //返回v的第一个邻接点 若该顶点
//在G中没有邻接点 则返回空
NextAdjVex(G,v,w);
//返回v的(相对于w的)下一个邻接点
// 若w是v的最后一个邻接点 则返回空
InsertVex(&G,v); //在图G中增加新顶点v
DeleteVex(&G,v); //删除G中顶点v及其相关的弧
InsertArc(&G,v,w); //在G中增添<v,w> 若G是无向的则删除对称弧<w,v>
DFSTraverse(G,v,Visit()); //从顶点v起深度优先遍历图G 并对每个顶点调用函数Visit一次且仅一次
BFSTraverse(G,v,Visit()); //从顶点v起广度优先遍历图G 并对每个顶点调用函数Visit一次且仅一次
图的存储结构
一、图的数组(邻接矩阵)存储表示
定义:矩阵的元素为
有向图的邻接矩阵为非对称矩阵
特点:
- 无向图的邻接矩阵对称,可压缩存储;有n个顶点 的无向图需存储空间为n(n+1)/2
- 有向图邻接矩阵不一定对称;有n个顶点的有向图 需存储空间为n²
- 无向图中顶点Vi的度TD(Vi)是邻接矩阵A中第i行“1” 的个数之和
- 有向图中,顶点Vi的出度是A中第i行“1”的个数之 和
- 顶点Vi的入度是A中第i列“1”的个数之和
网的邻接矩阵定义:
邻接矩阵表示法的优缺点:
优点:容易实现图的操作,如:求某顶点的度、判 断顶点之间是否有边、找顶点的邻接点等等
缺点::n个顶点需要 n*n 个单元存储边;空间效率为 O(n2)。对稀疏图而言尤其浪费空间。
邻接矩阵实现:
typedef struct AreCell { //弧的定义
VRType adj; //VRType是顶点关系类型
//对无权图 用1或0表示是否相邻
//对有权图 则为权值类型
InfoType *info; // 该弧相关信息的指针
}ArcCell;
AdjMatrix[MAX_VERTEX_NUM] [MAX_VERTEX_NUM];
typedef struct { //图的定义
VertexType vexs[MAX_VERTEX_NUM]; // 顶点信息
AdjMatrix arcs; //邻接矩阵
int vexnum,arcnum; //顶点数,弧数
GraphKind kind; //图的种类标志
}MGraph;
//邻接矩阵的存储表示
//用两个数组分别存储顶点表和邻接矩阵
#define MaxInt 32767 //表示极大值 即∞
#define MAX_VERTEX_NUM100 //最大顶点数
typedef char VerTexType; //假设顶点的数据类型为字符型
typedef int VRType; (最简单的) //假设边的权值类型为整型
typedef struct{
VerTexType vexs[MVNum]; //顶点表
AdjMatrix arcs; //邻接矩阵
//或ArcCell arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
//VRType arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
int vexnum,arcnum; //图的当前顶点数和边数
}AMGraph;
采用邻接矩阵表示法创建无向网:
算法思想:
- 输入总顶点数和总边数
- 依次输入点的信息存入顶点表中
- 初始化邻接矩阵,使每个权值初始化为极大值
- 构造邻接矩阵
算法描述:
Status CreateUDN(AMGraph &G) {
//采用邻接矩阵表示法,创建无向网G
cin>>G.vexnum>>G.arcnum; //输入总顶点数 总边数
for(i = 0;i<G.vexnum; ++i)
cin>>G.vexs[i]; //依次输入顶点的信息
for(i = 0;i<G.vexnum; ++i) //初始化邻接矩阵 边的权值均置为极大值
for(j = 0; j<G.vexnum; ++j)
G.arcs[i][j] = MaxInt;
for(k = 0; k<G.arcnum; ++k) { //构造邻接矩阵
cin>>v1>>v2>>w; //输入一条边依附的顶点和权值
i = LocateVex(G,v1);
j = LocateVex(G,v2); //确定v1和v2在G中的位置
G.arcs[i][j].adj = w; //边<v1,V2>的权值置为w
G.arcs[j][i].adj = G.arcs[i][j].adj; //置<v1, v2>的对称边<v2, v1>的权值为w
}
ewruen OK;
}//CreateUDN
int LocateVex(MGraph G,VertexType u) {
//存在则返回u在顶点表中的下标;否则返回-1
int i;
for(i=0;i<G.vexnum;++i)
if(u==G.vexs[i])
return i;
return -1;
}
二、图的邻接表存储表示
有向图的邻接表:
有向图的逆邻接表:
在有向图的逆邻接 表中,对每个顶点,链接的是指向该顶点的弧。
特点:
- 表结点与边数的关系
- 无向图中顶点Vi的度为第i个单链表中的结点数
- 有向图中:
- 顶点Vi的出度为第i个单链表中的结点个数
- 顶点Vi的入度为整个单链表中邻接点域值是i 的结点个数
邻接表的实现:
typedef struct ArcNode {
int adjvex; //该弧所指向的顶点的位置
struct ArcNode *nextarc; // 指向下一条弧的指针
InfoType *info; // 该弧相关信息的指针
}ArcNode;
typedef structVNode {
VertexType data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧
}VNode, AdjList[MAX_VERTEX_NUM];
typedef struct {
AdjList vertices;
int vexnum, arcnum;
intkind; // 图的种类标志
}ALGraph;
采用邻接表表示法创建无向网:
算法思想:
- 输入总顶点数和总边数
- 依次输入点的信息存入顶点表中,使每个表头结 点的指针域初始化为NULL
- 创建邻接表
算法描述:
Status CreateUDG(ALGraph &G) {
//采用邻接表表示法 创建无向图G
cin>>G.vexnum>>G.arcnum; //输入总顶点数 总边数
for(i = 0;i<G.vexnum; ++i) { //输入各点 构造表头结点
cin>>G.vertices[i].data; //输入顶点值
G.vertices[i].firstarc = NULL; //初始化表头结点的指针域为NULL
}
for(k = 0;k<G.arcnum;++k) { //输入各边 构造邻接表
cin>>v1>>v2; //输入一条边依附的两个顶点
i = LocateVex(G,v1);
j = LocateVex(G, v2);
p1=new ArcNode; //生成一个新的边结点*p1
p1->adjvex=j; //邻接点序号为j
p1->nextarc= G.vertices[i].firstarc; G.vertices[i].firstarc=p1;
//将新结点*p1插入顶点vi的边表头部
p2=new ArcNode; //生成另一个对称的新的边结点*p2
p2->adjvex=i; //邻接点序号为i
p2->nextarc= G.vertices[j].firstarc; G.vertices[j].firstarc=p2;
// /将新结点*p2插入顶点vj的边表头部
}
return OK;
}
邻接表表示法的优缺点:
优点:空间效率高,容易寻找顶点的邻接点
缺点:判断两顶点间是否有边或弧,需要搜索两结点对应的单链表,没有邻接矩阵方便
邻接矩阵与邻接表表示法的关系:
- 联系:邻接表中每个链表对应于邻接矩阵中的一行, 链表中结点个数等于一行中非零元素的个数。
- 区别:
对于任一确定的无向图,邻接矩阵是唯一的(行列 号与顶点编号一致),但邻接表不唯一(链接次序 与顶点编号无关)。
邻接矩阵的空间复杂度为O(n2),而邻接表的空间复 杂度为O(n+e)。 - 用途:邻接矩阵多用于稠密图;而邻接表多用于稀 疏图
三、有向图的十字链表存储表示
typedef struct ArcBox { //弧的结构表示
int tailvex.headvex;
InfoType *info;
struct ArcBox *hlink,*tlink;
}ArcBox;
typedef struct VexNode { //顶点的结构表示
VeertexType data;
ArcBox *firstin,*firstout;
}VexNode;
有向图的结构表示(十字链表)
typedef struct {
VexNode xlist[MAX_VERTEX_NUM];
// 顶点结点(表头向量)
int vexnum, arcnum; //有向图的当前顶点数和弧数
}OLGraph;
四、无向图的邻接多重表存储表示
边的结构表示:
typedef struct EBox {
VisitIf mark; //访问标记
int ivex,jvex; //该边依附的两个顶点的位置
struct EBox *ilink,*jlinkl; //分别指向依附这两个顶点的下一条边
InfoType *info; // 该边信息指针
}
顶点的结构表示:
typedef struct VexBox {
VertexType data;
EBox *firstedge; // 指向第一条依附该顶点的边
}VexBox;
无向图的结构表示:
typedef struct { // 邻接多重表
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum, edgenum;
}AMLGraph;
图的遍历
从图中某个顶点出发游历图,访遍 图中其余顶点,并且使图中的每个顶点 仅被访问一次的过程。
一、深度优先搜索
连通图的深度优先搜索遍历
从图中某个顶点V0 出发,访问此顶 点,然后依次从V0的各个未被访问的邻接 点出发深度优先搜索遍历图,直至图中所 有和V0有路径相通的顶点都被访问到
从深度优先搜索遍历连通图的过 程类似于树的先根遍历
如何判别V的邻接点是否被访问:
解决的办法是:为每个顶点设立一 个“访问标志visited[w]”
void DFS(Graph G,int v) {
//从第v个顶点出发,深度优先搜索遍历连通图G
visited[v] = TRUE;
VisitFunc(v);
for(w = FirstAdjVex(G,v);w>=0; w=NextAdjVex(G,v,w))
if (!visited[w]) DFS(G, w); // 对v的尚未访问的邻接顶点w // 递归调用DFS
}
非连通图的深度优先搜索遍历
首先将图中每个顶点的访问标志设 为FALSE, 之后搜索图中每个顶点,如 果未被访问,则以该顶点为起始点,进 行深度优先搜索遍历,否则继续检查下 一顶点。
bool visited[MAX];// 访问标志数组
Status (*VisitFunc)(int v);
//函数变量,是 一个静态分配的指针,VisitFunc指向一 个函数,
//这个函数原型为Status fun(int v),即返回类型为Status,
//形参类型为 int,VisitFunc是定义的指向具有这种函数 原型的指针。
void DFSTraverse(Graph G, Status(*Visit)(int v)){
// 对图G 作深度优先遍历
VisitFunc = Visit; //使用全局变量VisitFunc,使DFS不必设函数指针参数
for(v=0; v<G.vexnum; ++v)
visited[v] = FALSE; // 访问标志数组初始化
for(v=0; v<G.vexnum; ++v)
if(!visited[v])
DFS(G, v); // 对尚未访问的顶点调用DFS
}
时间复杂度分析:
遍历图的过程实质上是对每个顶点查找其邻 接点的过程。其耗费的时间取决于所采用的 存储结构。当用邻接矩阵做存储结构时,查 找每个顶点的邻接点所需时间为O(n^2),当 以邻接表做存储结构时,找邻接点所需时间 为O(e)。因此,当以邻接表作存储结构时,深 度优先搜索遍历图的时间复杂度为O(n+e)
注意:
- 每次调用前一定要判断起点的visited标志
- 这个算法还可以求图G的连通性、连通分量 的个数以及每个连通分量的顶点个数(注 意是无向图)
二、广度优先搜索
从图中的某个顶点V0出发,并在访问此 顶点之后依次访问V0的所有未被访问过的 邻接点,之后按这些顶点被访问的先后次 序依次访问它们的邻接点,直至图中所有 和V0有路径相通的顶点都被访问到。 若此时图中尚有顶点未被访问,则另选 图中一个未曾被访问的顶点作起始点,重 复上述过程,直至图中所有顶点都被访问 到为止。
void BFSTraverse(Graph G,Status (*Visit)(int v)) {
for(v=0; v<G.vexnum; ++v)
visited[v] = FALSE; //初始化访问标志
InitQueue(Q);// 置空的辅助队列Q
for( v=0; v<G.vexnum; ++v )
if (!visited[v]){// v 尚未访问
visited[v] = TRUE; Visit(v); // 访问v
EnQueue(Q, v);// v入队列
while(!QueueEmpty(Q)) {
DeQueue(Q, u); // 队头元素出队并置为u
for(w=FirstAdjVex(G, u); w>=0; w=NextAdjVex(G,u,w))
if ( ! visited[w]){
visited[w]=TRUE; Visit(w);
EnQueue(Q, w);// 访问的顶点w入队列
} // if
} // while
}
}
时间复杂度分析:
遍历图的过程实质上是通过边或弧找邻接点 的过程,因此广度搜索遍历图的时间复杂度 和深度优先搜索遍历相同,两者不同之处仅 仅在于对顶点的访问顺序不同。
三、遍历应用举例
求一条从顶点i 到顶点s 的简单路径
结论:
- 从顶点i 到顶点s ,若存在路径,则从 顶点i 出发进行深度优先搜索,必能搜索 到顶点s
- 遍历过程中搜索到的顶点不一定是路 径上的顶点
- 由它出发进行的深度优先遍历已经完 成的顶点不是路径上的顶点需要被删除
voidDFSearch( int v, int s, char*PATH) {
// 从第v个顶点出发递归地深度优先遍历图G,
// 求得一条从v到s的简单路径,并记录在PATH中
visited[v] = TRUE; // 访问第v 个顶点
Append(PATH, getVertex(v));// 第v个顶点加入路径
for(w=FirstAdjVex(v); w>=0&&!found; w=NextAdjVex(v) )
if(w=s) {
found = TRUE;
Append(PATH, w);
Output(PATH);
}
else if (!visited[w])
DFSearch(w, s, PATH);
if (!found) Delete (PATH,v);// 从路径上删除第v个顶点
}
求两个顶点之间的一条路径长度短的路 径
若两个顶点之间存在多条路径,则其中必有 一条路径长度短的路径。如何求得这条 路径?
- 将链队列的结点改为“双链”结点。即 结点中包含next 和prior两个指针
- 修改入队列的操作。插入新的队尾结点 时,令其prior域的指针指向刚刚出队列的 结点,即当前的队头指针所指结点;
- 修改出队列的操作。出队列时,仅移动 队头指针,而不将队头结点从链表中删除。
typedef DuLinkList QueuePtr;
void InitQueue(LinkQueue &Q) {
Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode));
Q.front->next = Q.rear->prior = NULL;
}
void EnQueue( LinkQueue &Q, QelemType e ) {
p = (QueuePtr) malloc(sizeof(QNode));
p->data = e; p->next = NULL;
p->prior = Q.front;
Q.rear->next = p; Q.rear = p;
}
void DeQueue( LinkQueue &Q, QelemType &e ) {
Q.front = Q.front->next;
e = Q.front->data
}
void BFSSearch(int v,int s){
for(v=0; v<G.vexnum; ++v)
visited[v] = FALSE; //初始化访问标志
InitQueue(Q);// 置空的辅助队列Q
if (!visited[v]){// v 尚未访问
visited[v] = TRUE;
Visit(v); // 访问v
EnQueue(Q, v);// v入队列
while(!QueueEmpty(Q)) {
DeQueue(Q, u); // 队头元素出队并置为u
for(w=FirstAdjVex(G, u); w>=0&&!found; w=NextAdjVex(G,u,w))
if ( ! visited[w]){
visited[w]=TRUE;
Visit(w);
EnQueue(Q, w);// 访问的顶点w入队列
if (w==s) {
outshortpath(Q);
found=TRUE;
}
} // if
if (found)
break;
} // while
}
}
生成树
定义:
所有顶点均由边连接在一起,但不 存在回路的图叫生成树
深度优先生成树与广度优先生成树
生成森林:
非连通图每个连通分量的生成 树一起组成非连通图的生成森林。
说明:
- 一个图可以有许多棵不同的生成树
- 所有生成树具有以下共同特点:
- 生成树的顶点个数与图的顶点个数相同
- 生成树是图的极小连通子图
- 一个有n个顶点的连通图的生成树有n-1条边
- 生成树中任意两个顶点间的路径是唯一的
- 在生成树中再加一条边必然形成回路
- 含n个顶点n-1条边的图不一定是生成树
(连通网的)最小生成树
假设要在n 个城市之间建立通讯 联络网,则连通n 个城市只需要修建 n-1条线路,如何在最节省经费的前 提下建立这个通讯网?
该问题等价于:
构造网的一棵小生成树,即:在e 条 带权的边中选取n-1 条边(不构成回路), 使“权值之和”为小
解决方案一:普里姆算法
算法的基本思想:
取图中任意一个顶点v 作为生成树的根, 之后往生成树上添加新的顶点w。在添加 的顶点w 和已经在生成树上的顶点v 之间 必定存在一条边,并且该边的权值在所有 连通顶点v 和w 之间的边中取值最小。之 后继续往生成树上添加顶点,直至生成树 上含有n-1 个顶点为止。
一般情况下所添加的顶点应满足下列条件:
在生成树的构造过程中,图中n 个 顶点分属两个集合:已落在生成树上的 顶点集U和尚未落在生成树上的顶点集 V-U,则应在所有连通U中顶点和V-U中 顶点的边中选取权值最小的边
算法步骤:
- 设N=(V,{E})是连通网,TE是N上小生成树 中边的集合
- 1、初始令U={u0},(u0属于V), TE为空
- 2、在所有u属于U,v属于V-U的边(u,v)属于E中,找一 条代价小的边(u0,v0)
- 3、将(u0,v0)并入集合TE,同时v0并入U
- 4、重复2和3步直至U=V为止,则T=(V,{TE}) 为N的小生成树
算法实现:使用邻接矩阵表示图
//设置一个辅助数组,对当前V-U集 中的每个顶点,
//记录和顶点集U中顶点 相连接的代价最小的边:
struct {
VertexType adjvex; // U集中的顶点序号
VRType lowcost; // 边的权值
}closedge[MAX_VERTEX_NUM];
void MiniSpanTree_P(MGraph G, VertexType u) {
//用普里姆算法从第u个顶点出发构造网G的最小生成树
k = LocateVex ( G, u );
for ( j=0; j<G.vexnum; ++j ) // 辅助数组初始化
if(j!=k)
closedge[j] = { u, G.arcs[k][j].adj };
closedge[k].lowcost = 0; // 初始,U={u}
for(i=0; i<G.vexnum; ++i) {
// 继续向生成树上添加顶点
k = minimum(closedge); // 求出加入生成树的下一个顶点(k)
printf(closedge[k].adjvex, G.vexs[k]); // 输出生成树上一条边
closedge[k].lowcost = 0; // 第k顶点并入U集
for(j=0; j<G.vexnum; ++j) //修改其它顶点的最小边
f (G.arcs[k][j].adj < closedge[j].lowcost)
closedge[j] = { G.vexs[k], G.arcs[k][j].adj };
}
}
时间复杂度分析:
假设网中有n个顶点,则第一个进行初始化的 循环语句的频度为n,第二个循环语句的频度 为n-1。其中有两个内循环:其一是在 closedge[v].lowcost中求小值,其频度为n-1 ;其二是重新选择具有小代价的边,其频 度为n。由此,普里姆算法的时间复杂度为 O(n^2),与网中的边数无关,因此适用于求 边稠密的网的小生成树。
算法评价:T(n)=O(n²)
解决方案二:克鲁斯卡尔算法
考虑问题出发点:
为使生成树上边的权 值之和达到最小,则应使生成树中每一条 边的权值尽可能地小。
具体做法:
先构造一个只含n 个顶点的子图 SG,然后从权值最小的边开始,若它的添 加不使SG 中产生回路,则在SG 上加上这 条边,如此重复,直至加上n-1 条边为止。
算法描述:
构造非连通图ST=( V,{ } );
k = i = 0; // k 计选中的边数
while(k<n-1) {
++i;
检查边集E 中第i 条权值最小的边(u,v);
若(u,v)加入ST后不使ST中产生回路,
则输出边(u,v);且k++;
}
算法的时间复杂度:
克鲁斯卡尔算法的时间复杂度为O(eloge),适 合于求边稀疏的网的小生成树。
两种算法比较
Prim算法:归并顶点,与边数无关,适于稠密网
Kruskal算法:归并边,适于稀疏网
拓扑排序
假设以有向图表示一个工程的施工图或程序 的数据流图,则图中不允许出现回路。 检查有向图中是否存在回路的方法之一,是 对有向图进行拓扑排序。
什么是拓扑排序:
对有向图进行如下操作:按照有向图给出的次序关系,将图中顶 点排成一个线性序列,对于有向图中没有 限定次序关系的顶点,则可以人为加上任 意的次序关系。
一个AOV网的拓扑序列不是唯一的
这种用顶点表示活动,用弧表示活动间的 优先关系的有向图称为顶点表示活动的网 (Activity On Vertex Network),简称AOV网
如何进行拓扑排序:
- 从有向图中选取一个没有前驱 的顶点,并输出之
- 从有向图中删去此顶点以及所 有以它为尾的弧
- 重复上述两步,直至图空,或者图不空但找 不到无前驱的顶点为止。
算法描述:
取入度为零的顶点v;
while (v<>0){
printf(v);
++m;
w=FirstAdj(v);
while (w<>0) {
inDegree[w]--;
w=nextAdj(v,w);
}
取下一个入度为零的顶点v;
}
if m<n printf(“图中有回路”);
说明:
- 以邻接表作存储结构
- 为避免每次都要搜索入度为零的顶点,在算法 中设置一个“栈”,以保存“入度为零”的顶点
算法实现如下:
Status TopologicalSort(ALGraph G){
FindInDegree(G,indegree); //对各顶点求入度
InitStack(S);
for( i=0; i<G.vexnum; ++i)
if (!indegree[i]) Push(S, i);
//入度为零的顶点入栈
count=0; //对输出顶点计数
while(!EmptyStack(S)) {
Pop(S, i); printf(i,G.vertices[i].data);++count;
for(p=G.vertices[i].firstarc;p;p=p->nextarc){
k=p->adjvex; //取出弧头顶点在图中的位置
if(!(--indegree[k]) Push(S, k);
//弧头顶点的入度减一,新产生的入度为零的顶点入 栈
}
}
if(count<G.vexnum)
return ERROR; //图中有回路”)
else
return OK;
}
关键路径
假设以有向网表示一个施工流图,弧上的权 值表示完成该项子工程所需时间。 问:哪些子工程项是“关键工程”? 即:哪些子工程项将影响整个工程的完成期限?
整个工程完成的时间为:从有向图的源点到汇点 的最长路径
这种用边表示活动的网,称AOE网。AOE网 是一个带权的有向无环图,其中,顶点表 示事件,权表示活动持续的时间
如何求关键活动:
- “事件(顶点)” 的最早发生时间ve(j) ve(j) = 从源点到顶点j的最长路径长度
- “事件(顶点)” 的最迟发生时间vl(k) vl(k) =在保证汇点在ve(汇点) 时刻完成 的前提下,事件k允许的最迟开始时间
假设第i 条弧为<j, k>
则对第i 项活动而言
“活动(弧)”的最早开始时间ee(i)
ee(i) = ve(j);
“活动(弧)”的最迟开始时间el(i)
el(i) = vl(k) –dut(<j,k>);
事件发生时间的计算公式:
ve(源点) = 0;
ve(k) = Max{ve(j) + dut(<j, k>)}
vl(汇点) = ve(汇点);
vl(j) = Min{vl(k) –dut(<j, k>)}
算法的实现要点:
- 显然,求ve的顺序应该是按拓扑有序的次 序
- 而求vl的顺序应该是按拓扑逆序的次序
- 因为拓扑逆序序列即为拓扑有序序列的 逆序列
- 因此应该在拓扑排序的过程中, 另设一个“栈”记下拓扑有序序列
两点之间的最短路径问题
求从某个源点到其余各点的短路径
算法的基本思想:
依最短路径的长度递增的次序求得 各条路径
路径长度最短的最短路径的特点:
在这条路径上,必定只含一条弧,并且 这条弧的权值最小。
下一条路径长度次短的最短路径的特点:
它只可能有两种情况:或者是直接从源 点到该点(只含一条弧);或者是从源点经 过顶点v1,再到达该顶点(由两条弧组成)。
再下一条路径长度次短的最短路径的特点:
它可能有三种情况:或者是直接从源点到 该点(只含一条弧);或者是从源点经过顶点 v1,再到达该顶点(由两条弧组成);或者是 从源点经过顶点v2,再到达该顶点。
其余最短路径的特点:
它或者是直接从源点到该点(只含一条 弧);或者是从源点经过已求得最短路径 的顶点,再到达该顶点。
求最短路径的迪杰斯特拉算法:
设置辅助数组Dist,其中每个分量Dist[k] 表示当前所求得的从源点到其余各顶点k 的最短路径
一般情况下, Dist[k] = <源点到顶点k 的弧上的权值> 或者= <源点到其它顶点的路径长度> +<其它顶点到顶点k 的弧上的权值>。
每一对顶点之间的短路径
弗洛伊德算法的基本思想是:
从v i到v j的所有 可能存在的路径中,选 出一条长度最短的路径。
以此类推,则vi至vj的最短路径应该是上述这些路径中,路径长度最小者。