探索数据结构:图(三)之最短路径算法

news2024/11/14 22:10:16


✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:数据结构与算法
贝蒂的主页:Betty’s blog

1. 最短路径算法

最短路径问题可分为单源最短路径多源最短路径。其指的是在带权有向图中,从某一顶点出发,找出通往另一顶点的路径,其中“最短”意味着该路径各边的权值总和达到最小。
其中单源最短路径是从图中的某一特定顶点出发,通往其他所有顶点的最短路径。与之不同,多源最短路径则是找出图中任意两个顶点之间的最短路径。

1.1 单源最短路径

解决单源最短路径的方法常见的有两种:Dijkstra(迪杰斯特拉算法)Bellman-Ford(贝尔曼福特算法)

1.1.1 Dijkstra

Dijkstra算法(迪杰斯特拉算法)是用于求解带权有向图中单源最短路径问题的算法。该算法以起始顶点为中心向外层层扩展,直到扩展到终点为止。它通过维护一个距离源点距离最短的顶点集合,每次从尚未确定最短路径的顶点中选择一个距离源点最近的顶点,将其加入到集合中,并更新与其相邻顶点到源点的距离。但是需要注意的是迪杰斯特拉算法要求权值为正数,如果有负数则可能出错。
二、算法步骤

  1. 初始化:
  • 把图中所有顶点分为两部分,一部分是已确定最短路径的顶点集合(初始时只有源点),另一部分是未确定最短路径的顶点集合。
  • 为每个顶点设置一个距离值,表示从源点到该顶点的当前最短路径长度(初始时,源点的距离值为 0,其他顶点的距离值为无穷大)。
  1. 重复以下步骤直到所有顶点都被加入到已确定最短路径的集合中:
  • 从未确定最短路径的顶点集合中选择一个距离源点最近的顶点u
  • 将顶点u加入到已确定最短路径的集合中。
  • 对于与顶点u相邻的每个顶点v,更新其距离值。如果通过顶点u到达顶点v的路径比当前已知的最短路径更短,则更新顶点v的距离值为从源点到顶点u的距离加上边<u,v>的权值。
  1. 算法结束后,每个顶点的最终距离值就是从源点到该顶点的最短路径长度。
  1. 首先选择s点作为起始点更新,更新两个离的最近ty顶点。

  1. 接下来选择已更新节点权值最小的顶点y出发继续更新顶点,首先t顶点会被再次更新,接着xz顶点也会被更新。同理我们继续选择顶点z作为起始点,会再次更新x节点。

  1. 最后选择t顶点再次更新x顶点,最后选择x顶点更新完毕。


接下来我们需要实现Djikstra算法,首先我们可以创建一个pPath数组来记录到达各个顶点的前驱顶点,方便最后得出最短路径。然后进行初始化,并且创建一个bool数组来标记已确定加入集合的顶点,然后遍历所有顶点选择未加入集合且当前距离源点最小的顶点开始更新距离,更新之前先将该顶点标记。

//Dijkstra算法
void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
{
    int n = _vertexs.size();
    int srci = getVertexsIndex(src);//获取源点下标
    dist.resize(n, MAX_W);//初始化
    pPath.resize(n, -1);//默认父节点下标为-1
    dist[srci] = W();//源点设为初始值
    vector<bool> visited(n, false);//已选中节点
    //更新n个顶点
    for (int i = 0; i < n; i++)
    {
        W minW = MAX_W;
        int u = -1;
        for (int j = 0; j < n; j++)
        {
            //选出距离的边记录其下标与权值
            if (visited[j] == false && dist[j] < minW)
            {
                minW = dist[j];
                u = j;
            }
        }
        //标记
        visited[u] = true;
        //进行松弛更新
        for (int v = 0; v < n; v++)
        {
            if (visited[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v])
            {
                dist[v] = dist[u] + _matrix[u][v];
                pPath[v] = u;
            }
        }
    }
}

然后我们可以通过父节点更新出最短路径。

void PrinrtShotPath(const V& src, const vector<W>& dist, const vector<int>& pPath)
{
	int n = _vertexs.size();
	int srci = getVertexsIndex(src);
	for (int i = 0; i < n; i++)
	{
		vector<int> path;
		int cur = i;
		while (cur != -1)
		{
			path.push_back(cur);
			cur = pPath[cur];
		}
		reverse(path.begin(), path.end());
		for (int j = 0; j < path.size(); j++)
		{
			cout << _vertexs[path[j]] << "->";
		}
		cout << "权值为:" << dist[i] << endl;
	}
}
1.1.2 Bellman-Ford

Bellman-Ford算法也是用于求解带权有向图中单源最短路径问题的算法。与Dijkstra算法不同的是,Bellman-Ford算法可以处理图中存在负权边的情况。
算法步骤:

  1. 初始化:
  • 为每个顶点设置一个距离值,表示从源点到该顶点的当前最短路径长度(初始时,源点的距离值为 0,其他顶点的距离值为无穷大)。
  1. 重复以下步骤V - 1次(V 是图中顶点的数量):
  • 对于图中的每一条边(u, v),如果从源点到顶点u的距离加上边(u, v)的权值小于当前从源点到顶点v的距离,则更新顶点v的距离值为从源点到顶点u的距离加上边 (u, v) 的权值。
  1. 检测负权回路(回路权值为负):
  • 再对图中的每一条边进行一次遍历。如果在这次遍历中,还能找到一条边(u, v),使得从源点到顶点u 的距离加上边(u, v)的权值小于当前从源点到顶点 v 的距离,那么说明图中存在负权回路。
  1. 算法结束后,如果没有负权回路,每个顶点的最终距离值就是从源点到该顶点的最短路径长度;如果存在负权回路,则无法得到正确的最短路径结果。
  1. 选取顶点s作为起始点,开始更新s-ts-y的距离。

  1. 分别以顶点yt作为起始点更新到达顶点xz的距离。

  1. 最后以顶点xz点作为起始点更新距离,其中因为到达顶点t的最小距离s-t被再次更新,所以到达顶点z的最小距离s-t-z也需要被再次更新。


下来我们需要实现Bellman-Ford算法,同样我们可以创建一个pPath数组来记录到达各个顶点的前驱顶点,方便最后得出最短路径。然后只需要循环遍历每一个顶点,以该顶点为起始点更新其他顶点,最后重复V-1轮即可。

bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
{
	int n = _vertexs.size();
	//获取源点下标
	int srci = getVertexsIndex(src);
	//初始化
	dist.resize(n, MAX_W);
	pPath.resize(n, -1);
	dist[srci] = W();
	//至多更新n-1轮
	for (int v = 0; v < n - 1; v++)
	{
		//判断是否发生更新
		bool update = false;
		for (int i = 0; i < n; i++)
		{
			for (int j = 0; j < n; j++)
			{
				if (_matrix[i][j] != MAX_W && dist[i] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
				{
					dist[j] = dist[i] + _matrix[i][j];
					pPath[j] = i;
					update = true;
				}
			}
		}
		//如果未发生则直接退出
		if (update == false)
		{
			break;
		}
	}
	//判断是否存在负权回路
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
			{
				return false;//存在负权回路
			}
		}
	}
}

Bellman-Ford算法还有一个优化方案叫做SPFA(Shortest Path Faster Algorithm),主要是用一个队列来维护可能需要再次的顶点,避免了冗余的过程,但是这里不再重点介绍,感兴趣可以自行了解。
思考题:为什么要重复更新V-1次?

因为每一次更新可能使得前面已经更新过的路径的长度可以变得更短,或者使得某些源顶点之前不可达的顶点变得可达。比如说s->t->x先被更新,但是后续可能存在其他路径使得s->t路径更小,此时却并没有更新s->t->x路径。所以每一次更新只能使得至少能确定最短路径中的一条边,一个有V-1条边,所以至多更新V-1次。

1.2 多源最短路径

解决多源最短路径问题,虽然可以通过遍历每一个顶点使用DijkstraBellman-Ford算法,但明显时间复杂度太高。而最常见的解决该问题便是Floyd-Warshall(弗洛伊德算法),但是学习这种算法需要有点门槛,你得需要了解什么是动态规划

1.2.1 Floyd-Warshall

Floyd-Warshall算法的原理基于动态规划。设 D i j D_{ij} Dij 为从 i i i j j j 只以 1.. k 1..k 1..k集合中的节点为中间节点的最短路径长度。

  1. 若最短路径经过点 k k k,则 D i , j , k = D i , k , k − 1 + D k , j , k − 1 D_{i,j,k} = D_{i,k,k-1} + D_{k,j,k-1} Di,j,k=Di,k,k1+Dk,j,k1
  2. 若最短路径不经过点 k k k ,则 D i , j , k = D i , j , k − 1 D_{i,j,k} = D_{i,j,k-1} Di,j,k=Di,j,k1

因此, D i , j , k = min ⁡ ( D i , j , k − 1 , D i , k , k − 1 + D k , j , k − 1 ) D_{i,j,k} = \min(D_{i,j,k-1},D_{i,k,k-1} + D_{k,j,k-1}) Di,j,k=min(Di,j,k1,Di,k,k1+Dk,j,k1) 。在实际算法中,为节约空间,可直接在原空间迭代,空间可降至二维。


路径 p p p是从结点 i i i到结点 j j j的一条最短路径,结点 k k k是路径 p p p上编号最大的中间结点。路径 p 1 p1 p1是路径 p p p上从结点 i i i到结点 k k k之间的一段,其所有中间结点取自集合 ( 1 , 2 , … , k − 1 ) (1,2,…,k - 1) (1,2,,k1)。从结点 k k k到结点 j j j的路径 p 2 p2 p2也遵守同样的规则。
算法步骤:

  1. 初始化:
  • 构建两个二维数组,一个用于存储最短路径长度估计值 D i j D_{ij} Dij,初始时,若两点之间有直接边则为边的权值,否则为无穷大;另一个数组用于记录路径的前驱节点。
  1. 动态更新:
  • 对于每个中间顶点 k k k,遍历所有的顶点对 i i i j j j。如果 D i k + D k j < D i j D_{ik} + D_{kj} < D_{ij} Dik+Dkj<Dij,则更新 D i j D_{ij} Dij D i k D_{ik} Dik + D k j D_{kj} Dkj,并更新前驱节点。

下来我们需要实现Floyd-Warshall算法,同样我们可以创建一个pPath数组来记录到达各个顶点的前驱顶点,方便最后得出最短路径。然后只需要循环遍历每一个顶点,以该顶点为中间顶点更新其他顶点对。

//FloydWarshall
void FloydWarshall(vector<vector<W>>& dist, vector<vector<int>>& pPath) 
{
	//初始化
	int n = _vertexs.size();
	dist.resize(n, vector<W>(n, MAX_W)); 
	pPath.resize(n, vector<int>(n, -1)); 
	//初始化直接相连的边
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++) 
		{
			if (_matrix[i][j] != MAX_W) 
			{ 
				dist[i][j] = _matrix[i][j]; 
				pPath[i][j] = i; 
			}
			//顶点本身
			if (i == j) 
			{ 
				dist[i][j] = W(); 
			}
		}
	}
	//依次取每个顶点作为中间节点
	for (int k = 0; k < n; k++) 
	{ 
		//起始顶点
		for (int i = 0; i < n; i++) 
		{
			//结束顶点
			for (int j = 0; j < n; j++) 
			{
				// i->k + k->j 比 i->j前面更新的距离更短,则更新
				if (dist[i][k] != MAX_W && dist[k][j] != MAX_W &&
					dist[i][k] + dist[k][j] < dist[i][j]) 
				{ 
					dist[i][j] = dist[i][k] + dist[k][j]; 
					pPath[i][j] = pPath[k][j]; 
				}
			}
		}
	}
}

2. 复杂度分析

2.1 Dijkstra

时间复杂度:

时间复杂度为 O ( N 2 ) O(N^2) O(N2),这里的 N N N 代表图中顶点的数量。具体原因如下:

  • 使用邻接矩阵存储图时,每次从未确定最短距离的顶点中找到距离最小的顶点需要 O ( N ) O(N) O(N)的时间,而总共要进行 N N N次这样的操作。对于每个确定了最短距离的顶点,更新其邻接顶点的距离也需要 O ( N ) O(N) O(N)的时间,因为需要遍历所有顶点。所以总的时间复杂度为 O ( N 2 ) O(N^2) O(N2)

空间复杂度:

空间复杂度为 O ( N ) O(N) O(N)。主要原因是需要存储每个顶点到源点的距离以及该顶点是否已确定最短距离等信息。对于有 N N N 个顶点的图,这些信息总共需要 O ( N ) O(N) O(N) 的空间。具体来说:

  • 需要一个标记数组来记录每个顶点是否已确定最短距离,这个数组的大小也为 N N N
  • 需要一个数组来存储每个顶点到源点的距离,这个数组的大小为 N N N

2.2 Bellman-Ford

时间复杂度:

总体时间复杂度为 O ( N × E ) O(N\times E) O(N×E),其中 N N N 是图中顶点的数量, E E E 是图中边的数量。

  • 分析:Bellman-Ford算法需要对图中的边进行 N − 1 N - 1 N1 轮遍历,每一轮遍历所有的边,以松弛操作来更新最短路径。对于每一轮,遍历所有边的时间复杂度为 O ( E ) O(E) O(E),而总共进行 N − 1 N - 1 N1 轮,所以时间复杂度为 O ( N × E ) O(N\times E) O(N×E)
  • 当使用邻接矩阵实现时,遍历图中的所有边的时间复杂度变为 O ( N 2 ) O(N^2) O(N2),从而导致上述代码的时间复杂度变为 O ( N 3 ) O(N^3) O(N3)

空间复杂度:

空间复杂度为 O ( N ) O(N) O(N)

  • 分析:主要需要存储每个顶点到源点的最短距离,以及一些辅助信息,这些信息总共需要 O ( N ) O(N) O(N) 的空间。对于有 N N N 个顶点的图,存储每个顶点的最短距离需要 N N N 个空间,同时可能还需要一些额外的空间来存储中间状态等信息,但在整体空间复杂度中,占主导地位的仍然是存储顶点最短距离的空间,所以空间复杂度为 O ( N ) O(N) O(N)

2.3 Floyd-Warshall

时间复杂度:

总体时间复杂度为 O ( N 3 ) O(N^3) O(N3),其中 N N N 是图中顶点的数量。

  • 分析:Bellman-Ford算法需要对图中的边进行 N − 1 N - 1 N1 轮遍历,每一轮遍历所有的边,以松弛操作来更新最短路径。对于每一轮,遍历所有边的时间复杂度为 O ( E ) O(E) O(E),而总共进行 N − 1 N - 1 N1 轮,所以时间复杂度为 O ( N × E ) O(N\times E) O(N×E)
  • 当使用邻接矩阵实现时,遍历图中的所有边的时间复杂度变为 O ( N 2 ) O(N^2) O(N2),从而导致上述代码的时间复杂度变为 O ( N 3 ) O(N^3) O(N3)

空间复杂度:

空间复杂度为 O ( N ) O(N) O(N)

  • 分析:主要需要存储每个顶点到源点的最短距离,以及一些辅助信息,这些信息总共需要 O ( N ) O(N) O(N) 的空间。对于有 N N N 个顶点的图,存储每个顶点的最短距离需要 N N N 个空间,同时可能还需要一些额外的空间来存储中间状态等信息,但在整体空间复杂度中,占主导地位的仍然是存储顶点最短距离的空间,所以空间复杂度为 O ( N ) O(N) O(N)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2070782.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何使用ssm实现珠宝首饰交易平台开发

TOC ssm101珠宝首饰交易平台开发jsp 第一章 绪 论 1.1背景及意义 系统管理也都将通过计算机进行整体智能化操作&#xff0c;对于珠宝首饰交易平台所牵扯的管理及数据保存都是非常多的&#xff0c;例如管理员&#xff1b;主页、个人中心、用户管理、商品分类管理、商品信息管…

【Material-UI】Radio Group中的 Label Placement 属性详解

文章目录 一、Radio Group 组件概述1. 组件介绍2. Label Placement 属性的作用 二、Label Placement 属性的基本用法三、Label Placement 属性详解1. 标签位置的选择2. 如何在实际项目中选择标签位置 四、Label Placement 属性的实际应用场景1. 表单布局中的应用2. 符合用户习惯…

多进程多线程及之间通信机制

目录 前言 一、多进程与多线程 1. 多进程 多进程的特点 多进程的应用场景 2. 多线程 多线程的特点 多线程的应用场景 3. 多进程与多线程的对比 二、进程与线程之间的通信机制 1. 进程间通信&#xff08;IPC, Inter-Process Communication&#xff09; 2. 线程间通信…

Golang | Leetcode Golang题解之第365题水壶问题

题目&#xff1a; 题解&#xff1a; type pair struct {x, y int }func canMeasureWater(jug1Capacity int, jug2Capacity int, targetCapacity int) bool {//剪枝if jug1Capacityjug2Capacity < targetCapacity {return false}var (dfs func(x, y int) bool // jug1有x水…

NC包含min函数的栈

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 描述 定义栈的数据…

基于STM32开发的智能水箱液位控制系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 系统初始化液位监测与控制水泵控制与状态显示Wi-Fi通信与远程监控应用场景 家庭用水系统的液位控制工业水箱的液位管理常见问题及解决方案 常见问题解决方案结论 1. 引言 智能水箱液位控制系…

线程池详解(建议收藏)

概念 线程池&#xff08;Thread Pool&#xff09;是一种基于池化技术的多线程处理形式&#xff0c;用于管理线程的创建和生命周期&#xff0c;以及提供一个用于并行执行任务的线程队列。线程池的主要目的是减少在创建和销毁线程时所花费的开销和资源&#xff0c;提高程序性能&…

C# messagePack对类(class)序列化简单示例

c# messagepack对类&#xff08;class&#xff09;序列化的简单示例 0 引言1 示例2 纠错3 结语 0 引言 MessagePack是一种高效的二进制序列化格式&#xff0c;它可以在C#中用于序列化和反序列化对象。与其他序列化格式相比&#xff0c;如JSON和XML&#xff0c;MessagePack的编…

西安电子高速PCB学习(五)

感抗&#xff08;Inductive Reactance&#xff09;和容抗&#xff08;Capacitive Reactance&#xff09;是电感和电容在交流电路中对电流产生阻碍的特性。这两个概念源于交流电路中&#xff0c;电感和电容对交流电流的相应反应。 感抗&#xff08;Inductive Reactance&#xf…

如何使用ssm实现电脑配件销售系统的设计与实现

TOC ssm128电脑配件销售系统的设计与实现jsp 第一章 绪 论 1.1背景及意义 系统管理也都将通过计算机进行整体智能化操作&#xff0c;对于电脑配件销售系统所牵扯的管理及数据保存都是非常多的&#xff0c;例如管理员&#xff1b;主页、个人中心、用户管理、商品分类管理、商…

PyTorch升级之旅——主要组成模块

本文仅作为个人学习记录使用 文章目录 前言 一、深度学习的简单流程 二、基本配置 三、数据读入 四、模型构建 五、模型初始化 六、损失函数 七、训练和评估 八、可视化 九、PyTorch优化器 总结 前言 学习链接&#xff1a;第三章&#xff1a;PyTorch的主要组成模块…

推荐几个缓解工作心情的VS Code插件

vs-cats 使用了这个插件&#xff0c;在写到"cat"/"Cat"单词时&#xff0c;可以在单词左边出现猫猫表情&#xff0c;而且在鼠标停靠在单词附近时还能显示猫猫图片 vscode-pets 使用这个插件可以在VS Code中养很多电子小动物&#xff0c;可以饲养的宠物有&a…

跟《经济学人》学英文:2024年08月24日这期 How to attract Indian tourists

How to attract Indian tourists Destinations are competing for the travelling rupee 原文&#xff1a; INDIANS ARE on the move. In 2019 international departures from India hit 27m, a number that will surely be exceeded this year and is predicted to rise t…

2024年好用的4款电脑录屏工具清单。

如果你需要录制教学视频、操作演示、记录游戏等等&#xff0c;但不知道使用哪些软件可以实现&#xff0c;那你一定要看看这个文章。因为我帮助大家筛选了4款能够用于电脑屏幕录制的高效工具。 1、福昕电脑REC 直达&#xff1a;www.foxitsoftware.cn/REC/ 这个软件是很多必备的…

电商api接口进行数据采集获取多平台商品价格

在电商运营中&#xff0c;从品牌角度来看&#xff0c;品牌方通过接口进行数据采集&#xff0c;获取多渠道商品价格信息的这一行为&#xff0c;能为品牌方带来诸多好处&#xff1a; 及时准确&#xff1a;API接口能为品牌提供实时数据&#xff0c;这意味着企业可以即时获取最新的…

Transformer总结(三):组件介绍(位置编码,多头注意,残差连接,层归一化,基于位置的前馈网络)

文章目录 一、位置编码1.1 介绍1.2 简单探讨 二、多头注意力2.1 一般的多头注意力机制2.2 解码器中的掩码多头注意力机制&#xff08;Look-ahead Mask&#xff09; 三、残差连接四、层归一化4.1 对比不同的Normalization4.2 Batch Normalization的实现4.3 Layer Normalization的…

vue3使用simple-mind-map,分分钟开发思维导图

这几天又来了新需求&#xff0c;老板想在系统里一眼可以看到所有部门的相关信息&#xff0c;并且可以编辑&#xff0c;分配任务。所以需要实现一个可编辑的思维导图页面。 思维导图&#xff1f;感觉很复杂的样子&#xff0c;这种很牛p的东西应该不是我三两天就能手写搞定的&am…

I2C代码硬件实现

环境 芯片:STM32F103ZET6 库&#xff1a;来自HAL的STM32F1XX.H 原理图 有图可知SCL和SDA两条线接到了PB10和PB11 芯片的复用功能是I2C2 代码 Driver_I2C.h #include "Driver_I2C.h"void Driver_I2C_Init(void) {/* 1. 打开引脚和片上外设的时钟 I2C2 PB10 PB11 …

【提示学习论文】AAPL: Adding Attributes to Prompt Learning for Vision-Language Models

AAPL: Adding Attributes to Prompt Learning for Vision-Language Models&#xff08;2024CVPR&#xff09; 问题&#xff1a;在unseen class上&#xff0c;性能提升有限解决&#xff1a;在提示学习中引入对抗性标记嵌入adversarial token embedding&#xff0c; 将低层次视觉…

SOMEIP_ETS_060: SD_Discover_Port_and_IP

测试目的&#xff1a; 验证设备&#xff08;DUT&#xff09;能够响应测试器发出的多播FindService消息&#xff0c;并返回一个单播OfferService消息&#xff0c;列出所有必要的IP地址和端口&#xff0c;以满足与DUT的所有可能通信需求。 描述 本测试用例旨在检查DUT是否能够…