第五章:图(图的应用01)
1.最小生成树
生成树:连通图包含全部顶点的一个极小连通子图
这里需要注意的是是一个极小连通子图
![379ce3c01551fd83b6dd9840a913d17e.png](https://i-blog.csdnimg.cn/blog_migrate/20d5eb0576c2a226649029b492161bc6.jpeg)
上面第一个是一个连通图,右侧两个图是它的生成树,它们包含了全部顶点且是极小连通子图,如果我们把图2中A和D链接起来他就不是一个生成树了,因为他不是极小的,如果我们把图3中的C和D中的边去掉,他也不是生成树了,因为他不是连通的。
![62fba0cc86ec254fb26cbf5461967046.png](https://i-blog.csdnimg.cn/blog_migrate/5ab677882695584b233f7c83c28ef6c0.jpeg)
如上图,我们把图的边加上权值他就叫做网,我们找出它的两个上生成树,其中图2的所有边的权值的和为:2+2+5+6=15,图3的所有边的权值之和是:1+2+4+5=12,可以看出图3的权值之和最小,则图3就是最小生成树。
最小生成树:对于带权的无向连通图G=(V,E),G的所有生成树当中边的权值之和最小的生成树为G的最小生成树(MST)
注意是带权的无向连通图,因为带权了才有最小的概念,只有无向连通图才有生成树
1.1性质
不一定唯一:最小生成树一定只有一种吗?
![3b10c02813b704ffab44b0597cc1b8c8.png](https://i-blog.csdnimg.cn/blog_migrate/8dd077d44ded02fe84db1a490b058d4b.jpeg)
如上图中的图2和图3他们都是最小生成树,所以最小生成树不一定唯一。
这是不一定,所有也有唯一的情况,比如:
- 所有边的权重皆不相同(没有权重相同的边最小生成树必为一,可以自行尝试)
- 图中n个顶点只有n-1条边(这样的无向图【网】它的最小生成树只有一个就是他自己,所以唯一)
性质
- 最小生成树不一定唯一,即最小生成树的树形不一定唯一。当带权无向连通图G的各边权值不等时或G只有结点数减1条边时,MST唯一
- 最小生成树的权值是唯一的,且是最小
- 最小生成树的边数为顶点数减1
1.2算法
我们根据贪心算法设计了如下最小生成树算法(贪心:每一步尽量做出最好的选择)
//伪代码GENRIC_MST(G){ T=NULL; while T //未形成一棵生成树 do //找到一条最小代价边(u,v)并且加入T后不会产生回路 T=Tu(u,v);}
两种最小生成树算法:Prim、Kruskal
1.3Prim
初始化:向空的结果树T=(Vt,Et)中添加图G=(V,E)的任一顶点u0,使Vt={u0},Et为空集;
循环(直到Vt=V):从图G中选择满足{(u,v)|u∈Vt,v∈V-Vt}且具有最小权值的边(u,v),并置Vt=VtU{v},Et=EtU{(u,v)}
{(u,v)|u∈Vt,v∈V-Vt} 相当于将已经添加到结果集中的结点和未添加进来的结点进行连接,这种方法就避免了生成的生成树出现回路
且具有最小权值的边:这个就是为了我们的最终目的啦,生成树的权值之和最小,即最小生成树
Vt=VtU{v},Et=EtU{(u,v)} 将新添加的顶点添加到结果的顶点集中,边添加到结果的边集中
![15857a2ce85c802f0114f66363cf1c71.png](https://i-blog.csdnimg.cn/blog_migrate/127f34323a6a8234e2a3f79b81430087.jpeg)
使用Prim算法生成最小生成树的过程:首先假设我们选择A作为初始顶点,然后我们从剩下的:B、C、E、D中选择一个顶点组成一条边,满足这样的边有三个:AB、AC、AE,并且我们需要选择该权值最小的那个,可以发现是AC边
![04530030a752160779be5f7ad4919346.png](https://i-blog.csdnimg.cn/blog_migrate/964428c913f372f1e31f25f759fea013.jpeg)
接着我们继续找,此时可以选择的顶点有三个B、E、D,可以组成的边有:AB、AE、CB、CE、CD,我们任然选择权值最小的那个CB边
![25ecd3bd328e024117ddecdb418cba0a.png](https://i-blog.csdnimg.cn/blog_migrate/ea7f1c5271f01e56e366756bca7e6e3f.jpeg)
我们继续找,此时可以选择的顶点有D和E,可以组成的边有:AE、CE、CD(注意这里没有AB,因为AB边会形成通路,当然我们可选择的顶点也没有B),任然选择权值最小的那个AE
![3626f46c521b404074e93f595b286e44.png](https://i-blog.csdnimg.cn/blog_migrate/8463bf195797bdb37f3ab5b42d276b86.jpeg)
接着我们继续找,此时可以选择顶点只有D,可以组成的边只有CD
![672d5339c96cf3f497ef5fcefc5b24da.png](https://i-blog.csdnimg.cn/blog_migrate/b1112fe228d255af086308166bfc9389.jpeg)
此时最小生成树构成成功,Prim算法伪代码,算法思想
void Prim(G,T){ T = 空集;//结果集 U = {w};//将w顶点加入结果的顶点集中 while((V-U)!=空集){ 设(u,v)是使u∈(V-U),且权值最小的边; T=TuP{(u,v)}; U=Uu{V}; }}
我需要两个辅助数组将这个伪代码转成算法代码:min_weight[n],adjvex[n]
- min_weight[n] 它的数组大小是顶点的数量大小,他代表我们每次循环时,我们知道我们的边一个顶点要从结果集中选择另一个顶点要从剩下的顶点中选择,它存放了到每一个顶点:我们已经生成的顶点到我们剩下的顶点的权值最小的那条边的,数组下标是被选择的顶点的下标,例如min_weight[2]代表我们已经挑选的顶点到剩下的顶点之前权重最小的那一条边,下标2则代表结果顶点集中的顶点到剩下的顶点中下标2的顶点。
- adjvex[n] 存放该顶点由那一个顶点引入结果集中的
![15857a2ce85c802f0114f66363cf1c71.png](https://i-blog.csdnimg.cn/blog_migrate/127f34323a6a8234e2a3f79b81430087.jpeg)
我们仍然使用上图作为例子,首先选A作为初始顶点,然后我们要初始化两个数组:
![f256bdd23cf69a51f3adedb1d35b9879.png](https://i-blog.csdnimg.cn/blog_migrate/d1c78bdfc64472667fbcc0971a490bed.jpeg)
这里有一个无穷大的原因是A到D是没有边的,在实际算法中令这个无穷大为一个比较大的整数就可以了
接着我们同样按照上面的过程找和A顶点组成的边的权值最小的点C,接着我们此时要修改min_weight数组了
![0e24efb4dfd4b9e4cd68d61a45944a13.png](https://i-blog.csdnimg.cn/blog_migrate/32f87a10306ac13084ae5cdde3a4401e.jpeg)
我们可以看到将3改成了2,因为在此时结果顶点集中可以存在一条从C到B的边它的权值为2比之前的AB3更小,同样修改∞为5,因为也存一条CE边,它的权值为5,因为CE的权值为6,所以4我们不需要修改,同时我们将1改成了0,因为C顶点已经被取走了,这里其中adjvex数组的值也进行修改了,我们修改adjvex[2]=0(2是c顶点的下标),因为A顶点的下标为0,所以看不出来。
![775b36758f256ba37130b18429ca989b.png](https://i-blog.csdnimg.cn/blog_migrate/469d7ba9cb23d94b5bcef892c32f01b4.jpeg)
同样我们继续挑选权值最小的边BC,同样修改辅助数组此时我们已经将A、B、C三个顶点选走所数组的前三个值为0,因为B顶点是由C顶点挑选走的,所以修改adjvex=[1]=2(1代表B顶点下标,2代表C顶点下标),由于不存在权值更小的边,所以不修改数组的值
![d19ea6f827b91584711a730e20cd23ed.png](https://i-blog.csdnimg.cn/blog_migrate/abf7e8dce679252515a7d930a4df0b18.jpeg)
我们继续寻找顶点,找到AE,同样我们修改此时修改min_weight=[4]=0,同时修改adjvex[4]=0,同样我们继续寻找找到CD边,我们修改min_weight[3]=0,修改adjvex[3]=2
![2349d491d1a207b88154d2beb0ef9381.png](https://i-blog.csdnimg.cn/blog_migrate/b48de9c3b3444b03bc306e7a0937e3b6.jpeg)
最终ming_weight数组中的值全部为0,说明所有顶点都已经被选择
代码实现,时间复杂度O(V^2) ,该算法的时间复杂度和边没有关系,所以他比较适合稠密图
void MST_Prim(Graph G){ //传入图G int min_weight[G.vexnum];//数组大小为顶点的数量 int adjvex[G.vexnum];//数组大小为顶点的数量 for(int i=0; i
1.4Kruskal
初始化:Vt=V,Et=空集,即是每个顶点构成一颗独立的树,T是一个仅含V个顶点的森林;
循环(直到T为树):按图G的边的权值递增的顺序依次从E-Et中选择一条边,若这条边加入后不构成回路,则将其加入Et,否则舍弃。
![08123b25ba5c902cc17f5935ef815371.png](https://i-blog.csdnimg.cn/blog_migrate/3faae66101858627a1de3eea481431a2.jpeg)
首先将这个n个顶点放到结果集,这n个顶点构成了n棵树组成的森林,然后我们将图G的边的权值按照递增的顺序排序,接着从头挑选权值最小的那条边AC发现不构成回路,则可以加入Et中,同样我们继续挑选BC这条边不构成回路可以加入,接着是AB这条边,但是它会构成回路,不加入.......最后形成最小生成树
![5916b813bc3b490514885a6ee1dc318a.png](https://i-blog.csdnimg.cn/blog_migrate/51cfdb32e1e1f5f7162011b0e2a3166b.jpeg)
Kruskal主要使用了堆排序对边的权重进行排序,然后使用并查集(数组)这个数据挑选边。代码实现
代码实现,时间复杂度:O(E*logE),适用于稀疏图
typedef struct Edge{ int a,b;//改变两个端点的下标 int weight;//权重};void MST_Kruskal(Graph G,Edge *edges,int *parent){//图G、属于该图的边的集合、并查集辅助变量 heap_sort(edges);//堆排序 Initial(parent);//初始化并查集 for(int i=0;i
2.最短路径
我们之前学过无权图单源最短路径问题,那是无权图,我们找最短路径只需找出边最少的就行,但是这里我们讨论的是带权的最短路径,此时就不能只看边了。
最短路径:两个顶点之前带权路径长度最短的路径为最短路径
在带权图中,把一个顶点v到另一个顶点u所经历的权值之和称为,路径的带权路径长度
我们有两种算法:Dijkstra 、Floyd
2.1Dijkstra(迪杰斯特拉)
Dijkstra(迪杰斯特拉):计算带权图单源最短路径。我们需要以下几个辅助数组:
- s[] 标记已经计算完成的顶点:数组中的值全部初始化为0(未计算完成),源点下标的值初始化为1(计算完成)
- dist[] 记录从源点v0到其他各顶点当前的最短路径长度:数组中的值初始化为源点到各个顶点边的权值,即dist[i]=arcs[0][i] (我们这里源点是0,所以从第0行取值初始化,这样看源点的变化)
- path[] 记录从最短路径中顶点的前驱顶点,即path[i] 为v到vi最短路径上vi的前驱顶点(最后我们不断的求前驱系结点组成的就是一条最短路径):数组中值的初始化,若源点v0到该顶点vi有一条有向边(无向边),则令path[i]=0,否则path[i]=-1
实现过程:
- 初始化数组,并集合S初始化为{0}(令源点为v0)
- 从顶点集合V-S中选出Vj,满足dist[j]=Min{dist[i],vi∈V-S},Vj就是当前求得的最短路径的终点,并令S=S∪{j}
- 修改此时V0出发到集合V-S上任一顶点Vk的最短路径的长度:若dist[j]+arcs[j][k]
![d0ff587a323c97d5d5a27efd6ad19ffb.png](https://i-blog.csdnimg.cn/blog_migrate/d47ba60be9c398d66274b590c724e7c7.jpeg)
- 重复 2、3操作n-1次,直到S中包含全部顶点
举个栗子:我们描述出下图的Dijstra的算法执行过程
![b20b52f36c8cf099f58614beaa2bde45.png](https://i-blog.csdnimg.cn/blog_migrate/64e2df003ddfa77c4d5fe31953600381.jpeg)
![510ee4a5f6765b8075dc6ff13560c1b4.png](https://i-blog.csdnimg.cn/blog_migrate/aa0bc6091aac38b0893c39b799542e42.jpeg)
首先我们设置源点为0,接着我们初始化三个辅助数组,其中dist[]存储的是0到每一个顶点的权值因为03不存在边,所以存储∞,s[0]=1表示0这个顶点已经计算完成,path[]初始化代表如果存在顶点0到另一个顶点有一条有向边,则设置值为顶点0的下标为0,否则为-1,初始化完成,开始执行具体的执行过程
![8d9a33b32d9795f618d0e520a3f740f2.png](https://i-blog.csdnimg.cn/blog_migrate/c386b432c7aa0ff5c7d20358c1a5c821.jpeg)
![020ecf85a57dfd3ea29c0358baa0b5fd.png](https://i-blog.csdnimg.cn/blog_migrate/8062496bf0aeebd2a20e3b4a3f82628e.jpeg)
第一轮:首先我们挑选dist[]中最小的值,我们知道是3,它对应的顶点为2,所以我们挑选2这个顶点,挑选完成后我们需要修改dist[]数组,挑选2完成之后会带来新的有向边,我知道04的权值是8,但是加入2之后存在024这样的一条有向边它的权值是7,所以我们需要修改dist[4]=7,同时由于2顶点计算过了,所以我们修改s[2]=1,同时由于此时我们是通过2顶点到达的4顶点所以修改paht[4]=2
![81f5ba53904c7a2c653480be17653f96.png](https://i-blog.csdnimg.cn/blog_migrate/4f69175954ef09f8c995ad42e48cf84e.jpeg)
![b134390bd005ecd3b6496a9cb996164c.png](https://i-blog.csdnimg.cn/blog_migrate/a0bf9f60f5cc368a2d8c1551d31b526a.jpeg)
第二轮:依旧挑选最小的那个值5(0和3值对应的顶点已经计算过了),它是1顶点对应的值,加入1顶点后带来了12和13边,对应的我们要修改dist[]的权值,这里我们只需修改从0到3的权值即可,因为从0-1-2的权值为7,而从0-2的权值为3,7>3不需要修改。同时修改s[1]=1,然后修改paht[3]=1,因为3顶点我们是通过1顶点抵达,权值为下标为1
![e4480fc4dc0485b6b92632068d31bd5b.png](https://i-blog.csdnimg.cn/blog_migrate/28caef018ff55b59de7f126292026b02.jpeg)
![6eab0fe6488c4ba6cd2288ac518229bb.png](https://i-blog.csdnimg.cn/blog_migrate/796e945c6e31b2a8c3facaa90ed3cbbb.jpeg)
第三轮:我们挑选到3这个顶点(0、3和5值对应的顶点已经计算过了),3这个顶点加入后会带来32这条边,所以但是2顶点已经计算完成了,所以不需要修改dist[]数组,然后我们修改s[3]=1,同样path[]数组不需要修改
![17559a49038d0a8b64190b78efb482ad.png](https://i-blog.csdnimg.cn/blog_migrate/e06f29ef3a0ee56d654a73d3633c647c.jpeg)
![c874a0d5a6647e63451151b68f4e13c1.png](https://i-blog.csdnimg.cn/blog_migrate/c0b8047c12b48813b06813296567918a.jpeg)
第四轮:只剩下值为7这个顶点4未被挑选了,4顶点加入后为未带来新的边,所以不修改dist[]数组,修改s[4]=1
最后遍历完成,最后我们得到s[]数组值都为1,表示所有顶点计算完成,同时dist[]数组代表顶点0到其他顶点的最短路径长度,比如5代表了0-1的最短路径长度。我们还知道path[]数组保存了最短路径经历的序列,那么我们是怎么通过path[]数组计算路径序列的呢?比如我们计算0->1路径的序列,它的长度为5,序列:首先1对应的值为0,说明它的前驱结点的下标为0,paht[0]=-1,所以结束,序列为:0->1;计算0->2路径的序列,他的长度为3,2对应的值为0,他的前驱结点为下标为0,paht[0]=-1,所以结束,序列为:0->2;计算0->3路径的序列,它的长度为6,3对应的值为1,则说明它的前驱结点的下标为1,path[1]=0,path[0]=-1,所以结束,序列为:0->1->3;......
代码实现,时间复杂度O(V^2)
![550e2a1e4f62f8bcd37f7c7d57b8d050.png](https://i-blog.csdnimg.cn/blog_migrate/e45bdc7c51c92e0c70396e646ad9f8c1.jpeg)
注意:Dijkstra算法并不适用于含有负权值边的图
3.2Floyd(弗洛伊德算法)
Floyd(弗洛伊德算法):计算各个顶点之间的最短路径
也就是求任意两个点之间的最短路径。这个问题这也被称为“多源最短路径”问题。
![38745b9f9be887a453b2ad0a8180ba3d.png](https://i-blog.csdnimg.cn/blog_migrate/5c632f5a79613e7eca33f16c1470c2df.jpeg)
![7355745999d04cc2f43214d9a15231e4.png](https://i-blog.csdnimg.cn/blog_migrate/a11fa1162a18baac6afc00854d636988.jpeg)
如上图,我们使用Floyd算法,进行计算各个顶点之间的最最短路径,首先进行初始化,两个顶点之间有路径的值为权值,没有路径的值为∞。
![0bfbd855b2bae7cc372cb443ea56d637.png](https://i-blog.csdnimg.cn/blog_migrate/487cd4e72b12257c824546ddd5e1ce16.jpeg)
接着我们加入第一个顶点0顶点,我们随意找一个一条边比如A^(-1)[2][1],它的值为∞,表示从2顶点到1顶点没有边,然后加入0顶点后,我我们知道从2顶点到0顶点也没有边值为∞,从0顶点到1顶点有边值为1,∞+1还是∞,所以它的值A^(-1)[2][1]的值不进行更改,按照这样的方法将整个矩阵遍历一遍得到下面的矩阵:
![a20b7ae74eb9072ff9829f22a4cbe99a.png](https://i-blog.csdnimg.cn/blog_migrate/1b5baf5e3ee026a34251894021cea279.jpeg)
接着我们继续加入顶点,加入顶点1,我们仍然按照上面的方法进行计算,得到A^(1)矩阵:
![ea93a87e516edbef5c877db0d38df573.png](https://i-blog.csdnimg.cn/blog_migrate/a4e0b4380c41c553619bf4ffae2ae569.jpeg)
任然按照上面的方法得到A^(2)和A^(3)矩阵,其中A^(3)矩阵中的值就是各个顶点之间的最短路径
![92ff251927586d51fba30772fd4057fd.png](https://i-blog.csdnimg.cn/blog_migrate/b14bb821508719d9924b47d6d715c2ac.jpeg)
代码实现,时间复杂度:O(V^3)
![eb83617e6b427564a27b5962f34c4e5d.png](https://i-blog.csdnimg.cn/blog_migrate/e8d040c27c9bb95e04f8068357b67a20.jpeg)
整个算法的思想也可以参考这个博客 https://www.cnblogs.com/wangyuliang/p/9216365.htm
7.理木客
数据结构相关知识,公众号理木客同步更新中,欢迎关注
![8208386975bd2c9e72494bb4631dc7ef.png](https://i-blog.csdnimg.cn/blog_migrate/da4bca8070238ee17f25c8c9c2357c18.jpeg)