DAS6

概念

在这里插入图片描述

邻接关系是顶点之间的关系。
关联是顶点和边之间的关系。
二叉树,链表都是特殊的图。二叉树的邻接关系只在父子结点之间有,链表的邻接关系只在前驱和后继之间有。
在这里插入图片描述
更为一般的图可以在任意两个顶点之间有邻接关系。
当然自己和自己也会有邻接关系,不过这样的我们先不考虑。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
无向图和混合图都能通过有向图实现,所以我们只研究有向图。

在这里插入图片描述
路径的长度其实就是边的个数(重复的边重复计数)。
简单路径是不经过重复顶点(除非是环路)的路径。
比如CABD
相反是CABAD,A就经过了两次。
简单环路的例子:CABC
一般环路:CABADBC
还有一种有向无环图(DAC)。
还有两种特殊的路径:欧拉路径,经过图的每条边可以形成一个环路。
CABADBCDC
哈密顿路径是每个顶点经过一次:CADBC。
在这里插入图片描述

邻接矩阵和关联矩阵

在这里插入图片描述
邻接矩阵的每一个元素代表邻接关系,1为存在,0为不存在,有的还有权重,比如路的长度,那么就可以将权重直接存在相应的矩阵元素的位置。如果是无向图,邻接矩阵是对称的。
关联矩阵为顶点和边的关联关系,每一列肯定存在两个1,因为边肯定连接两个顶点(顶点自己连自己不考虑)。
例子:
在这里插入图片描述

顶点和边的实现

在这里插入图片描述
顶点给了三种状态:未被发现;被发现和已访问。
在这里插入图片描述
边有五种状态。这些在下面的查找算法会看到作用。在这里插入图片描述
这里使用二维向量来表示邻接矩阵,可以用类似的E[i][j]来取出矩阵的对应元素。
顶点需要封装的接口:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

邻接矩阵的优缺点

在这里插入图片描述
在这里插入图片描述

平面图就是:不相邻的边不相交的可以绘制在平面上的图。
例子:在这里插入图片描述
反例:
在这里插入图片描述
数学本质:
在这里插入图片描述
v为顶点数,e为边数,f为面数,c为连通数。或者
参考https://blog.csdn.net/qq_33229466/article/details/78045402
对于单连通图(图中任意两点存在路径,C=1)
V+F=E+2
根据平面图的欧拉公式,可以得到边的个数为O(n),这样邻接矩阵的利用率就为1/n,太低了。(这个证明先略)。

图的搜索

在这里插入图片描述
树的搜索我们的思路是把它转化为序列,有四种遍历方式。序列是线性的,这样处理问题就方便很多。
而图的搜索也是一样,先把它转化为树或者支撑树,然后再把树转化为线性的序列。

广度优先(BFS)

在这里插入图片描述

选定一个顶点S为初始的根,BFS先找S的邻接顶点,遍历,然后再以这些S的邻接顶点为根,依次类推。有一些需要注意的是:黑色的边才是被访问的边,而灰色的边没有被访问,因为要保证每个顶点只被遍历一次。
这样最后生成一种有向无环子图,或者树,称为支撑树。
这种BFS其实对应了树的层次遍历,按照邻接距离来进行遍历。
这里面的难点在于确定哪些边要描黑,哪些边要变灰(被舍弃)。

层次遍历用的是队列,那么这里也用队列。
在这里插入图片描述
初始顶点的时候状态都是未发现,现在只要入队的顶点的状态都是已发现了。然后从后往前找当前顶点的邻接顶点。
这些邻接顶点需要看情况处理,因为可能已经在栈中了,就不能重复的访问。如果顶点被发现,会同时将边的状态置为TREE。
如果该点还是未被发现的,那么就将此边的状态设为CROSS。
当前顶点的邻接顶点访问完毕之后改顶点状态改为VISITED。
在这里插入图片描述

例子:
白色为未发现状态,黑色为发现状态。父亲是直接要忽略的。
这个顶点的序列可以认为是gfedbcas,s的位置没那么重要。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
忽略掉CROSS的边,就得到了支撑树。
上面的其实只针对单连通图。如果图为多连通的,上面肯定是不行的。
因为肯定会漏掉不在S连通域的点。那么:
在这里插入图片描述

这遍历了图的顶点,对多连通区域分别进行BFS。

复杂度分析

对于C=1来分析:
在这里插入图片描述
首先Q出队操作肯定要执行n次,n为顶点的数量。内循环的每个顶点入队操作也要O(n)次。
那么现在就有了O(n^2)了。
而内循环for的实际执行次数一共应该是O(e),那么最后为O(n^2+e)=O(nxn)
。因为e最多为nxn个。
不过:
在这里插入图片描述

对于任何一级存储器和高速缓冲比较,速度会差8倍。在这里插入图片描述

实际上这个算法的复杂度大概在O(n+e)。这个复杂度为搜索的下限了,因为遍历至少需要遍历顶点和边一次。
在这里插入图片描述
如果不是用邻接矩阵而是邻接表,则会直接得到O(n+e)的复杂度,邻接表可能是基于链表实现的。

最短路径

这个应该是要假设所有边的权重为1。
在这里插入图片描述
如果是树结构,那么一个结点到根的深度是固定唯一的,然而对于图,可能有多条路径通向v。
在这里插入图片描述
参考https://www.pianshen.com/article/19071004386/
其实BFS的路径就是最短的路径。我其实也有点想法,就类似于反证法,
首先假如有一个顶点的最短路径为1,但是在BFS中2的等价类。这种情况可能出现吗?不可能,因为第一次对S的邻接距离为1的遍历肯定全都会遍历到。所以1和2的等价类都是最短的,那么数学归纳法以此类推。

加权最短路径

我的感觉是用DP,就是先考虑可以到v的顶点们v1…vk。这个最短路径应该是vi到v的距离加上s到vi的最短距离。这样就把问题规模减1了,虽然分成了很多减一规模的问题,这样递归就可以了。递归基就是和s邻接距离为1的顶点了。然后是迭代求最短路径,直到到v。

Dijkstra算法

参考https://blog.csdn.net/yalishadaa/article/details/55827681
注意该算法的前提:不存在负的权重。理解负权重
负权重的出现其实不只体现在数学上,在实际应用中其实非常实用。以任务调度为例。
在这里插入图片描述

以上图为例。边的权重指的是任务需要的时间。例如任务0需要41个时间单位才能完成,任务0完成后才能开始任务1,任务1需要82个时间单位,然后才能开始任务2…

而6指向3的路径权重为-20,这是指6号任务需要在3号任务开始后的20个时间单位内开始,或者说3号任务不能早于6号任务的20个时间单位。

这个算法的不变性为S中的顶点的路径的最小值小于等于U中顶点的最小值。
单调性就是U的元素个数越来越少,最终为0。那么可以得到正确性。

第一轮的时候首先找到了和S邻接的顶点的最短路径。设顶点为v1。
第二轮找的是次短的路径,次短的路径的终点可能在与S邻接的顶点中,也可能在经v1的路径中,那么可能在不经过v1的路径吗?不可能,因为不经过v1的话,s->vi(i不为1)->vk的路径肯定比直接的s->vi长。所以这种情况无需考虑。

这个复杂度可以分析一下:
外循环:肯定是要遍历U中的元素,。
内循环:
进行D的更新。
更新的过程需要访问邻接表或者邻接矩阵。邻接矩阵的复杂度为O(n)
最后还要求最小值,以确定从U中移除的顶点。求最小值也是O(n).

总体应该是O(n^2)。

代码的实现:
https://blog.csdn.net/qq_39630587/article/details/83240036?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase

注意这个方法的权重非负,不然算法的正确性得不到保证。

还可以参考下
https://www.bilibili.com/video/BV1q4411M7r9/?spm_id_from=333.788.videocard.0
有动画,如果还要求出具体路径而不是只求最短路径的值,那么还需要维护一个路径的表。就是parent,parent像个静态链表。
这种算法是一种贪心策略。这种策略能成功就在于如果v到vk最短路径为v->…vi->…vk,那么该路径包含的v到vi的路径也一定是最短的,因为如果不是,那么这个就不是v到vk的最短路径。

Dijskstra的堆优化

https://www.bilibili.com/video/BV1UC4y147zW/?spm_id_from=333.788.videocard.0

Floyd-warsh算法

它是求图中任何两顶点之间的最短路径,和dijkstra不同,它是多源的,那么最简单的就是以dijksra为基础,再外加一层循环。维护一个D的矩阵。复杂度为O(n^3)。
实际上的思想是动态规划。而且这个算法的的权值可以为负。在这里插入图片描述
在这里插入图片描述
如果选择的中间结点在最短路径上,那么从s到v的最短路径势必等于s到vi的最短路径加上从vi到v的最短路径。
如果不在最短路径上,那么这个关系是小于号,这个很好理解。
在这里插入图片描述
第一行就是在做比较,维护D矩阵,然后维护S矩阵,S矩阵存的是路径信息。
在这里插入图片描述
D存着最短路径的值。
S存储的是路径,比如0行4列对应的值为4,说明从0可以直接到4,而0到1则需要先经过4,然后看4到3,值为3,可以直接达到,然后看3到1,得先经过2,最后看2到1,可以直接到达,-1表示不可达。我们这里只有对角线的是不可达,也就是自己到自己,是单连通图。
那么不难理解:在这里插入图片描述
视频中还有例子,推荐去看。
这种方法便利了所有的中间结点。肯定可以找到最短的路径。

Bellman Ford

这种算法的复杂度大概为O(VE),大于dijkstra算法,小于佛洛伊得算法。
但是它可以适用于权值为负的情况。
举个简单的例子来说明diskstra为何不适用于权值为负的情况。

假如 0 1 3
0 2 4
2 1 -2

这里以0为源点。
0到1的最短距离应该是0->2->1,但是按照dijkstra算法,这个值会是2。

参考https://www.bilibili.com/video/BV1gb41137u4/?spm_id_from=333.788.videocard.2
这个可视化视频不错。里面也解释了负环的问题。
https://blog.csdn.net/lady_killer9/article/details/102724637
和https://blog.csdn.net/yuewenyao/article/details/81026278

#include<bits/stdc++.h>
const int INF = 99999999;
using namespace std;
int main()
{
    int u[100] , v[100] , w[100] , dis[100] , n , m ;
    cin>>n>>m;
    for(int i = 1 ; i <= m ; i ++)
    {
        cin>>u[i] >> v[i] >> w[i];
    }
    for(int i = 1 ; i  <= n ; i ++)
    dis[i] = INF;
    dis[1] = 0;
    for(int k = 1 ; k <= n - 1 ; k ++)
        for(int i = 1 ; i <= m ; i ++)
            if(dis[v[i]] > dis[u[i]] + w[i])
                dis[v[i]] = dis[u[i]] + w[i];
            for(int i = 1 ; i <= n ; i ++)
                cout<<dis[i]<<" ";
    return 0 ;
}
 
 
/*
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
*/

上边的代码外循环共循环了n - 1 次(n为顶点的个数),内循环共循环了m次(m代表边的个数)即枚举每一条边, dis 数组是的作用和dijkstra 算法一样,也是用来记录源点到其余各个顶点的最短路径,u,v,w 三个数组用来记录边的信息。例如第i条边存储在u[i]、v[i]、w[i]中,表示从顶点u[i]到顶点v[i]这条边(u[i] --> v[i])权值为w[i]。
两层for循环的意思是:看看能否通过u[i]—>v[i] (权值为w[i])这条边,使的1号顶点的距离变短。即1号顶点到u[i]号顶点的距离(dis[u[i]]) 加上 u[i] —> v[i]这条边(权值为w[i])的值是否比原来1号顶点到v[i]号距离(dis[v[i]])要小。这点感觉和迪杰斯特拉的松弛操作有点儿像。
看下整个过程:
输入:


在这里插入图片描述
第一轮的松弛操作,对所有的边进行。我们来看看从1到5的松弛:
1到1是0,1到5是5,加起来小于正无穷,松弛成功。
1到2为-3也松弛成功。
2到3为2,而D[2]=-3,D[3]=+∞,可以松弛为-1。
3到4的w为3,D[3]=-1,现在D[4]=∞,松弛为2。
4到5的w为2,D[4]=2,可以松弛D[5]=4。
这里就遍历了一遍所有的边了。
此时
0 -3 -1 2 4
这里例子比较简单,我这一遍就出结果了。
事实上,这个和遍历边的顺序有关系的,
如果先松弛的是3,而不是2和5:
D[2]=∞,D[3]=无穷,w=2,松弛失败。
这样就需要多松弛几次。

这个算法外循环个数等于n-1的道理在于,最短路径最多就是经过n-1个点(假设没有负环)。或者说将s到v最短路径可能的n-1种情况都考虑了:
s->v直接
s->…v1->v经过v1到v。

除了s也就n-1种可能,只经过v,最后由v1到v等。

每次的松弛操作都离最短路径更近一步。

我们来总结一下。因为最短路径上最多有n-1条边,所以Bellman-Ford算法最多有n-1个阶段。在每-一个阶段,我们对每一条边 都要执行松弛操作。其实每实施一次松弛操作, 就会有一些顶点已经求得其最短路,即这些顶点的最短路的“估计值”变为“确定值”。此后这些顶点的最短路的值就会一直保持不变,不再受后续松弛操作的影响(但是,每次还是会判断是否需要松弛,这里浪费了时间,是否可以优化呢? )。在前k个阶段结束后,就已经找出了从源点发出“最多经过k条边”到达各个顶点的最短路。直到进行完n-1个阶段后,便得出了最多经过n-1条边的最短路。除此之外,bellman-ford 算法还可以检测出一个图是否含有负权回路。如果进行n-1轮松弛操作之后仍然存在

if(dis[v[i]] > dis[u[i]] + w[i])

            dis[v[i]] = dis[u[i]] + w[i];

的情况,也就是说在进行n-1轮松弛后,仍可以继续成功松弛,那么此图必然存在负权回路。如果一个图没有负权回路,那么最短路径所包含的边最多为n-1条,即进行n-1轮松弛操作后最短路不会再发生变化。如果在n-1轮松弛后最短路仍然可以 发生变化,则这个图一定有负权回路,代码如下

 for(int k = 1 ; k <= n - 1 ; k ++)
        for(int i = 1 ; i <= m ; i ++)
            if(dis[v[i]] > dis[u[i]] + w[i])
                dis[v[i]] = dis[u[i]] + w[i];
                //检测负权回路
                flag = 0 ;
                for(int i = 1 ; i <= m ; i ++)
                if(dis[v[i]] > dis[u[i]] + w[i])
                    flag = 1 ;
                if(flag == 1)
                    printf("此图没有负权回路\n");

显,Bellman-Ford 算法的时间复杂度是O(NM),这个时间复杂度貌似比Dijkstra 算法还要高,我们还可以对其进行优化。在实际操作中,Bellman-Ford算法经常会在未达到n-1轮松弛前就已经计算出最短路,之前我们已经说过,n-1其实是最大值。因此可以添加一个一维数组用来备份数组dis。如果在新-一轮的松弛中数组dis没有发生变化,则可以提前跳出循环,代码如下。

#include<bits/stdc++.h>
const int INF = 9999999;
using namespace std;
int main()
{
    int u[100] , v[100] , w[100] , dis[100] , n , m , ck , flag;
    cin>>n>>m;
    for(int i = 1 ; i <= m ; i ++)
    {
        cin>>u[i] >> v[i] >> w[i];
    }
    for(int i = 1 ; i  <= n ; i ++)
    dis[i] = INF;
    dis[1] = 0;
    for(int k = 1 ; k <= n - 1 ; k ++)
    {
        ck = 0 ; //用来标记本轮松弛操作中数组dis是否会发生更新
        for(int i = 1 ; i <= m ; i ++)
        {
            if(dis[v[i]] > dis[u[i]] + w[i])
            {
                 dis[v[i]] = dis[u[i]] + w[i];
                 ck = 1 ;  //数组dis发生更新,改变check的值
            }
        }
        if(ck == 0)
            break;   //如果dis数组没有更新,提前退出循环结束算法
    }
    flag = 0 ;
    for(int i = 1 ; i <= m ; i ++)
    if(dis[v[i]] > dis[u[i]] + w[i])
                flag = 1;
                if(flag == 1)
                    printf("此图包含有负权回路\n");
                else
                {
                    for(int i = 1 ; i <= n ; i ++)
                        printf("%d ",dis[i]);
                }
    return 0 ;
}
 
/*
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
*/

不过如果有负环的话这个循环就也会一直的进行了。
Bellman-Ford算法的另外一种优化在文中已经有所提示:在每实施一次松弛操作后,就会有一些项点已经求得其最短路,此后这些顶点的最短路的估计值就会一直保持不变, 不再受后续松弛操作的影响,但是每次还要判断是否需要松弛,这里浪费了时间。这就启发我们:每次仅对最短路估计值发生变化了的顶点的所有出边执行松弛操作。
美国应用数学家Richard Bellman (理查德。贝尔曼)于1958 年发表了该算法。此外Lester Ford, Jr在1956年也发表了该算法。因此这个算法叫做Bellman-Ford算法。其实EdwardF. Moore在1957年也发表了同样的算法,所以这个算法也称为Bellman-Ford-Moore算法。Edward F. Moore很熟悉对不对?就是那个在“如何从迷宫中寻找出路”问题中提出了广度优先搜索算法的那个家伙。
dijkstra和这个bellman-ford都是SSP(单源最短路径的算法)。

Bellman-Ford的队列优化SPFA

https://www.bilibili.com/video/BV1Lg4y1q7Cg?from=search&seid=10219992226676427212
在这里插入图片描述

这个改进主要是可以在合适的时间停止,而且最先松弛的是和源点邻接的点,这样是很好的。
但是这样的SPFA方法不能有负环,不然无限循环了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值