由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),G表示一个图,V是图G顶点的集合,E是图G边的集合
定义
- 图中元素叫顶点(Vertex,Node也可以)
- 图不允许没有顶点,顶点又穷非空(一种说法可空)
- 任意两个顶点之间都有可能有关系,关系用边来表示,边集可以是空的(可以没边但不能没顶点)
-
无向边(Edge),用
(vi, vj)
表示 -
无向图(Undirected graphs),任意顶点之间都是无向边
-
有向边,也叫弧(Arc),用
<vi, vj>
表示,vi
表示弧尾(Tail),vj
为弧头(Head) -
有向图(Directed graphs)
-
简单图,无环,同一条边不出现两次
-
无向完全图,无向图且任意顶点间都有边,n个顶点共有n*(n-1)/2条边
-
有向完全图,任意顶点间都有方向相反的两条弧,n个顶点共有n*(n-1)条边
-
稀疏图,边或弧很少
-
稠密图,边或弧很多
-
权(Weight)
-
网(Network),带权的图
-
子图(Subgraph)
顶点与边的关系
-
无向图,存在边(v,v‘),则v和v’互为邻接点(Adjacent)
-
顶点的度(Degree),是和v相关联的边的数目,TD(v),边数是所有度之和的1/2
-
有向图,存在弧<v,v‘>,v邻接到v‘,v’邻接自v
-
入度(inDegree),出度(outDegree)
-
TD(v)= ID(v) + OD(v)
-
顶点到顶点的**路径(Path)**是一个顶点序列
-
路径长度是路径上的边或弧的数目
-
回路、环(Cycle),首位同顶点的路径
-
简单路径,顶点不重复的路径
-
简单回路、简单环,无重复顶点的环
连通图
抽象数据类型
ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合
Operation
CreateGraph(*G, V, VR) //顶点集V和边弧集VR的定义构造图G
DestroyGraph(*G) //存在则销毁
LocateVex(G, u) //若存在顶点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) //删除顶点v和与其相关的弧
InsertArc(*G, v, w) //在图G中增加弧<v,w>,若G是无向图,还需要增加对称弧<w,v>
DeleteArc(*G, v, w) //在图G中删除弧<v,w>,若G是无向图,还需要删除对称弧<w,v>
DFSTraverse(G) //深度优先遍历,遍历过程调用每个顶点
HFSTraverse(G) //广度优先遍历,遍历过程调用每个顶点
endADT
存储结构
邻接矩阵(Adjacency Matrix)
因为图是由顶点和弧(边)两部分组成,所以可以那两个结构来存储
顶点没有先后之分,用一维数组即可
边或弧是顶点之间的关系,就用二维数组来保存
顶点数组vertex[4]={v0, v1, v2, v3},边数组arc[4] [4],无向图的边数组是一个对称矩阵
- 某个顶点的度就是顶点所在行(列)的元素之和
- 求顶点的所有邻接点就是把所在行为1的元素扫描一遍
这时就不对称了
有向图看入度和出度,入度看列,出度看行
求邻接点就将所在行扫描一遍
typedef char VertexType; /*顶点类型*/
typedef int EdgeType; /*权值*/
#define MAXVEX 100 /*最大顶点数*/
#define INFINITY 65535 /*65535表示最大数*/
typedef struct
{
VertexType vexs[MAXVEX]; /*顶点表*/
EdgeType arc[MAXVEX][MAXVEX]; /*邻接矩阵*/
int numVertexes, numEdges; /*顶点数和边数*/
}MGraph;
/*建立无向图的邻接矩阵*/
void CreateMGraph(MGraph *G)
{
int i, j, k, w;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numVertexes, &G->numEdges);
for(i = 0; i < G->numVertexes; i++)
{
scanf(&G->vexs[i]); /*顶点数组*/
}
for(i = 0; i < G->numVertexes; i++)
for(j = 0; j < G->numVertexes; j++)
G->arc[i][j] = INFINITY; /*邻接矩阵初始化*/
for(k = 0; k < numEdges; k++) /*numEdges条边*/
{
printf("输入边的下标i,j,权值w:\n");
scanf("%d,%d,%d", &i, &j, &w);
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j]; /*无向图,对称矩阵*/
}
}
邻接表(Adjacency List)
邻接矩阵对边数较少的图(稀疏图)会产生很大的空间浪费。
所以考虑另外一种存储结构方式,使用链式存储结构来避免空间浪费问题
数组与链表相结合的方法称为邻接表(Adjacency List)
- 顶点用一个一维数组存储,单链表也行,顶点数组的每个元素还要一个指向第一个邻接点的指针
- 每个顶点的所有邻接点构成一个线性表,因为个数不定,用单链表存储,无向图程顶点的边表,有向图称顶点作为弧尾的出边表\
无向图
有向图
顶点作为弧尾
顶点作为弧头
带权值的网图
typedef char VertexType; /*自定义顶点类型*/
typedef int EdgeType; /*自定义边上的权值类型*/
typedef struct EdgeNode /*边表结点*/
{
int adjvex; /*顶点域,存对应下标*/
EdgeType weight; /*权值*/
struct EdgeNode *next; /*链域*/
}EdgeNode;
typedef struct VertexNode /*顶点表结点*/
{
VertexType data; /*顶点域*/
EdgeNode *firstedge; /*边表头指针*/
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList; /*顶点数组*/
int numVertexes, numEdges; /*顶点数和边数*/
}GraphAdjList; /*邻接表*/
/*建立邻接表,无向图,头插法*/
void CreateALGraph(GraphAdjList *G)
{
int i, j, k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
scanf("%d, %d",&G->numVertexes, &G->numEdges);
for(i = 0; i < G->numVertexes; i++) /*首先建立顶点表*/
{
scanf("%d",&G->adjList[i].data);
G->adjList[i].firstedge = NULL; /*边表置空*/
}
for(k = 0; k < G->numEdges; k++)
{
printf("输入边(vi,vj)的顶点i和j:\n"); /*输入边对,所以就不用套循环了,而且因为是两个顶点,所以可 以一次操作俩*/
scanf("%d,%d",&i, &j);
e = (EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex = j;
printf("输入权值:\n");
scanf("%d",&e->weight);
e->next = G->adjList[i].firstedge; /*头插法*/
G->adjList[i].firstedge = e;
e = (EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex = i;
printf("输入权值:\n");
scanf("%d",&e->weight);
e->next = G->adjList[j].firstedge; /*头插法*/
G->adjList[j].firstedge = e;
}
}
创建邻接表,n个顶点e条边,时间复杂度O(n+e)
十字链表(Orthogonal List,一种有向图的存储方法)
对有向图来说,邻接表只能记录入度或出度
将邻接表和逆邻接表组合在一起就是十字链表
顶点表结构**data | firstin | firstout
**
边表结构(网的话再加一个weight)tailvex | headvex | headlink | taillink
弧尾顶点 | 弧头顶点 | 入边表指针域(指向终点相同的下一条边)| 出边表指针域(指向起点相同的下一条边)
实线箭头就是邻接表(出边表)
虚线是逆邻接表
创建图算法的时间复杂度和邻接表相同(一条边对两个顶点)O(n+e)
有向图的应用中,十字链表是极好的数据结构模型
邻接多重表
对于无向图的邻接表关注于顶点但是如果要对边进行标记,删除等操作,就很麻烦
按照十字链表的方式,边表结点结构ivex | ilink | jvex | jlink
构造过程:
邻接表的边由两个顶点的结点表示,而在多重表中,一条边就用一个edgeNode存,例如要删除(v0,v2)时,删掉结点然后把6,9置^就行,方便的雅痞
边集数组![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/9f4ca058179fbc0ca8f3492b16ea293f.jpeg)
不适用于顶点操作,适合于对边依次进行的操作
图的遍历
因为图的每一个顶点都可以当起点,没有树的那种从根发散的结构,所以要设一个访问数组visited[n]对访问过的顶点进行标记
深度优先遍历(Depth_First_Search,DFS)
- 一个递归的过程,和树的前序遍历类似
- 非连通图对其每个连通分量都进行DFS就行,另选一个未被访问的顶点DFS,直到所有顶点都被访问
/*邻接矩阵*/
typedef int Boolean; /*是否访问*/
Boolean visited[MAX]; /*访问标记数组*/
/*深度优先递归算法*/
int DFS(MGraph G, int i) /*每到一个顶点传G和当前顶点位置*/
{
int j;
visited[i] = TRUE;
printf("%c", G.vexs[i]); /*打印操作*/
for(j = 0; j < G.numVertexes; j++) /*相当于遍历矩阵的行,也就是当前顶点的邻接点*/
if(G.arc[i][j] && !visited[j]) /*顶点存在并且未被访问*/
DFS(G, j);
return 0;
}
/*邻接矩阵的深度优先遍历*/
int DFSTraverse(MGraph G)
{
int i;
for(i = 0; i < G.numVertexes; i++)
visited[i] = FALSE; /*初始化访问数组*/
for(i = 0; i < G.numVertexes; i++)
{
if(!visired[i]) /*如果是连通图,只执行一次,因为顶点全被标记上了
非连通图就保证不落下任何一个连通分支的起始点*/
DFS(G, i);
}
return 0;
}
/*如果图是邻接表结构,DFSTraverse()函数几乎相同,在递归时数组和链表操作不同*/
void DFS(GraphAdjList GL, int i)
{
EdgeNode *p;
visited[i] = TRUE;
printf("%c", GL->adjList[i].data); /*元素操作*/
p = GL->djList[i].firstedge; /*把直接的下一个邻接点作为起始点继续遍历,递归*/
while(p) /*遍历一个顶点的邻接表*/
{
if(!visited[p->adjvex])
DFS(DL, p->adjvex);
p = p->next;
}
}
/*遍历操作*/
void DFSTraverse(GraphAdjList GL)
{
int i;
for(i = 0; i < GL->numVertexes; i++) /*初始化*/
visited[i] = FALSE;
for(i = 0; i < GL->numVertexes; i++)
if(!visited[i]) /*同上*/
DFS(GL, i);
}
对于n个顶点e条边的图
- 邻接矩阵遍历时,二维数组,时间复杂度O(n2)
- 邻接表遍历,遍历每个顶点和边一次,时间复杂度O(n+e)
无向图算法通用
广度优先遍历(Breadth_First_Search,BFS)
类似树的层序遍历
/*邻接矩阵的广度遍历算法*/
void BFSTraverse(MGraph G)
{
int i, j;
Queue Q; /*辅助队列*/
for(i = 0; i < G.numVertexes; i++)
visited[i] = FALSE; /*初始化访问数组*/
InitQueue(&Q); /*初始化队列*/
for(i = 0; i < G.numVertexes; i++)
{
if(!visited[i]) /*顶点未被访问*/
{
visited[i] = TRUE;
printf("%c", G.vexs[i]); /*操作元素*/
EnQueue(Q, i); /*顶点入队列*/
while(!QueueEmpty(Q)) /*队列不为空*/
{
DeQueue(&Q, &i); /*出队*/
for(j = 0; j < numVertexes; j++)
{
if(!visited[j] && G.arc[i][j])
{
visited[j] = TRUE;
printf("%c", G.vexs[j]);
EnQueue(&Q, j);
}
}
}
}
}
}
/*邻接表的广度遍历算法*/
void BFSTraverse(GraphAdjList GL)
{
int i;
EdgeNode *p;
Queue Q;
for(i = 0; i < GL->numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q);
for(i = 0; i < GL->numVertexes; i++)
{
if(!visited[i])
{
visited[i] = TRUE;
printf("%c",GL->adjList[i].data);
EnQueue(&Q, i);
while(!QueueEmpty(Q))
{
DeQueue(&Q,&i);
p = GL->adjList[i].firstedge;
while(p)
{
if(!visited[p->adjvex])
{
visited[p->adjvex] = TRUE;
printf("%c",GL->adjList[p->adjvex].data);
EnQueue(&Q, p->adjvex);
}
p = p->next;
}
}
}
}
}
时间复杂度和DFS一样,深度和广度的思想让我变成了带哲学家,nb
最小生成树
引言
小学奥数嗷
一个连通图的生成树是一个极小的连通子图,有n个顶点n-1条边
把构造连通网的最小代价生成树称为最小生成树
- 克鲁斯卡尔算法针对边遍历,在边少的图中效率很高,适用于稀疏图,相应的Prim算法更加适用于稠密图
普里姆(Prim)算法
简单归纳一下算法构造方法:从一个顶点开始,遍历矩阵的行,把权值存入lowcost[]数组,然后找出权值最小的下一个顶点,加入生成树,遍历这个顶点在矩阵中的行,更新lowcost数组,找出权值最小的下一个顶点,加入生成树,……
- adjvex数组表示顶点i从adjvex[i]邻接点而来
- lowcost数组是当前树中的所有顶点到未入树顶点的权取最小时的值
- lowcost值0,说明此下标顶点已经入树(这种实际情况好像权不可能为0的样子,其他情况换一个一定取不到的值就行)
- 有了一个邻接矩阵,用65535来代表无穷
- INFINITY为权值极大值,这里就是65535
/*Prim算法生成最小生成树*/
void MiniSpanTree_Prim(MGrapg G)
{
int min, i, j, k;
int adjvex[MAXVEX]; /*保存相关顶点下标*/
int lowcost[MAXVEX]; /*保存相关顶点间边的权值*/
lowcost[0] = 0; /*表示从0顶点开始,也表示0顶点入树*/
adjvex[0] = 0;
for(i = 1; i < G.numVertexes; i++)
{
lowcost[i] = G.arc[0][i]; /*初始化0顶点到其他顶点的权值*/
adjvex[i] = 0; /*初始化其他顶点都从0顶点邻接而来*/
}
for(i = 1; i < G.numVertexes; i++)/*开始构造,每次循环让一个顶点入树*/
{
min = INFINITY; /*初始化min*/
k = 0; /*每次循环的顶点下标*/
for(j = 1; j < G.numVertexes; j++) /*找lowcost数组中的最小权*/
{
if(lowcost[j] != 0 && lowcost[j] < min)
{
min = lowcost[j];
k = j;
}
}
printf("(%d,%d)",adjvex[k], k); /*打印加入的边*/
lowcost[k] = 0; /*已入树*/
for(j = 1; j < G.numVertexes; j++) /*更新lowcost数组*/
{
if(lowcost[j]!=0 && G.arc[k][j] < lowcost[j])
{
lowcost[j] = G.arc[k][j]; /*最小权进数组*/
adjvex[j] = k; /*j顶点从k顶点邻接而来*/
}
}
}
}
算法遍历了一遍矩阵,时间复杂度为O(n2)
克鲁斯卡尔(Kruskal)算法
Prim算法从一个顶点开始,一步一步寻找权值最小的边来构成最小生成树
另外一种思路是直接寻找边上的权值,使用图存储结构中的边集数组
但是要用一个数组来判断边是否成环
大致流程:找有最小权值的边,把边连着两个顶点都入树,找下一个有最小权值的边,入树,递归。。。
parent数组大概的意思就是顶点i和顶点parent[i]连通,Find
/*边集数组结构定义*/
typedef struct
{
int begin;
int end;
int weight;
}Edge;
/*算法*/
void MiniSpanTree_Kruskal(MGraph G)
{
int i, n, m;
Edge edged[MAXEDGE]; /*定义边集数组*/
int parent[MAXVEX]; /*用来判断边与边是否形成环*/
/*默认的边集数组的weight从小到大排列*/
for(i = 0; i < G.numVertexes; i++)
{
parent[i] = 0; /*初始化数组*/
}
for(i = 0; i < G.numEdges; i++) /*循环每一条边,weight已经默认是从小到大*/
{
n = Find(parent, edge[i].begin); /*此边的begin顶点与顶点n连通*/
m = Find(parent, edge[i].end); /*此边的end顶点与m顶点连通*/
if(n != m) /*如果相等说明有环*/
{
parent[n] = m; /*此边的结尾顶点放入parent[begin],可以借此找通路*/
printf("(%d, %d) %d",edge[i].begin, edge[i].end, edge[i].weight);
}
}
}
int Find(int *parent, int f) /*查找连线顶点的尾部下标*/
{
while(parent[f] > 0)
f = parent[f];
return f;
}
时间复杂度O(eloge)
最短路径
网图和非网图中,最短路径的含义不同。非网图中没有权值,最短路径就是顶点之间边数最少的路径;对于网图来说,最短路径是顶点之间权值之和最少的路径,并且路径的第一个顶点是源点,最后一个顶点是终点
迪杰斯特拉(Dijkstra)算法
一个按路径长度递增的次序产生最短路径的算法
简单概括:从源点开始,遍历与之相连的所有顶点,即得到了该点到所有邻接点的距离,存入数组,取其中最小距离的顶点,遍历邻接点,
- final数组用来指明源点到final[i]是否以求得最短路径
- D数组存从源点到各个顶点的最短距离
- P数组存顶点在最短路径中的前驱
》》》遍历完后,根据D数组和P数组可以知道源点到每一个顶点的最短路径
》》》D得到最短路径长度,反向遍历P可得到路径
#define MAXVEX 9
#define INFINITY 65535
typedef int Pathmatirx[MAXVEX]; /*存储最短路径下标的数组*/
typedef int ShortPathTable[MAXVEX]; /*存储个点最短路径的权值和*/
/*Dijkstra算法,求有向图G的v0顶点到其余顶点v最短路径P[v]及带权长度D[v]*/
/*P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和*/
void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx *P, ShortPathTable *D)
{
int v, w, k, min;
int final[MAXVEX]; /*结果数组,final[w]=1表示顶点v0至vw的最短路径*/
/*初始化*/
for(v = 0; v < G.numVertexes; v++)
{
final[v] = 0; /*所有点都为求得最短路径*/
(*D)[v] = G.matirx[v0][v]; /*存入源点到各个顶点的距离*/
(*P)[v] = 0; /*全为0表示没有路径*/
}
(*D)[v0] = 0; /*v0到v0自身*/
final[v0] = 1; /*自己到自己算是已有*/
/*主循环,每次求v0到其中一个v的最短路径*/
for(v = 1; v < G.numVertexes; v++)
{
min = INFINITY; /**/
for(w = 0; w < G.numVertexes; w++) /*寻找离v0最近的顶点*/
{ /*获取顶点下标和与距离v0*/
if(!final[w] && (*D)[w] < min)
{
k = w;
min = (*D)[w];
}
}
final[k] = 1; /*找到了就置1*/
for(w = 0; w < G.numVertexes; w++) /*更新数组*/
{ /*遍历刚刚找到的顶点k的邻接点*/
if(!final[w] && (min+G.matirx[k][w] < (*D)[w]))
{ /*w不在顶点数组并且,v0到w的距离比距离数组中的小*/
(*D)[w] = min + G.matirx[k][w]; /*更新距离数组*/
(*P)[w] = k;
}
}
}
}
时间复杂度O(n2)
若还需知道,除源点外的各个顶点到其余各点的最短路径就要对每个顶点进行一次Dijkstra算法,这样时间复杂度就变成了O(n3)
弗洛伊德(Floyd)算法
简单来说:比较顶点直接到另一顶点的距离和顶点通过中继顶点到另一顶点的距离,将小的入矩阵
- P数组存的好像是顶点到顶点最短路径除源点外的第一个路径顶点
typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
/* Floyd算法,求网图G中各顶点v到其余顶点w最短路径P[v][w]及带权长度D[v][w] */
void ShorttestPath_Floyd(MGraph G, Pathmatirx *P, ShortPathTable *D)
{
int v, w, k;
for(v = 0; v < G.numVertexes; v++) /*初始化D与P矩阵*/
{
for(w = 0; w < G.numVertexes; w++)
{
(*D)[v][w] = G.matirx[v][w];
(*P)[v][w] = w;
}
}
for(k = 0; k < G.numVertexes; k++)
{
for(v = 0; v < G.numVertexes; v++)
{
for(w = 0; w < G.numVertexes; w++)
{
if((*D)[v][w] > (*D)[v][k]+(*D)[k][w])
{ /*如果经过下标为k顶点路径比原两顶点路径更短,则更新矩阵*/
(*D)[v][w] = (*D)[v][k]+(*D)[k][w];
(*P)[v][w] = (*P)[v][k];
}
}
}
}
}
/*最短路径的显示代码*/
for(v = 0; V < G.numVertexes; v++)
{
for(w = v+1; w < G.numVertexes; w++)
{
printf("v%d-v%d weight: %d ", v, w, D[v][w]);
k = P[v][w]; /*获得第一个路径顶点下标*/
printf(" path: %d", v); /*打印源点*/
while(k != w) /*若不是终点*/
{
printf(" -> %d", k); /*打印路径顶点*/
k = P[k][w]; /*获得路径的下一个顶点*/
}
printf(" -> %d\n", w); /*打印终点*/
}
printf("\n");
}
时间复杂度和上一个算法一样O(n3),适用于所有顶点到所有顶点的最短路径问题
拓扑排序
好像是针对无环(无回路)图
基础
一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先级关系,这样的网,叫AOV网(Activity On Vertex Network)
AOV网中不存在回路
活动之间的优先级关系举例:手机开机->打游戏(得先开机再操作)
设G=(V, E) 是一个由n个顶点的有向图,V中的顶点序列v1, v2,… ,vn,若从vi到vj有一条路径,则在顶点序列中顶点vi必在vj之前,这样的一个顶点序列是一个拓扑序列
而拓扑排序就是对一个有向图构造拓扑序列的过程,有两个结果:图的顶点全部被输出,说明它不存在回路,是AOV网;只要有一个顶点未输出,有回路,不是AOV网
拓扑排序算法
基本思路:从AOV网中输出一个入度为0的顶点,删,把以此点为弧尾的弧也删了,递归。。。
由于要删除顶点,用邻接表会好一点
in | data | firstedge
typedef struct EdgeNode /*边表结点*/
{
int adjvex; /*邻接点域,存储该顶点对应的下标*/
int weight; /*用于存储权值,对于非网图可以不需要*/
struct EdgeNode *next; /*链域,指向下一个邻接点*/
}EdgeNode;
typedef struct VertexNode /*顶点表结点*/
{
int in; /*顶点入度*/
int data; /*顶点域,存储顶点信息*/
EdgeNode *firstedge; /*边表头指针*/
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numVertexes, numEdges; /*图中当前顶点数和边数*/
}graphAdjList, *GraphAdjList;
/*用辅助数据结构————栈,存储处理过程中入度为0的结点*/
/*拓扑排序,若GL无回路,则输出拓扑排序序列并返回OK,若有回路返回ERROR*/
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i, k, gettop;
int top = 0; /*栈指针下标*/
int count = 0; /*输出顶点个数*/
int *stack; /*建栈存储入度为0的顶点*/
stack = (int *)malloc(GL->numVertexes * sizeof(int));
for(i = 0; i < GL->numVertexes; i++)
if(GL->adjList[i].in == 0)
stack[++top] = i; /*入度为0的顶点入栈*/
while(top != 0)
{
gettop = stack[top--]; /*出栈*/
printf("%d-> ", GL->adjList[gettop].data); /*打印*/
count++;
for(e = GL->adjList[gettop].firstedge; e; e = e->next)
{
k = e->adjvex;
if(!(--GL->adjList[k].in)) /*k顶点的邻接点入度-1*/
stack[++top] = k; /*入栈*/
}
}
if(count < GL->numVertexes) /*如果count小于顶点数,说明存在环*/
return ERROR;
else
return OK;
}
对AOV网来说,遍历顶点表入栈O(n),入度-1就是删弧的操作,O(e)
O(n+e)
关键路径
基础
拓扑排序是为了解决一个工程能否顺序进行
而关键路径是为了求工程完成的最短时间问题
这个就是在所有拓扑排序中,找出最小权值的的路径
AOE网(Activity On Edge Network)
在带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间
- 无入边(入度为0)的顶点为源点
- 无出边(出度为0)的顶点为终点
- 正常情况下AOE网只有一个源点和一个终点
- AOV网的顶点表示活动,AOE网的边表示活动,权值表示活动持续时间
- 路径上各个活动持续时间之和就是路径长度
- 从源点到终点具有最大长度的路径叫关键路径,关键路径上的活动叫关键活动
算法原理
找所有活动的最早开始时间和最晚开始时间,如果相等此活动就是关键活动,活动间的边就是关键路径
如果不相等意味着,当前活动和其他活动之间有空余时间可以利用
此算法相当于要充分利用时间,舍弃空余时间
- 事件最早发生时间etv(earliest time of vertex)
- 事件最晚发生时间ltv(latest time of vertex)
- 活动最早开工时间ete(earliest time of edge)
- 活动最晚开工时间lte(latest time of edge),不推迟总完成时间的最晚开工时间
算法
用邻接表,弧链表加一个weight域
/*全局变量*/
int *etv, *ltv; /*最早发生时间和最迟发生时间 数组*/
int *stack2; /*用于存储拓扑序列的栈*/
int top2; /*stack2的指针*/
/*拓扑排序,用于关键路径的计算*/
Status TopologicalSort(GraphAdjList GL)
{
EdgeNode *e;
int i, k, gettop;
int top = 0; /*栈指针下标*/
int count = 0; /*输出顶点个数*/
int *stack; /*建栈存储入度为0的顶点*/
stack = (int *)malloc(GL->numVertexes * sizeof(int));
for(i = 0; i < GL->numVertexes; i++)
if(GL->adjList[i].in == 0)
stack[++top] = i; /*入度为0的顶点入栈*/
top2 = 0; /*初始化0*/
etv = (int *)malloc(GL->numVertexes * sizeof(int)); /*事件最早发生时间*/
for(i = 0; i < GL->numVertexes; i++)
etv[i] = 0; /*初始化为0*/
stcak2 = (int *)malloc(GL->numVertexes*sizeof(int)); /*初始化*/
while(top != 0)
{
gettop = stack[top--]; /*出栈*/
count++;
stack2[++top2] = gettop; /*弹出顶点序号压入拓扑排序的栈*/
for(e = GL->adjList[gettop].firstedge; e; e = e->next)
{
k = e->adjvex;
if( !(--GL->adjList[k].in) ) /*k顶点的邻接点入度-1*/
stack[++top] = k; /*入栈*/
if((etv[gettop]+e->weight) > etv[k]) /*求各顶点事件最早发生时间值*/
etv[k] = etv[gettop] + e->weight;
}
}
if(count < GL->numVertexes) /*如果count小于顶点数,说明存在环*/
return ERROR;
else
return OK;
}
/*关键路径,GL为有向图,输出GL的各项关键活动*/
void CriticalPath(GraphAdjList GL)
{
EdgeNode *e;
int i, gettop, k, j;
int ete, lte; /*声明活动最早发生时间和最迟发生时间*/
TopologicalSort(GL); /*求拓扑序列,计算数组etv和stack2的值*/
ltv = (int *)malloc(GL->numVertexes*sizeof(int)); /*事件最晚发生时间*/
for(i = 0; i < GL->numVertexes; i++)
{
ltv[i] = etv[GL->numVertexes-1]; /*初始化ltv,等于终点的etv*/
}
while(top2!=0) /*计算ltv,就是通过(弧头-weight)获得*/
{
gettop = stack2[top2--]; /*拓扑序列出栈*/
for(e = GL->adjList[gettop].firstedge; e; e = e->next)/*遍历顶点的弧表*/
{ /*求各顶点事件的最迟发生时间ltv值*/
k = e->adjvex;
if(ltv[k]-e->weight < ltv[gettop]) /*求各顶点事件最晚发生事件ltv*/
ltv[gettop] = ltv[k] - e->weight;
}
}
for(j = 0; j < GL->numVertexes; j++) /*求ete,lte和关键活动*/
{
for(e = GL->adjList[j].firstedge; e; e = e->next)
{
k = e->adjvex;
ete = etv[j]; /*活动最早发生时间*/
lte = ltv[k] - e->weight; /*活动最迟发生时间*/
if(ete == lte) /*两者相等即在关键路径上*/
printf("<v%d,v%d> length: %d , ",
GL->adjList[j].data, GL->adjList[k].data, e->weight);
}
}
}
关键路径算法时间复杂度O(n+e)
如果是多条关键路径,提高同时在几条关键路径的活动的速度