图讲解

图
图(Graph)是由顶点的有穷非空集合和顶点之间的边的集合组成,通常表示为:G(V, E), V为顶点集,E为边集,G为图

图的定义
图(Graph)是由顶点的有穷非空集合和顶点之间的边的集合组成,通常表示为:G(V, E), V为顶点集,E为边集,G为图
几点说明
1)线性表中的数据元素称元素,树中称结点,图中则称为顶点(Vertex)
2)线性表可以没有数据元素,称空表,树可以没有结点称空树,而图则一般不允许V是空集,即至少有一个顶点
3)线性表中,相邻数据元素之间具有线性关系,树中相邻两层具有层次关系,而图中任意两个顶点之间都可能有关系,其逻辑关系用边来表示
边集可以为空


各种图的定义
1)无向边:顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序对(vi, vj)来表示,特别的,无论vi vj 是否相等
(vi, vj) = (vj, vi)
2)有向边:顶点vi到vj的边有方向,则称这条边为有向边,也称为弧(Arc),用有序对<vi, vj>来表示,vi称弧头(Head),vj称弧尾(Tail)
如果图中任意两个顶点之间都是有向边,称该图为有向图(Directed graphs)
3)在图中,若不存在顶点到其自身的边,且同一条边不重复出现,称这样的图为简单图
4)在无向图中,若任意两个顶点之间都存在边,称为无向完全图,n个顶点称为n阶无向完全图,n阶无项完全图的边数为(n - 1)n / 2
5)在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图,n阶有向完全图的边数为(n - 1)n
6)有很少条边或弧的图称为稀疏图,反之称为稠密图
7)有些图的边或弧具有与它相关的数字,这种数称为权(Weight),这些权可以表示从一个顶点到另一个顶点的距离或其他数据,这种带权的图
通常称为网(Network)
8)假设有两个图G = { V,E }, G' = {V', E'}  其中,V'包含于V, E'包含于E,则称G'为G的子图


图的顶点与边之间的关系
1)相邻,邻接点,点的度TD(v),出度OD(v),入度ID(v), 对有向图TD = OD + ID
2)路径的长度是路径上的边或弧的数目
3)第一个顶点到最后一个顶点相同的路径称为回路或环,序列中顶点不重复出现的路径称为简单路径,除了第一个顶点和最后一个顶点之外,其余顶点
不重复出现的回路称为简单回路或简单环


连通图相关术语
1)在无向图G中,如果顶点v到顶点 v' 之间有路径,称这两个顶点是连通的,如果对于图中任意两个顶点都是连通的,则称G是连通图
2)无向图中的极大连通子图称为连通分量,(强调以下几点)
(1)要是子图
(2)子图要是连通的
(3)连通子图含有极大顶点数
(4)具有极大顶点数的连通子图包含依附于这些顶点的所有边
3)在有向图G中,如果每一对vi, vj属于V, vi != vj,从vi到vj和从vj到vi都存在路径,则称G是强连通图
有向图中的极大强连通子图称做有向图的强连通分量
4)连通图的生成树定义
一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n - 1条边
如果一个有向图恰有一个顶点的入度为0,其余顶点的入度为1,则是一棵有向树
一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧



图的存储结构
1)邻接矩阵
考虑到图是由顶点和边两部分组成,自然考虑到使用两个结构来存储,顶点不分大小,主次,可以使用一个一维数组来存放
边是顶点和顶点之间的关系,一维解决不了,考虑使用一个二维数组来存储
图的邻接矩阵存储方式是用两个数组来表示图,一个一维数组存储图中顶点的信息,一个二维数组(称为邻接矩阵)存储图中边的信息
NOTE 无向图的边数组是一个对称矩阵
有向图顶点的入度是列数和,出度是行数和
/*结构创捷*/
typedef char VertexType;           //顶点类型由用户定义
typedef int EdgeType;              //边上的权值类型由用户定义
#define MAXVEX 100                 //最大顶点数,用户定义
#define INFINITY 65565             //用 65565 代表无穷
typedef struct {
	VertexType vexs[MAXVEX];        //顶点表
	EdgeType arc[MAXVEX][MAXVEX];   //邻接矩阵,可看成边表
	int numVertexes, numEdges;      //图中当前的顶点数和边数
}MGraph;
/*无向网图的创建*/
void CreatMGraph(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 < G->numEdges; k++)                 //读入numEdges条边,建立邻接矩阵
	{
		printf("输入边(vi,vj)上的下标 i ,下标 j 和权 w\n");
		scanf("%d%d%d", &i, &j, &w);
		G->arc[i][j] = w;
		G->arc[j][i] = G->arc[i][j];                  //因为是无向图,矩阵对称
	}
}


2)邻接表
邻接矩阵对于边数相对顶点较少的图,这种存储结构浪费存储空间,考虑使用链式存储结构来存储边或弧来避免空间浪费
把数组与链表相结合的存储方法称为邻接表法
1)图中顶点用一个一维数组存储,(也可以使用单链表),但是相较于单链表,数组可以更方便的读取结点信息
对于顶点数组,每个数据元素还需要存储指向第一个邻接点的指针,以便查找该顶点的边信息
2)图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,
有向图称为顶点vi作为弧尾的出边表
顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点信息,firstedge是指针域,指向边表的第一个结点,即
此结点的第一个邻接点
边表结点由adjvex和next两个域组成,adjvex是邻接点域,存储某结点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值
/*结构定义*/
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 CreatALGraph(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(&G->adjList[i].data);             //输入顶点信息
		G->adjList[i].firstedge = NULL;         //将边表置为空表
	}

	for (k = 0; k < G->numEdges; k++)          //建立边表
	{
		printf("输入边(vi,vj)上的顶点序号\n");
		scanf("%d%d", &i, &j);
		e = (EdgeNode *)malloc(sizeof(EdgeNode));    //申请空间,建立边表结点
		
		//这里使用的是头插法
		e->adjvex = j;                               //邻接序号为 j
		e->next = G->adjList[i].firstedge;           //将 e 指针指向当前顶点指向的结点
		G->adjList[i].firstedge = e;                 //将当前顶点的指针指向 e

		e = (EdgeNode *)malloc(sizeof(EdgeNode));
		e->adjvex = i;                                //邻接序号为 i
		e->next = G->adjList[j].firstedge;
		G->adjList[j].firstedge = e;
	}
}
NOTE 对于无向图,一条边对应得都是两个顶点,所以在循环中一次就针对 i 和 j 分别进行了插入
对于n个顶点e条边来说,该算法时间复杂度O(n + e)



3)十字链表

4)邻接多重表

5)边集数组




图的遍历
从图中某一顶点出发访问图中其余顶点,且使每个顶点仅被访问一次,这一过程称为图的遍历

1)深度优先遍历(Depth_First_Search),简称DFS
/*使用邻接矩阵*/
typedef int Boolean;     //Boolean 是布尔类型,其值是TRUE or FALSE
Boolean visited[MAX];    //访问标志的数组

/*邻接矩阵的深度优先递归算法*/
void DFS(MGraph G, int i)
{
	int j;
	visited[i] = TRUE;
	printf("%c ", G.vexs[i]);          //打印顶点,可以改成其他操作
	for (j = 0; j < G.numVertexes; j++)
		if (G.arc[i][j] == 1 && !visited[j])     //如果 vi 到 vj 之间存在路径,且 vj 顶点没有被访问
			DFS(G, j);                //对访问的邻接顶点递归调用
}

/*邻接矩阵的深度遍历操作*/
void DFSTraverse(MGraph G)
{
	int i;
	for (i = 0; i < G.numVertexes; i++)
		visited[i] = FALSE;            //初始状态所有顶点都是未访问过状态
	for (i = 0; i < G.numVertexes; i++)
		if (!visited[i])               //对未访问过的顶点调用DFS,若是连通图,只会执行一次
			DFS(G, i);
}


/*使用邻接表*/
/*邻接表的深度优先递归算法*/
void DFS(GraphAdjList GL, int i)
{
	EdgeNode *p;
	visited[i] = TRUE;
	printf("%c ", GL->adjList[i].data);
	p = GL->adjList[i].firstedge;
	while (p)
	{
		if (!visited[p->adjvex])
			DFS(GL, 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);
}
NOTE 对于 n 个顶点 e 条边的图来说,邻接矩阵由于是二维数组,在遍历时需要访问每个元素,其复杂度是 O(n^2),而
邻接链表则取决于顶点和边的数量,是O(n+e)




2)广度优先遍历(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);             //将队中元素出列,赋值给 i
				for (j = 0; j < G.numVertexes; j++)
				{
					//判断其他顶点若与当前顶点存在边且未访问过
					if (G.arc[i][j] == 1 && !visited[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;        //指针指向下一个邻接点
				}
			}
		}
	}
}
NOTE 图的深度和广度优先遍历算法,其时间复杂度是相同的,即在全图遍历时,两者没有优劣之分,不同之处仅在与对顶点访问的顺序不同
但是深度优先更适合目标比较明确,以找到目标为主要目的的情况,广度优先更适合在不断扩大遍历范围时找到相对最优解的情况




最小生成树
把构造连通网的最小代价生成树称为最小生成树(Minimum Cost Spanning Tree)
1)普里姆(Prim)算法
/*Prim 算法生成最小生成树*/
void MiniSpanTree_Prim(MGraph G)
{
	int min, i, j, k;
	int adjvex[MAXVEX];          //保存相关顶点下标
	int lowcost[MAXVEX];         //保存相关顶点间边的权值

	lowcost[0] = 0;               //初始化第一个权值为 0,即 v0 加入生成树
	                              //lowcost 的值为 0 在这里就是此下标的顶点已经加入生成树
	adjvex[0] = 0;                //初始化第一个顶点下标为 0
	for (i = 1; i < G.numVertexes; i++)         //循环除下标为 0 外的全部顶点
	{
		lowcost[i] = G.arc[0][i];       //将 v0 顶点与之有边的权值存入数组
		adjvex[i] = 0;                  //初始化都为 v0 的下标
	}

	for (i = 1; i < G.numVertexes; i++)
	{
		min = INFINITY;              //初始化最小权值为无穷
		                             //通常设置不可能的大数字如32767,65535等作为无穷
		j = 1;                       
		k = 0;

		while (j < G.numVertexes)             //循环全部顶点
		{
			if (lowcost[j] != 0 && lowcost[j] < min)
			{
				//如果权值不为0且权值小于min
				min = lowcost[j];           //则让当前权值成为最小值
				k = j;                      //将当前最小值的下标存入 k
			}
			j++;
		}
		printf("(%d,%d)", adjvex[k], k);       //打印当前顶点边中权值最小边
		lowcost[k] = 0;           //将当前顶点的权值设置为 0,表示此顶点已经完成任务

		for (j = 1; j < G.numVertexes; j++)        //循环所有顶点
		{
			if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j])
			{
				//若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值
				lowcost[j] = G.arc[k][j];        //将较小权值存入 lowcost
				adjvex[j] = k;                   //将下标为 k 的顶点存入 adjvex
			}
		}
	}
}

Prim算法的实现定义
假设N=(P,{E}) 是连通网,TE是N上最小生成树中边的集合。算法从U={u0}(u0∈V),TE={}开始,重复执行下述操作:
在所有u∈U,v∈V-U的边(u,v)∈E中找一条代价最小的边(u0,v0)并入集合TE,同时v0并入U,直至U=V为止,此时TE
中必有n-1条边,则T={V,{TE}}为N的最小生成树
该算法时间复杂度为 O(n^2)



2) 克鲁斯卡尔(Kruskal)算法
/*对边集数组Edge结构的定义*/
typedef struct {
	int begin;
	int end;
	int weight;
}Edge;

/*Kruskal算法生成最小生成树*/
void MiniSpanTree_Kruskal(MGraph G)
{
	int i, n, m;
	Edge edges[MAXEDGE];              //定义边集数组
	int parent[MAXVEX];               //定义一数组用来判断边与边是否形成环路
	                            //此处省略将邻接矩阵G转化为边集数组edges并按权由小到大排序的代码 --  详细代码见实现部分
	for (i = 0; i < G.numVertexes; i++)
		parent[i] = 0;                //初始化数组值为 0

	for (i = 0; i < G.numEdges; i++)         //循环每一条边
	{
		n = Find(parent, edges[i].begin);
		m = Find(parent, edges[i].end);
		if (n != m)           //假如 n != m,说明此边没有与现有生成树形成环路
		{
			parent[n] = m;      //将此边的结尾顶点放入下标为起点的parent中
			                    //表示此顶点已经在生成树集合中
			printf("(%d,%d) %d ", edges[i].begin, edges[i].end, edges[i].weight);
		}
	}
}

int Find(int *parent, int f)      //查找连线顶点的尾部下标
{
	while (parent[f] > 0)
		f = parent[f];

	return f;
}

//Kruskal 算法的简单描述
1).记Graph中有v个顶点,e个边
2).新建图Graphnew,Graphnew中拥有原图中相同的e个顶点,但没有边
3).将原图Graph中所有e个边按权值从小到大排序
4).循环:从权值最小的边开始遍历每条边 直至图Graph中所有的节点都在同一个连通分量中
if 这条边连接的两个节点于图Graphnew中不在同一个连通分量中
添加这条边到图Graphnew中


对比两个算法,Kruskal 算法主要是针对边展开,边数少时效率会非常高,所以对于稀疏图有很大的优势,Prim算法则对于稠密图,
即边数非常多的情况会更好一些





最短路径
在网图和非网图中,最短路径含有不同。
非网图边上没有权值,其最短路径就是指两顶点之间经过的边数最少的路径
网图其最短路径则是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点
esp 非网图可以理解为所有边的权值都为 1 的网
下面简述两种求最短路径的算法
1----从某个源点到其余各顶点的最短路径问题
1)迪杰斯特拉(Dijkstra)算法
这是一个按路径长度递增的次序产生最短路径的算法
#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];      //将与 v0 点有连线的顶点加上权值
		(*P)[v] = 0;                    //初始化路径数组 P 为 0
	}
	(*D)[v0] = 0;                  //v0 至 v0 路径为 0
	final[v0] = 1;                 //v0 至 v0 不需要求路径

	//开始主循环,每次求得v0到某个v顶点的最短路径
	for (v = 1; v < G.numVertexes; v++)
	{
		min = INFINITY;              //当前所知离v0顶点的最近距离
		for (w = 0; w < G.numVertexes; w++)       //寻找离v0最近的顶点
		{
			if (!final[w] && (*D)[w] < min)
			{
				k = w;
				min = (*D)[w];           //w顶点离v0顶点更近
			}
		}

		final[k] = 1;                 //将目前找到的最近的顶点置为 1
		for (w = 0; w < G.numVertexes; w++)      //修正当前最短路径及距离
		{
			//如果经过 v 顶点的路径比现在这条路径的长度短的话
			if (!final[w] && (min + G.matirx[k][w] < (*D)[w]))
			{
				//说明找到了更短的路径,修改 D[w] 和 P[w]
				(*D)[w] = min + G.matirx[k][w];       //修改当前路径长度
				(*P)[w] = k;
			}
		}
	}

}


2)弗洛伊德(Floyd)算法
typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];

//Floyd 算法,求网图G中各顶点到其余顶点 w 最短路径P[v][w] 及带权长度D[v][w]
void ShortestPath_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];       //D[v][w] 值即为对应点间的权值
			(*P)[v][w] = w;                    //初始化 P
		}
	}
	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];           //路径设置经过下标为 k 的顶点
				}
			}
		}
	}
}

//求最短路径的显示代码
for (v = 0; v < G.numVertexes; ++v)
{
	for (w = v + 1; w < G.numVertexes; w++)
	{
		printf("v%d-v%d weight %d\n", 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");
}
NOTE Floyd 算法是 O(n^3)复杂度的,但是如果面临需要求所有顶点至所有顶点的最短路径问题时,Floyd算法是很好的选择






拓扑排序
上面的最短路径是有环的图的引用,现在来看一看无环图的应用

拓扑排序简介
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网
我们称为AOV网(Activity On VertexNetwork)
AOV网中的弧表示活动之间存在某种制约关系,且AOV网中是不能存在回路的
设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1,v2,...,vn满足若从顶点vi到vj有一条路径,则在顶点序列中顶点vi必在顶点vj之前,
我们称这样的顶点序列为一个拓扑序列
所谓拓扑排序其实就是对一个有向图构造拓扑序列的过程
构造时会有两个结果,如果此网的全部顶点都被输出,则说明它是不存在环的AOV网,如果输出顶点少了,则这个网存在环,不是AOV网

拓扑排序算法
对AOV网进行拓扑排序的基本思路是:从AOV网中选择一个入度为 0 的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,
继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为 0 的顶点为止
对于这个图使用的数据结构,和前面求最小生成树和最短路径时有所不同,因为牵扯到顶点的删除,采用链表结构可能会更好
同时需要查找入度为 0 的结点,需要增设一个数据域来表示入度
//拓扑排序中涉及的结构
typedef struct EdgeNode {      //边表结点--和之前定义的邻接表相同
	int adjvex;
	int weight;
	struct EdgeNode *next;
}EdgeNode;

typedef struct VertexNode {         //顶点表结点--增设了一个表示入度的数据域 in
	int in;               //顶点入度
	int data;             //顶点域,存储顶点信息
	EdgeNode *firstedge;    //边表头指针
}VertexNode,*AdjList[MAXVEX];

typedef struct {
	AdjList adjList;
	int numVerteses, numEdges;       //图中当前的顶点数和边数
}graphAdjList,*GraphAdjList;
在算法中,还需要辅助的数据结构--栈,用来处理过程中入度为 0 的顶点,其目的是为了避免每个查找时都要去遍历顶点表找有没有入度为 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(sizeof(int)*GL->numVerteses);

	for (i = 0; i < GL->numVerteses; i++)        //将入度为 0 的顶点入栈
		if (GL->adjList[i].in == 0)
			stack[++top] = i;

	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 ,如果此时入度为 0 则入栈
				stack[++top] = k;
		}
	}

	if (count < GL->numVerteses);
	return ERROR;
	else
		return OK;
}
//分析:对该算法,具有n个顶点e条弧的AOV网来说,扫描顶点表用O(n),之后的while循环中,每个顶点入栈一次,出栈一次,入度减 1 的操作共
//执行了 e 次,所以整个算法的时间复杂度为 O(n+e)







关键路径
拓扑排序主要是为了解决一个有先后顺序的工程是否能顺利进行,但是有时还需要解决工程完成所需要的最短时间问题
因此如果要对一个流程图获得最短时间,就必须要分析它们的拓扑关系,并且找到当中最关键的流程,这个流程所用时间最短
在介绍了AOV网之后,现在介绍一个新概念:
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,
这种有向图的边表示活动的网,称为AOE网(Activity On EdgeNetwork)
把AOE网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点,一般一个工程总有一个开始和一个结束,
所以正常情况下AOE网只有一个源点一个汇点。

注意AOV与AOE网描述工程的不同
AOV网是顶点表示活动的网,它只描述活动之间的制约关系,而AOE网是用边表示活动的网,边上的权值表示活动持续的时间
因此AOE网是要建立在活动之间制约关系没有矛盾的基础之上,再来分析完成整个工程需要多少时间等问题

我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动
需要指出,一般对于一个工程活动,只有缩短关键路径的长度,完成整个活动所需的时间才会缩短,这在现实中需要处理的是关键活动所需要的时间



关键路径算法原理
只需要找到所有活动的最早开始时间和最晚开始时间,并且比较它们。
如果相等就意味着此活动是关键活动,活动间的路径为关键路径,如果不等就不是
为此,定义如下参数
1)事件的最早发生时间 etv(earlist time of vertex) :即顶点 vk 的最早发生时间
2)事件的最晚发生时间 ltv(latest time of vertex) : 即顶点 vk 的最晚发生时间
3)活动的最早开工时间 ete(earliest time of edge) : 即弧 ak 的最早发生时间
4)活动的最晚开工时间 lte(latest time of edge) : 即 弧 ak 的最晚发生时间,即不推迟工期的最晚开工时间
由此可以看出,由 1)2)可以求出 3)4)然后根据 ete[k] 是否等于 lte[k] 来判断 ak 是否是关键活动
算法
求事件的最早发生事件etv的过程就是从头至尾找拓扑序列的过程,因此在求关键路径之前,需要先调用一次拓扑序列算法来计算 etv 和拓扑序列列表
为此定义以下几个全局变量
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;
	stack = (int *)malloc(sizeof(int)*GL->numVerteses);
	for (i = 0; i < GL->numVerteses; i++)             //将入度为 0 的顶点入栈
		if (GL->adjList[i].in == 0)
			stack[++top] = i;

	top2 = 0;
	etv = (int *)malloc(sizeof(int)*GL->numVerteses);     
	for (i = 0; i < GL->numVerteses; i++)        //将事件最早发生时间初始化为 0
		etv[i] = 0;

	stack2 = (int *)malloc(sizeof(int)*GL->numVerteses);

	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))
				stack[++top] = k;
			if (etv[gettop] + e->weight > etv[k])       //求各顶点时间最早发生时间
				etv[k] = etv[gettop] + e->weight;       //这里求的是较大值是因为对于一个结点步骤,只有当前置步骤都结束后才能开始,
		}                                               //需要得到这些前置步骤里最晚的结束时间是该结点的最早开始时间
	}

	if (count < GL->numVerteses)
		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(sizeof(int)*GL->numVerteses);        //事件最晚发生时间
	for (i = 0; i < GL->numVerteses; i++)
		ltv[i] = etv[GL->numVerteses - 1];       //初始化ltv

	while (top2 != 0)      //计算ltv
	{
		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[gettop] = ltv[k] - e->weight;
		}
	}

	for (j = 0; j < GL->numVerteses; 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> len %d\n", GL->adjList[j].data, GL->adjList[k].data, e->weight);
		}
	}
}





算法来源--大话数据结构

 

猜你喜欢

转载自blog.csdn.net/wyzworld/article/details/82807428