数据结构 6-0 图

图的基本概念

图可以看作由点和边组成的集合,其中点集一定非空,但边集不一定非空,点的个数用图的阶来表示。

一些没什么太大难理解地方的概念就直接放在下面了
在这里插入图片描述
在这里插入图片描述
有向图和无向图是图中最常考的两个载体,主要区别就是这个方向问题,根据边是否是有向的,区别了有向图和无向图。
在这里插入图片描述
简单图和多重图是一对相对的概念,区别在于一对点中是否有多条边,一般考研题中不考察多重图。
在这里插入图片描述
即无向图中任意两个点之间都有边,有向图中任意两个点都有来回的两条边。
在这里插入图片描述
在这里插入图片描述
子图概念中一定要看好,不是单纯的取子集就可以凑出来子图,单纯取子集也可能搞出来一个非图的结构。
在这里插入图片描述
连通是一个很重要的概念,连通即可达,这个概念多用在无向图中,一个点通过任何路径可以到达另一个点就是连通,如果一个图所有点都是连通的,图就为连通图。极大连通子图为连通分量,最形象的表示就是在画好的图中,可以把两个连通分量完全拆开成图的两部分。另外,这里提到的连通图点和边数的关系,常作为构建最小非连通图的方法。
在这里插入图片描述
在这里插入图片描述
作为连通的对应,有向图中多考虑强连通性,即一个点可以到另一个点并且可以再从这个点再返回出发点,参考连通的定义,可以类似去理解强连通分量等概念。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

图的四种存储方式

主流的表示方法是邻接矩阵和邻接表法,也有不是很常用的十字链表和临接多重表。

一、邻接矩阵
邻接矩阵一般学过离散数学的都理解的很透彻,无向图中有边对应位置即为1,否则为0,有向图中则是根据起点终点来确定哪个位置是1,哪个位置是0。如果是带权的图,也可以将1换为边的权重。

实际实现起来,一般用二维数组来实现。由于二维数组是点来标记的,所以当图里面边比较少的时候,矩阵里面大多数元素都是初始化时的0,利用率很低,所以一般用邻接矩阵存储稠密图。邻接矩阵表示法的空间复杂度为 O(n2)。

使用邻接矩阵有如下特点:

二、邻接表法
就好比数组和链表,邻接表法优点在于灵活,根据需求分配空间,所以适合存储稀疏图。

邻接表实际上是由两部分组成的,顶点表和边表,顶点表用来记录顶点,边表连在顶点后面,表示这个点的边的情况。顶点表结点由顶点域和指向第一条邻按边的指针构成,边表结点由邻接点域和指向下一条邻接边的指针域构成。 一般顶点表采用顺序存储,边表则换用链式存储。

一定要会根据题意画邻接表,邻接表在画的时候,需要注意顶点直接的连接和边之间的连接。记好一个例子即可。
在这里插入图片描述
另外,在代码题里面很喜欢用邻接表存储,邻接表遍历的模板如下:

for(int i=1;i<=n;i++) 
{
	now=head[i];
	for(int j=head[i]->next;j!=NULL;j=j->next)
	{
		
	}
}

使用邻接表有下面的特点:
在这里插入图片描述
三、十字链表
十字链表是有向图的一种链式存储结构。 在十字链表中,对应于有向图中的每条弧有一个结点,对应于每个顶点也有一个结点。在十字链表中,既容易找到只为尾的弧,又容易找到只为头的弧,因而容易求得顶点的出度 和入度。 图的十字链表表示是不唯一的,但一个十字链表表示确定一个图。

在这里插入图片描述
简单说明一下,弧结点用来记录边,其中尾域和头域表示这条边的两个终点和起点,hlink指向的是同一个起点的下一条边,tlink指向的是同一个终点的下一条边,info用于存储这条边的相关信息,比如权重之类的。顶点结点中只有三部分,data存储数据,firstin指向以这个顶点为起点的第一条边,firstout指向以这个为重点的第一条边。

在这里插入图片描述
四、邻接多重表
邻接多重表是无向图的另一种链式存储结构。 与十字链表类似, 在邻接多重表中,每条边用一个结点表示。
在这里插入图片描述
其中,mark表示标记,用于区分这条边是否已经经历过,ivex和jvex分别表示这条边的起点和终点,ilink表示同一起点的下一条边,jlink表示同一个终点的下一条边,info用于存储相关信息比如权重之类的。
每个顶点也用一个结点表示:
在这里插入图片描述
一共就只有两个量,存储数据和以该节点为顶点的第一条边。

在邻接多重表中,所有依附于同一顶点的边串联在同一链表中 ,由于每条边依附于两个顶点, 因此每个边结点同时链接在两个链表中 。 对无向图而言,其邻接多重表和邻接表的差别仅在于, 同一条边在邻接表中用两个结点表示,而在邻接多重表中只有一个结点。
在这里插入图片描述

图的遍历

图的遍历这里主要是讲了蓝桥必考的两种搜索方式:广度优先和深度优先。

广度优先搜索类似于二叉树的层序遍历算法,基本思想是从一个点出发,把它周围所有未访问的点入队,相当于产生了一个包围圈把这个起点给包起来了,再从队列里面取点,以此将所有周围未访问的点入队,看起来就像是一层一层进行扩展。广度优先搜索遍历图的过程是以 v 为起始点,由近至远依次访问和 ν 有路径相通且路径长度为 1, 2, …的顶点。
广度优先搜索是一种分层的查找过程,每向前走一步可能访问一 批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。 为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。

广搜的过程中需要一个数组来记录是否已经访问过了,同时需要一个存放节点的队列,所有节点都会入队一次,最差情况下空间复杂度为O(|V|)。采用邻接表存储方式时,每个顶点均需搜索一次(或入队一次), 故时间复杂度为O(|V|),在搜索任一顶点的邻接点时,每条边至少访问一次,故时间复杂度为 0(|E|),算法总的时间复杂度 为 0(|V| +IEI)。 采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为 O(|V|), 故算法总 的时间复杂度为O(|V|2)。

在广度遍历的过程中,我们可以得到一棵遍历树, 称为广度优先生成树。需要注意的是, 一给定图的邻接矩阵存储表示是唯一的,故其广度优先生成树也是唯一的,但由于邻接表存储表示不唯一,故其广度优先生 成树也是不唯一的。即邻接矩阵存储的图的广度优先生成树是唯一的,邻接表存储的图的广度优先搜索树是不唯一的。
在这里插入图片描述

广搜的代码模板如下:

void bfs()
{
	初试状态入队列
	while(!q.empty())
	{
		p=q.front();
		q.pop();
		for(当前状态的所有可达状态)
		{
			if(未访问)
			{
				q.push(未访问状态);
				visit[未访问状态]=1;
			}
		}	
	}
 } 

深度优先搜索类似于树的先序遍历,说白了就是暴力,一条路走到黑。它的基本思想是从一个点出发,移动到未访问的邻接点,移动后再继续访问邻接点的未访问的邻接点,访问完了就返回上一层继续访问其它未访问的邻接点,重复这个动作直到没有点可以访问为止。

根据DFS的特点,一般使用递归来实现代码:

void dfs(int x,int y)
{
	for(所有可达的点)
	{
		int tx=x+dir[i][0];
		int ty=y+dir[i][1];
		移动到新的点
		if(可访问且未访问)
		{
			vis[tx][ty]=1;
			dfs(tx,ty);
		} 
	}
}

同样地,对于同样一个图,基于邻接矩阵的遍历所得到的 DFS 序列和 BFS 序列是唯一的, 基于邻接表的遍历所得到的 DFS 序列和 BFS 序列是不唯一的。
在这里插入图片描述

DFS 算法是一个递归算法, 需要借助一个递归工作栈,故其空间复杂度为O(|V|) 。 遍历图的过程实质上是对每个点查找其邻接点的过程,其耗费的时间取决于所用的存储结构。以邻接矩阵表示时,总的时间复杂度为 O(|V|2)。 以邻接表表示时,总的时间复杂度为O(|V|+|E|)。

与广度优先搜索一样, 深度优先搜索也会 产生深度优先搜索树。当然, 这是有条件的,即对连通图调用 DFS 才能产生深度优先生成树, 否则产生的将是深度优先生成森林。
在这里插入图片描述
根据DFS的特点,不难发现,每次DFS的过程,都是将一个图的一个连通分量完全访问了一次,基于这一点可以用图来实现连通分量的判断,如果一次DFS就可以遍历图中的每一个节点,说明图就只有一个连通分量,即图为连通图,否则就是非连通图。

图的应用

应用这部分主要涉及五个知识,最小生成树、最短路径、有向无环图、拓扑排序、关键路径。

一、最小生成树
简而言之,最小生成树就是一个图所有生成树中权值最小的那个,可以不唯一,当所有边权值都不相等的时候最小生成树唯一。虽然最小生成树有时候不唯一,但权值之和唯一。

最小生成树的考点主要在于手工模拟两个求最小生成树的算法:普里姆算法和克鲁斯卡尔算法。

普里姆算法类似于迪杰斯特拉算法,主要思想是不断向已确定的点的集合中加入离集合最近的点。初试时选择一个点加入集合,之后看这个集合可以到达的点中哪一个距离最近,选择最近的点加入集合,不断重复这个过程直到所有点都加入了集合。
在这里插入图片描述
Prim 算法的时间复杂度为O(|V|2),因此它适用于求解边稠密的最小生成树。 虽然采用其他方法能改进 Prim 算法的时间复杂度,但增加了实现的复杂性。主要是会手写这个加入边选择点的过程。

克鲁斯卡尔算法则是一种从边下手的方法,初试状态只有点没有边,每次选择一条权值最小的边加入图中,如果没有构成回路就在图中保留这条边,重复这个过程直到图变成连通图。
在这里插入图片描述
克鲁斯卡尔算法时间复杂度为O(|E|log|E|),适合边稀疏而点较多的图。

二、最短路径问题
当图是带权图时,把从一个 顶点 Vo 到图中其余任何一个顶点 V的一条路径(可能不止一条)所经过边上的权值之和,定义为 该路径的带权路径长度, 把带权路径长度最短的那条路径称为最短路径。一般最短路径问题分为两类,单源最短路径(一个点到其余各个点)和每对顶点之间的最短路径,分别对应迪杰斯特拉算法和佛罗伊德算法。

迪杰斯特拉算法在计算机网络求OSPF协议时也用到了,算法在运行过程中,需要两个数组来记录过程,dist用于记录当前到原点的最短距离,path则记录前驱,方便在算法结束时求出整个路径。算法的第一步是初始化,起点如果是V0,那么就将V0可达的点在dist数组中初始化为对应的距离,不可达的点就初始化为无穷,每次选择dist数组中最短的边对应的点加入集合,再将dist数组进行更新,如果经过新加入的点再到达另一个点距离小于从原点直接到另一个点的距离,那么就将dist更新为新的最小距离,重复这个操作,直到全部点都进入了集合。

迪杰斯特拉算法最重要的点在于会画整个过程的图示,包括dist数组的更新,边的选择过程。
在这里插入图片描述
迪杰斯特拉算法是基于贪心思想的,每次选择的都是当前的最优解,无论是用邻接表还是用邻接矩阵存储,迪杰斯特拉算法算法的时间复杂度都是O(|V|2)。需要注意的是,当边的权值为负数是不能使用迪杰斯特拉算法的。

弗洛伊德(黑人的命也是命)算法是另一种求最短路径问题的算法, 相比于迪杰斯特拉算法,佛罗伊德算法更高级同样理解起来也比较抽象。
在这里插入图片描述
简单来说就是从初始状态不断迭代,初始状态为图的邻接矩阵,这时表示的是两个点之间直接相连,不经过其他点中转,后序迭代就是不断进行中转,如果中转的距离更短,那么就取较短距离作为真正的距离。
在这里插入图片描述
Floyd 算法的时间复杂度为 O(|V|3)。 不过由于其代码很紧凑,且并不包含其他复杂的数据结构, 因此隐含的常数系数是很小的, 即使对于中等规模的输入来说,它仍然是相当有效的。 弗洛伊德算法可以解决边的权值为负数的情况,但是仍然不能处理有回路的情况。

三、无环有向图的表示
这一部分没什么难点,就是利用有向无环图来实现重复子式的共享,进而减少存储空间。
在这里插入图片描述
四、拓扑排序
在这里插入图片描述
简单来说就是把一个图看成完成一个事情的流程,这个流程包括了很多活动,拓扑排序就是找出一个顺序,可以让这些活动根据这个顺序完成而没有冲突。拓扑排序并不唯一,重要的是确定拓扑排序的算法。同时一些情况下会用到逆拓扑排序,只要将下面拓扑排序的过程反过来就可以了。
在这里插入图片描述
五、关键路径
关键路径是这五个应用中最麻烦的一个,需要计算不少数据,但是过程并不难。
在带权有向图中, 以顶点表示事件,以有向边表示活动,以边的权值表示完成该活动的开销(完成活动所需的时间),称之为用边表示活动的网络,简称 AOE 网 。 AOE 网和 AOV 网都 是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的, AOE 网中的边有权值;而 AOV 网中的边无权值,仅表示顶点之间的前后关系。 其中入度为0的点为起点也称源点,出度为0的点为终点也称汇点。AOE网图中活动是可以并行进行的,只有所有活动全部完成才可以算是工程全部完成。完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和。 这是因为关键活动影响了整个工程的时间,即若关键活动不能按时完成, 则整个工程的完成时间 就会延长。因此,只要找到了关键活动,就找到了关键路径,也就可以得出最短完成时间。

求关键路径时主要计算四个量,事件的最早、最迟发生时间,活动的最早最迟发生时间。实际计算时,先画出AOE图理顺好先后关系,之后从起点开始,先算事件的最早发生时间,源点的最早发生时间为0,之后可以借助拓扑排序,看每个节点的所有前驱,找前驱最早发生时间加活动时间最长的作为自己的最早发生时间,完成所有事件的最早发生时间,再计算最迟发生时间,倒着从汇点开始,取所有后继事件的最迟发生时间减去活动时间的最小值。之后再计算活动的最早、最迟开始时间,活动的最早开始时间就是弧起点所代表的事件的最早开始时间,最迟开始时间则是弧终点所代表的事件与该活动用时的差值,活动的最早和最迟开始时间的差值代表的就是这个活动的灵活时间,如果二者相等,说明这个活动不能等,如果等了就会推迟后面的活动,即关键活动
在这里插入图片描述
在这里插入图片描述
关键路径上的活动都是关键活动,可以缩减这些活动的时间来减小总用时,但是不能减少太多,如果减少太多可能会让关键路径改变,进而改变关键活动。AOE网中的关键路径并不唯一,且对于有几条关键路径的网,只提高一条关键路径上的关键 活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的,即关键路径的交集。

典型题

在这里插入图片描述
题目关键在于那句在任何情况下,区别于最少情况,最少情况只需要6条边就可以完成,但是不是任何情况,这里采用的方法是先取6个点,6个点的完全图需要5+4+3+2+1=15条边,此时再把第七个点补上去,加一条边变成16条边,那么一定是连通的。
在这里插入图片描述
无向图中边数目的两倍等于各顶点度数的和,所以这个题中,度数之和为162=32,减去43+34+2x+1*y,题目问至少有多少个顶点,所以y=0,此时可以解得x=4,总节点数为4+4+3=11个。同理这个题如果问最多有多少个顶点,那么就是x=0,y=8,总结点数为4+3+8=15个。
在这里插入图片描述
题目有点摸不到头脑。首先有n个节点的树有n-1条边,假设有x棵树,那么将森林连成一棵树就会增加x-1条边,那么可以列出等式e+(x-1)+1=n,故x=n-e。
在这里插入图片描述
当我看到这个题时是蒙逼的,这不是考线性代数矩阵乘法嘛,题目确实有点抽象,也不是很清楚怎么解的,个人感觉完全是靠找规律,先写出一个矩阵的低次方的情况,看具体的值来猜元素值得含义。
在这里插入图片描述
在这里插入图片描述
使用深度优先遍历,若从有向图上的某个顶点 u出发,在 DFS(u)结束之前出现一条从顶点 v 到 u 的边,由于 v 在生成树上是 u 的子孙,则因中必定存在包含 u 和 v的环,因此深度优先搜索可以检测出一个有向图是否有环。
拓扑排序时, 当某顶点不为任何边的头时才能加入序列 , 存在环路时环路中的顶点一直是某条边的头, 不能加入拓扑序列。 也就是说,还存在无法找到下 一个可以加入拓扑序列的顶点,则说明此图存在回路。
最短路径本身允许有环,所以无法用于判断是否有回路。
关键路径有点牵强,在找关键路径时,一般需要用到拓扑排序得到一个序列,按照这个序列来计算最早最迟时间,所以可以判断是否有回路。
在这里插入图片描述
顶点不在拓扑序列,说明这些顶点一直都是某条边的头,这些点放在一起单独构成回路,而这些回路刚好是一个强连通分量。
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ayakanoinu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值