图的总结

上一篇关于树的博客:二叉树的总结-CSDN博客

本篇并不会对每一个点写的很细,只是对概念进行理解和梳理。

如何理解图:

将一些散落的点连接在一起,那便构成了图。回想我们之前接触的树,那就是一种有约束的图,相对于树而言,图的约束更少了一点。同时,为了更好的编程,或是去约束图,于是就强调了边的概念。(树也是有边的概念的,比如哈夫曼树的权,只是我们一般都忽视了)

注意:一个孤立的点也能称之为图

图的一些名词及理解

  • 子图:取图的一部分,本身还是图,对照树和子树的概念
  • 度:对于无向图而言,度就是点的边
  • 出度,入度:对于有向图,出与入的概念就是边的箭头的指向。指向这个点就是入度,反之出度
  • 连通分量:对于无向图,如果有:有边ba,bc,那么a,c之间是连通。abc这三个点构成的子图就是就是连通分量
  • 强连通分量:对于有向图,它的任意两个点都可以连通的,那它就是强连通分量。
    注意:一个孤立的点也是强连通分量。

图的存储

邻接矩阵:

就是一个二维数组去存储。先把点编号:1,2,3…,然后看数组下标a[1][2]就代表1和2这两个点的边,如果这个边存在,那就a[1][2]=1否则局赋值0。

它的优势就是好查找,缺点就是存储空间的利用很有问题。这其实就是数组存储本来就有的问题。

邻接表:

创建数组存储点,创建链表来表示临边。比如编号1的点a[1]有两个边(2 3),那就创建两个节点,data存2和3(主要用于记录指向的点)。然后有 :1的next指向data=2的节点,这个data=2的节点的next指向data=3的节点,这个data=3的节点的next指向空(先3再2也可以),这个链表就是1的邻接表。然后创建2的邻接表时,又根据它的边数去创建节点。

它的优势就是优化了存储好,查找麻烦,也是链表本来的问题

补充:

  • 有向图就只需要考虑出度或者入度,考虑入度的叫:邻接表,考虑出度的叫:逆邻接表。他们的优点和缺点很明显,就是单向查找。
  • 同时,相对于无向图,有向图的每一个节点都可以理解为对应的一条独立的边。而无向图的的节点就是边的两倍,毕竟有重复的嘛。
  • 以及,使用邻接表的时候就通过a[node->data]来得到对应的点。

十字链表:

针对有向图邻接表的不方便,于是诞生了十字链表。这其实和之前的双向链表的来源很像,不也是为了处理一个问题从而诞生一种方式嘛,只不过数据结构的构成不一样而已,本质区别不大的。

依旧是用数组存储点,点的指针有两个(很像双向链表)一个是考虑入度的next一个考虑出度的next。

这里不太一样的是:用节点一一代表边,然后边的的结构就分为了两个data和两个next。两个data作用自然就是存储箭头的两边的点的编号,next就是对应的考虑入度(逆邻接表)和出度(邻接表)的指针。

这样,就把有向图的邻接表和逆邻接表结合起来了。

小拓展

不知道大家注意到了没有,用邻接表拿点的时候是:a[node->data],这个a的值在哈希表里面就是value,而node->data就是key转化的编号。

哈希表(Hash Table又名散列表),就如其名,和我们伟大的哈希有关的一种处理零散数据的表。(有兴趣可以去看看哈希筛

它的原理就是把数据分为两个部分,一个是key,一个是value,并且这两个部分可以是任何类型的数据。然后把key经过一定的算法转化为Int类型,然后令:a[key] = value建立两者的关系。

至于那个算法是啥,就不多拓展了哈,大家可以自行搜索一下(或者后面我可能写一篇相关的博客)。

图的两个优先搜索

先给理论:

  • 广度搜索:从编号为1的点开始,先找他所有的临边的点,找完后再依次以临边的点为基础,找临边的临边的点。
  • 深度搜索:从编号为1的点开始,选中一个临边的点,去找这个点的临边的点,直到找不到为止,再以这个编号1开始找另一个临边的点。

由于上一篇二叉树的总结写过了,所以具体实现代码就不贴了哈,毕竟是总结嘛,篇幅有限,篇幅有限,绝对不是因为我懒。

注意: 找过点不需要重复再找,实现的时候可能要多开一个数组用b[next->data]=0或者=1来判断是否遍历过了。

图的应用:

然后呢,来到应用层:找最短路径什么的,毕竟来到图之后,才算是真实世界有了具体的联系了,而不像之前,总是要归于具体的问题我们才能处理。

最短路径

这个很好理解,我们有很多条路,但是我们只关心两个点:起点和终点。然后在众多路径之中找到最短的路径。它的应用场景是:地图的导航。我们取自己的定位为起点,一些建筑物为终点,得到起点到这些建筑物的最短路径。

迪杰斯特拉算法

迪杰斯特拉(Dijkstra),简单来说,我们开一个数组a[n]其中a[1]就代表起点到标记点1的距离一开始记为很大很大。然后开始依次放入点,并更新距离a[n]:首先把起点放进去,更新出起点可以直接到达的点的距离。遍历数组a[n]找到没有放入,且距离最小的点放入其中。这样就可以保证起点可以沿着这个点去到其他的点,并得到一个新的到达其他的点的距离,更新到对应点的最小的距离。如此循环,直到把所有的点都放进去更新一遍。

补充: 这种方式保证了局部最优,其实并不会出现:a->b更新一次后,又有a->c->d->b来更新距离,因为如果有的话,那么就不会先引入a->b了而是先引入的a->c->d。

弗洛伊德算法

由于有时候我们不仅仅是需要只要一个起点,我们想直到任意一点到任意一点的距离。我们如果用n次迪杰斯特拉的话,显得很呆,于是就有了:弗洛伊德算法。但是他们两者在时间复杂度上是一致的,甚至可以说:弗洛伊德简化了 n次迪杰斯特拉 的使用。

首先:我们拿到这两个点vj vi得到距离(数值或无穷)然后在他们中间加入V0比较:(vj vi)(vj,V0,vi)

的距离,取最小的。然后加入V1用上面的方式得到(vj....V1)(V1....vi)把他们合起来(vj...V1...vi)与上一个距离比较,依次类推。

最小生成树

这个最小生成树和最短路径可不一样,它是需要关联起所以的点的。它的应用范围呢大概就是去处理:为一个地区规划路线,如何做到即连通了所有的城市,又可以使得修路的代价最小。

之前便提到过,图就是边随意连接的一种树,树就是限制了边的一种图,我们现在的目的就是现在它的边。这个边不能瞎限制,每一条路都要代价,也就是权值,我们要找到最小的哪一种修路的方法。于是。就有两位伟大的人基于贪心算法的思想基础,以不同的关注基点提出了对应的算法。普利姆(prim)算法(加点算法)克鲁斯卡尔(kruskal)算法(加边算法)。自然,当稠密图(边相对多)时,用加点的算法会好一些,反正用加边的算法会好一些。这就是侧重不同使用范围也不同。

小声叨叨:难道局部最优就是全局最优吗?(有大神解释不?)

普里姆算法:

普里姆(prim)算法,又称为加点法。正如其名他就是以点为基础而成立的。首先选一个幸运观众A作为一个基础点计入树tree中,然后开一个数组closedge[n]去记录这个点的每一条边的权值,找到最小边连接的那个点B,然后把这个最小的B加入树tree中,并且更新数组closedge[n]的权值,就是把这个(A,B)的权去掉,然后加入B这个点的和非树tree中的点的所有边的权值。重复如上行为,直到把所有的点都加入tree中。

强调: 非树tree中的点,就保证了不会连成图。

参考实现代码:(辅助理解)

//假设有n个点
struct{
    int from;  //表示tree中的点的编号,为0表示存入tree中了
    int value;  //表示对应权值,赋值0表示不存在
}closedge[n]//下标n表示与from连接的点的编号
    
struct{
    int Inx;
    int value;
}Min; //记录closedge最小权值的下标 

 void Prim(Gn:gn,int a,Tree* root){ //gn:连通网,a:初始点 Tree:树
    //初始化:
     closedge[a].value = 0; closedge[a].form = 0; 
     Min.Inx = 0; Min.value = Max; //Max就当一个很大的数吧,摆烂了
    for(int i = 1;i<=n;i++){
        if(!a){
            closedge[i].from = a;
            closedge[i].value = gn.weights[a][i]; //表示a,i边的权值
            if(closedge[i].value<Min.value && closedge[i].value !=0 ){
                Min.value = closedge[i].value;
                Min.Inx = i;
            }
        }
    }
    //生成树:
    for(int e = 1;e<n-1;e++){
        if(Min.Inx ==0){
            printf("出错了");
             break; 
        }
        int v = Min.Inx;
        int u = closedge[v].from;
        root.connect(v,u)//树的连接两点方法,不是重点
        closedge[v].from = 0;  //表示这个点被收编了
        //更新closede:和Min
        Min.Inx = 0 ;Min.value = Max;
        for(int i = 0; i<n;i++ ){
            //更新到某个点的最小权值:
            if(closedge[i].from != 0 && gn.weights[v][i]!=0 
               && gn.weights[v][i]<closedge[i].value){
                closedge[i].value = gn.weights[v][i];
                closedge[i].from = v ;
            }
            //更新最小数据:
            if(closedge[i].from != 0 && closedge[i].value !=0 
               && closedge[i].value<Min.value ){
                   Min.value = closedge[i].value;
                   Min.Inx = i;
               }
        }
    }
    
}
克鲁斯卡尔算法

克鲁斯卡尔(kruskal)算法又称为加边法,还是那句话,正如其名,以边为基础。先把所有的边存入数组:edge[n],然后依次连接权值最小的边,注意不要成一个环。如过选的这个边导致它成环了,那就跳过这个边,选下一个。

那么,我如何知道是否构成了一个环呢?我的想法是:记录这个边连接的两个点A,B。然后从A开始对这个新构成的图做深度查找,如果找不到B,那就是不成环。

有向无环图:

它用来表示一些有层次递进的约束关系。比如:工程上器件的递进组装,工程施工图等约束关系

拓扑排序

简单来说就是把被要求的放在最前面,我们不妨就找没要求的放在第一层,也就是入度为0的点。然后,这些点出度所连接的点,把他们的入度减一,就相当于我们满足了它的一个要求了。这个时候再去审视这些没有放入数组的点,谁的入度为0,谁就放在第二层,依旧是没要求的放进去。

关键路径

由于之前就已经为我的器件们分好了层,现在我需要知道完成这一个项目的时间到底是多少,于是就在这些边上面,加了完成时间,也就是权值。

关键路径 简单来说:就是耗时最长的路径。因为很多的工作都是可以分路径完成部分零部件,再组装到一起的,如果是非最耗时路径耽误几天也无所谓(只要在那个开始组装时间内完成就可以了),所以耗时最长的路径自然就是最关键的路径了。

由于一个大的产品是由许多小的器件构成,小的器件是由他们的零件构成,于是就有点套娃的感觉在里面了,我们把它分为三层,这里以中间的器件为主要视角来讲解一些名词的理解:

  • 事件最早开始时间:就是零件的关键路径导致的开始组装器件的时间。这就可以顺着拓扑结构去推时间
  • 事件最晚开始时间:就是组装产品时的非关键路径器件时,可以晚几天开工,但是不能大于关键路径的器件路径完工时间,这就耽误工期了不是。这个自然就是逆着拓扑结构去推时间了。
  • 活动的开始时间:可以理解为开始组装的时间,那就自然有最早,最晚,冗余三个时间点了。

注意: 具体在找的时候,我们可以先去找时间最早开始时间,并对每一个活动点进行记录。然后依据这个最早开始时间,才能反向最大限度的去给予最晚开始时间。而关键路径,一般就是活动冗余时间为0的那一群点组合起来的(因为如果有冗余时间,那就意味着,这一层有别的点的时间比你长,那你就不是关键路径)。

  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值