C语言——图(下)(图的连通性、有向无环图及其应用、最短路径)

前言

本篇继续图的学习,
看完本篇,你将了解到:
(1)图的连通性,基于此将开展一系列相关问题。(介绍连通分量及最小生成树等概念)
(2)有向无环图及其应用(将介绍重要的拓扑排序和关键路径)
(3)最短路径(基于关键路径进行求解,介绍迪杰斯特拉算法和弗洛伊德算法)
(划重点)关键路径和两大求解最短路径算法非常重要,是数据结构中图部分非常重要的部分,并依次解决众多后序算法问题。
(图较多)

一、图的连通性

1.无向图的连通分量和生成树

(1)连通分量
在这里插入图片描述

由于该图是连通图,故从任意顶点出发,都能访问到所有其他的顶点
加上所有顶点间的边,可得原图
故:连通图的连通分量是它自己

在这里插入图片描述

需经3次遍历,才可访问所有结点,故G2有3个连通分量

(2)DFS生成树
定义:对一个无向连通图进行深度优先搜索遍历,遍历过程中经过的边
和原图中的顶点组成的图
在这里插入图片描述

其对应的可能生成的DFS树为
在这里插入图片描述
在这里插入图片描述

(3)BFS生成树
定义 :对一个无向连通图进行广度优先搜索遍历,遍历过程中经过的边
和原图中的顶点组成的图
在这里插入图片描述

其对应的可能生成的DFS树为
在这里插入图片描述

(4)DFS生成森林
定义:对一个非连通图进行深度优先搜索遍历,需要多次遍历,每次遍历得到
一棵树
在这里插入图片描述

(5)DFS生成森林
定义:对一个非连通图进行广度优先搜索遍历,需要多次遍历,每次遍历得到
一棵树
在这里插入图片描述

2.有向图的强连通分量

在有向图G中,从某个顶点v出发,顺着弧的方向进行DFS得到
顶点集合V﹔再顶点v出发,逆着弧的方向进行DFS,得到顶点集合V2﹔
其中:
Vs= vi n V2 (交集);
则VS中的任意两个顶点都是互相可到达的
VRs为Vs中所有顶点在G中的弧.

于是 ,各顶点加上集合内的弧,得到一个强连通分量:Gs= (Vs,VRs)
将该强连通分量从原图中去掉 ,对剩下的再重复上述操作 ,即可求出所有
强连通分量
在这里插入图片描述

3.网的最小生成树

(1)定义:在网G的各生成树中,其中各边的权之和最小的生成树

(2)举例:假设有n个城市,要建立一个通信网
需考虑的问题: 如何建立这个通讯网,经费最省。
即如何确定n-1条线路,使得总的费用最低,同时任意两个城市之间可以相互通信。
可将城市看为顶点,线路当作带权值的边,用n-1条线路连接n
个城市的方法有很多,每一种均可看作一颗生成树

则将上述问题转化成:各边权值之和具有最小值

在这里插入图片描述
T3 T5均为最小生成树,可见,最小生成树可能不唯一

(3)MST性质
设G= (V,E)是一个连通网,U是V的一个非空子集。则图中所有顶点构成的集合
被分为U, V-U
如果边(u,v)是G中具有最小权值的一条边,(一端在U中(即u∈U),
而另一端在V-U中(即v∈V-U)),则存在一棵包含边(u,v)的最小生成树。
在这里插入图片描述

(4)普里姆(prim)算法 :以选顶点为主
给定G= (V,E)是一个无向连通网
对n个顶点的连通网,初始时,T= 二元组(U,TE),
初始时U中只有一个开始顶点uO,TE=空集,
在所有边的集合中间,找一条最小权值的边,记作UV
将顶点V加入到集合U中,边UV加入到T1中间

根据MST性质,每次增加一个顶点和一条边,重复n-1次。
U不断增大,有n个顶点,T中有n-1条边,V一U不断减小直到为空。
即可得最小生成树

例如:在这里插入图片描述

例:从顶点A出发(这里A就是uO)
在这里插入图片描述

由于可能存在多条具有最小权值的边,选择的次序不同,可能得到
不同形态的最小生成树
在这里插入图片描述

假定以邻接矩阵表示法作为物理存储结构,依据求普里姆(prim)算法解时
需要1个大小为n的结构数组closedge,来辅助完成
记录顶点集合V-U中每个顶点到U中间顶点的最小权值的边,以及在U中依附
的顶点

//closedge定义
struct
{
	VertexType adjvex;//最小权值的边在u中依附的顶点 
	VRType lowcost;//记录最小权值 
}closedge[MAX_VERTEXT_NUM]; 

//普里姆(prim)算法,算法适合边稠密的无向连通网,T(n,e)=o(n^2) 
void Prim(M Gragh G,VertexType u)
{
	k = LocateVex(G,u);//确定起始顶点u的位置序号 
	for (int j=0;j<G.vexnum;j++) //初始化最短边权值和依附顶点值 
	{
		if (j!=0)
		{
			closedge[j] = {u,G.arcs[k][j].adj};
			closedge[k].lowcost = 0;//选定起点u到U中 
		}

		for (int i=1;i<G.vexnum;i++)//依次加入n-1个顶点、n-1条边 
		{
			k = minimum(closedge);//现在下一个最短边对应的顶点序号
			printf (closedge[k].adjvex,G.vexs[k])//输出生成树边
			closedge[k].lowcost = 0;//顶点k加到U中 
		}
		for (j=0;j<G.vexnum;j++)
		{
			if (G.arcs[k][j].adj < closedge[j].lowcost)
				closedge[j]={G.vexs[k],G.arcs[k][j].adj};//替换最小权值和依附顶点 
		} 
	} 
}

(5)克鲁斯卡尔算法:以选边为主,需要将边按递增次序排列以供选择
以邻接表作为物理存储结构,再使用排序算法
算法适合边稀疏的无向连通网,T(n,e)=O(e log e)

二、有向无环图及应用

1.有向无环图及应用

定义:一个无环的有向图称为有向无环图(directed acyclline graph),简称DAG图
在这里插入图片描述

如图:图1、图2为DAG图,图3有回路,不是DAG图

2.拓扑排序

(1)定义:由一个集合的偏序得到一个全序的过程(偏序、全序的概念在离散数学中)
在这里插入图片描述

可用图直观呈现,顶点表示课程,弧表示它们之间的先决条件
在这里插入图片描述

课程1是课程2的先决条件

(2)AOV网(Activity On Vertex network):以顶点表示活动,弧表示活动之间的优先关系
继续讨论:假定一个时间段只能学习一门课程,如何安排学习计划才能保证在学习任何一门课程
的时候,它的先修课程都事先学习完了

为解决上述问题,可给出所有课程的一个拓扑排序,可得学习计划

(3)拓扑排序算法思想
重复下列操作,直到所有顶点输出完。
①在有向图中选一个没有前驱的顶点输出(也就是选择入度为0的顶点)
②从图中删除该顶点和所有以它为弧尾的弧(并相应修改其它顶点的入度)

有回路的有向图不存在拓扑排序:如果没有输出所有的顶点,则表示这个有向图中间有回路
如图,只能输出V1,V3

在这里插入图片描述

(4)举例:
在这里插入图片描述

按照上述操作,能输出的是V1,V6
①这里我们选择输出V6,删除以它为弧尾的弧
②再输出V1,删除以它为弧尾的弧
在这里插入图片描述

③此时入度为0的顶点为V3,V4,此时我们选择V4输出,再输出V3
④继续按算法步骤处理,直到得到输出序列为
V6 V1 V4 V3 V2 V5

(5)拓扑排序算法代码(物理结构是邻接表)

//拓扑排序
Status ToplogicalSort (ALGraph G)
{
	CountInDegree (G,indegree);//统计顶点入度到indegree[0...Gvexnum-1]
	InitStack(S);//初始化栈 
	count = 0; //初始化访问顶点计数
	
	for (i=0;i<G.vexnum;i++)//入度为0的顶点序号进栈
	{
		if (!(--indegree[j]))//入度减一后为0,进栈 
			push(S,j);
	}
	while (!StackEmpty(S))
	{
		pop(S,i);
		printf (G.vertices[i].data);
		count++;
		
		for (p=G.vertices[i].firstarc;p;p=p->nextarc)
		{
			j=p->adjvex;//取弧头顶点序号赋值给j
			if (!(--indegree[j]))
				push(s,j);//入度减一后为0,进栈 
		}
	} 
	if (count==G.vwxnum)
		return OK;//完成拓扑排序
		else
			return ERROR;//有回路 
} 


3.关键路径

(1)AOE(Activity On Edge) 网:一个带权的有向无环(DAG)图,顶点表示事件,弧表示活动
权表示活动持续的时间

当AOE网用来估算工程的完成时间时,只有一个开始点(入度为0,称为源点)
和一个完成点(出度为0,称为汇点)。

在这里插入图片描述

v1为工程开始点,v9为结束点

AOE网研究的问题:
(1)完成整项工程至少需要多少时间?
(2)哪些活动是影响工程进度的关键?
在AOE网中,部分活动可并行进行,所以完成工程的最短时间
是从开始点到完成点的最长路径长度(最长指路径上的权值之和具有最大值)。

(2)一些定义
①关键路径(Critical Path):路径长度最长的路径
②关键活动:构成关键路径上的所有活动
③活动最早开始时间: e(k)
④活动最迟开始时间:l(k)
⑤活动余量:1(k)-e(k)
表示一个活动的进行可以有对应于这个活动的活动余量的延时,并且不影响整个工程的工期
⑥关键活动:选取活动余量为0的活动,关键路径上的活动都是关键活动
⑦活动持续时间:dut(<i,j>)
为求解活动最早、最迟开始时间,需求解各顶点事件的最早、最迟开始时间
⑧顶点V的最早发生时间ve(j):是源点V1到Vj的最长路径。

(3)顶点V的最早发生时间ve(j)的计算方法:
①ve(Vi)=0
②递推公式: ve(j)=MAX{ ve(i) + dut(<i,j>)}j=2,3,…,n;
<i,j>代表图中所有以j作为弧头的弧,即引入到顶点j的弧,dut(<i,j>)表示活动持续时间。
在这里插入图片描述

	仅有一个前驱顶点:
		ve(V2)=ve(Vi)+6=0+6=6
		ve(Va)=ve(Vi)+4=0+6=4
		ve(V4)=ve(Vi)+6=0+5=5
	有多个前驱顶点:
		ve(Vs)=max{ve(前驱顶点)+前驱活动时间}=max{6+1,4+1}=7

(4)顶点V的最迟发生时间vl(j)
不推迟整个工程完成的前提下。汇点vn的最迟发生时间为vl(n)=ve(n)。
所有顶点Vi的最迟发生时间:
vl(i)=MIN{ vl(j) - dut(<i,j>)},i=n-1,n-2,…,1

在这里插入图片描述

v1(Vg)= ve(V9)=18
仅有一个后继顶点:
v1(V7)= v1(V9)-2=16
vl(Vs)= vl(Vg)-4=14
vl(Vo)= vl(V8)-4=10
有多个后继顶点:
v1(V5)= min{v1(V7)-9, v1(V8)-7,v1(V9)-10}=min{7,7,8}=7

关键活动:选取e(i)=1(i)的活动。
关键路径:
(1)V1→V2→Vs→V7→V9
(2)V1→V2→V5→V8→V9

(5)方法:对DAG图进行拓扑排序,按所得顶点顺序计算得到各顶点最早发生时间,
按所得顶点顺序相反的顺序计算得到各顶点最迟发生时间

三、最短路径

1.举例引入

假设有一城市交通网,从A城市到B城市,考虑出行方案。考虑因素有多种,其中之一
是希望中转的城市最少,则实现该需求,即从A开始实行广度优先搜索到B城市
此时所得的树中,从根结点A到B的路径即为计划方案。但有时考虑的是,尽可能降低成本,
此时对应有向网或无向网中路径权值之和,用有向网表示一个交通网,而后在有向网中讨论最短路径问题

2.从某个源点到其余各顶点的最短路径

在这里插入图片描述
可列举出从v0到其他各顶点的最短路径及长度
在这里插入图片描述
问:该如何求解最短路径呢?

3.Dijkstra路径长度递增法(采用邻接矩阵物理存储结构)

在这里插入图片描述

(1)需要几个大小为n的一维数组
①最短路径长度数组D:其中每个分量Di记录源点v0到顶点vi的最短路径长度值
②前驱顶点数组P:每个分量Pi记录在当前得到的,从源点v0到顶点vi的最短路径中,vi的直接前驱顶点
③数组Final:每个分量Fi表示源点v0到顶点vi的最短路径是否已经确定,确定则fi为true,否则为false
在这里插入图片描述
(2)(最短路径)算法基本思想
①初始化:将邻接矩阵中对应源点v0的这一行赋值到D中,
此时,若v0到vi有弧,则Di为弧上的权值,否则表示为无穷大
长度为D[j]=min{D[i]]|Vi属于V}的路径,为由vo出发的最短路径
②前驱结点P中各元素设置为v0,表示目前路径均从v0出发,即D中存放的是v0到各顶点的长度
③Final初始化为false,表示均为找到最短路径,f0修改为true,因为v0是源点,不必考虑最短路径
(3)求最短路径
在这里插入图片描述

假设S为已经求得最短路径的终点的顶点集合,假定下一条最短路径的终点是Vk,
可用反证法证明,该最短路径要么是v0到vk,要么中途只经过s中的顶点
由此可按递增次序得到vo到其他各顶点的最短路径
在这里插入图片描述

至此,按以上方法,可确定最短路径
在这里插入图片描述

4.每一对顶点之间的最短路径

(1)算法1:Dijkstra算法:
算法思想:
以每一个顶点为源点,重复执行Dijkstra算法n次,即可求出每一对顶点之间的最短路径。
时间复杂度:T(n)=o(n^3)

(2)算法2:弗洛伊德(Floyd)算法:
以邻接矩阵为基础的一种形式简单的求每一对顶点之间最短路径的算法。
时间复杂度:T(n)=O(n3)算法思想:
假设求Vi到Vj的最短路径,如果从Vi到Vj有弧,则存在一条长度为arcs[i][i]的路径,
该路径不一定是最短路径,尚需进行n次试探。

	步骤:
	 ①首先考虑(Vi,Vo,Vj)是否存在(即判断(Vi,Vo)和(Vo,Vj)是否存在),
	 如果存在,比较(Vi,Vj)和(Vi,Vo)+( Vo,Vj),取长度较短的为从Vi到Vj的中间顶点序号不大于0的路径长度。
		![在这里插入图片描述](https://img-blog.csdnimg.cn/20210607203414440.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzUxMzA4Mzcz,size_16,color_FFFFFF,t_70#pic_center)

②再考虑路径上再增加一个顶点V1,如果考虑(Vi,…….Vi)和(V1,…Vj)
都是中间顶点序号不大于0的最短路径。
那么,(Vi,…V1,…Vj)可能是从Vi到Vj的中间顶点序号不大于1的最短路径。
在这里插入图片描述

比较(Vi,…V1)+(V1,…Vj)和Vi到Vj的中间顶点序号不大于0的最短路径,
取长度较短的为从Vi到Vj的中间顶点序号不大于1的最短路径。
③接着使用相同方法,继续试探顶点v2、v3等
④(Vi,Vj ) =min { ( Vi,Vk )+ ( Vk,Vj) , ( Vi,Vj)}为顶点Vi到Vj的中间顶点不大于k的最短路径

	假定邻接矩阵为cost[N][N]
		Floyd算法的基本思想是递推产生一个矩阵序列:
			D(-1),D(0),..,D(k),...D(n-1)
			D(-1)[i][j]= G.arcs[i][j]
			D(k)[i][j] = Min {D(k-1)[i][j], D(k-1)[i][k]+D(k-1)[k][j]
							0<=k<=n-1	n=G.vexnum
							
	(需定义2个n*n的二维数组)
		①D:完成最短路径的计算 
		②P:记录Vi到Vj中,中间的结点
//Floyd算法
void ShortPath2(MGraph G,PathMatrix &P,ShortPath &D)
{
	for (int i=0;i<G.vexnum;i++)
	{
		for (j=0;j<G.vexnum;j++)//初始化
		{
			P[i][j] = -1;//-1表示无中间顶点 
			D[i][j] = G.arcs[i][j];
		} 
	}
	for (int j=0;k<G.vexnum;k++)//依次选定中间顶点V0,V1,...,Vn-1
	{
		for (int i=0;i<G.vexnum;i++)//i,j配合处理所有顶点Vi,Vj
		{
			for (int j=0;j<G.vexnum;j++)
			{
				if (D[i][j]>D[i][k]+D[k][j])
				{
					D[i][j] = D[i][k]+D[k][j];//取较短路径
					P[i][j] = k;//Vi到Vj的中间顶点Vk 
				}
			}
		} 
	} 
} 
	

5.弗洛伊德(Floyd)算法举例

(1)首先给出一个有向网及其对应邻接矩阵
在这里插入图片描述

(2)初始化得到D(-1),P(-1)
在这里插入图片描述

(3)依次测试各结点
首先考虑V0作为中间结点,发现经过v0,V2到V1的路径更短
故在D中修正V2到V1的路径长度值,同时设置P21为0,表明V2到V1的最短路径上要经过顶点V0
在这里插入图片描述

再考虑V1作为中间结点,依次类推,继续修正
在这里插入图片描述

最后考虑V2作为中间结点,继续修正
在这里插入图片描述

此时所得D2表示各顶点间最短路径长度值,P2表示中间结点
在这里插入图片描述

如D(02)=6,V0到V2最短路径长度为6,P(02)=1,表明有中间结点v1

总结

本篇介绍的关键路径及最短路径的两种算法,在图中非常重要。后序许多竞赛类题目或是引发题都以此作为核心思想。最短路径的求解算法中,迪杰斯特拉和弗洛伊德算法最为典型。需要着重理解,最好能自己手动过一遍代码。

下篇将讲解经典查找问题,以及查找解法。

  • 13
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论
以下是C语言实现Floyd算法求解无向最短路径及其长度的示例代码: ```c #include <stdio.h> #define INF 0x3f3f3f3f // 无穷大 int main() { int n, m; scanf("%d %d", &n, &m); // 输入结点数和边数 int dist[n+1][n+1]; // 存储结点间距离 memset(dist, INF, sizeof(dist)); // 初始化为无穷大 for(int i = 1; i <= n; i++) dist[i][i] = 0; // 自身到自身的距离为0 int u, v, w; for(int i = 1; i <= m; i++) { scanf("%d %d %d", &u, &v, &w); // 输入边的起点、终点和权值 dist[u][v] = w; dist[v][u] = w; // 无向需要双向存储 } // Floyd算法求解最短路径 for(int k = 1; k <= n; k++) for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) if(dist[i][j] > dist[i][k] + dist[k][j]) dist[i][j] = dist[i][k] + dist[k][j]; // 输出最短路径及其长度 for(int i = 1; i <= n; i++) for(int j = i+1; j <= n; j++) printf("%d 到 %d 的最短路径为 %d,路径长度为 %d\n", i, j, j, dist[i][j]); return 0; } ``` 在输入时,需要按照以下格式输入: - 第一行为结点数n和边数m,用空格分隔; - 接下来m行每行为一条边的起点u、终点v和权值w,用空格分隔。 例如,以下是一个包含5个结点和7条边的无向的输入示例: ``` 5 7 1 2 2 1 3 4 2 3 1 2 4 7 3 4 3 3 5 6 4 5 5 ``` 输出结果如下: ``` 1 到 2 的最短路径为 2,路径长度为 2 1 到 3 的最短路径为 3,路径长度为 4 1 到 4 的最短路径为 3,路径长度为 10 1 到 5 的最短路径为 4,路径长度为 9 2 到 3 的最短路径为 3,路径长度为 1 2 到 4 的最短路径为 4,路径长度为 8 2 到 5 的最短路径为 3,路径长度为 7 3 到 4 的最短路径为 3,路径长度为 3 3 到 5 的最短路径为 3,路径长度为 6 4 到 5 的最短路径为 5,路径长度为 5 ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬茶@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值