单元最短路问题

什么是单元最短路

在图论中,单元最短路(Single-Source Shortest Path)问题是指在带权有向图或无向图G=(V,E)中,从给定源顶点a出发,计算到图中所有其他顶点b∈V的最短路径及其权重 的问题。
关键定义

  1. 带权图:图G中的每条边e∈E都被赋予一个实数值w(e),称为边的权重或长度。
  2. 路径长度:对于从顶点a到b的一条路径p=(a,v₁,v₂,...,vₖ,b),其路径长度L§定义为该路径上所有边权重的总和。
  3. 最短路径:在所有从a到b的可能路径中,具有最小路径长度的路径称为a到b的最短路径。

而实现单元最毒单路,常见的方法有dijkstra算法bellman_ford算法spfa算法

常规版dijkstra算法:

dijkstra算法基于贪心思想的单元最短算法,求解的是"非负权图"上单元最短路径。
算法流程

  1. 准备工作:创建一个长度为 n 的数组 dist,dist[i] 表示从起点到 i 的最短路,创建一个长度为 n 的 bool 数组 st,st[i] 表示 i 位置是否已经确定了最短路。
  2. 初始化:dist[1] = 0,其余节点的 dist 值为无穷大,表示还没找到最短路
  3. 重复:在所有没有确定的最短路中,找出最短路长度最小的点u。打上确定最短路的标记后,对 u 的出边进行松弛操作。
  4. 重复步骤3,直至所有点都确定了最短路

(松弛操作:假设起点到a的最短路长度为dist[a],a到b的边权为w,那么如果dist[a] + w < dist[b],则更新b点的dist[b]。)
代码实现

cpp 复制代码
vector<pair<int, int>>edge[N];
void dijkstra()
{
	//初始化
	for (int i = 0; i <= n; i++)dist[i] = INF;
	dist[s] = 0;
	
	for (int i = 1; i <= n; i++)
	{
		//1.找出没有确定最短路的点中,当前最短路最小的点
		int a = 0;
		for (int j = 1; j <= n; j++)
			if (!st[j] && dist[j] < dist[a])
				a = j;

		//2.打上标记然后松弛
		st[a] = true;

		for (auto& t : edge[a])
		{
			int b = t.first, c = t.second;
			if (dist[a] + c < dist[b])
				dist[b] = dist[a] + c;
		}
	}
}
堆优化版dijkstra算法:

在常规版的基础上,用优先级队列维护待确定最短路的节点。
算法原理

  1. 准备工作:创建一个长度为 n 的 dist 数组,其中 dist[i] 表示从起点到 i 结点的最短路,创建一个长度为 n 的bool数组 st,其中 st[i] 表示 i 点是否已经确定了最短路,创建一个小根堆,维护更新后的结点(也就是需要确定最短路的结点)
  2. 初始化:dist[1] = 0,然后将 { 0,s }加到堆里;其余结点的dist值为无穷大,表示还没有找到最短路
  3. 重复:弹出堆顶元素,如果该元素已经标记过,就跳过;如果没有标记过,打上标记,进行松弛操作
  4. 重复上述操作,直到队列中没有元素为止
cpp 复制代码
priority_queue<pair<int, int>, vector<pll>, greater<pll>>heap;
//这里greater按照第一个关键字进行比较,对堆创建有问题的读者可以移步笔者之前队列相关文章
void dijkstra()
{
	//初始化
	memset(dist, 0x3f, sizeof dist);
	dist[s] = 0;
	heap.push({ 0,s });//距离,结点


	while (heap.size())
	{
		auto t = heap.top(); heap.pop();

		int a = t.second;
		if (st[a])continue;
		st[a] = true;

		for (auto& t : edge[a])
		{
			int b = t.first, c = t.second;
			if (dist[a] + c < dist[b])
			{
				dist[b] = dist[a] + c;
				heap.push({ dist[b],b });
			}
		}
	}
}

注:dijkstra算法不能出现负权边,只要出现就会出错!!!

bellman_ford算法:

为了处理负权边的相关问题,笔者这里还有一计。

不断尝试对图上每一条边进行松弛,直到所有的点都无法松弛。bellman_ford算法是一种基于松弛操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。
算法流程

  1. 准备工作:创建一个长度为n的dist数组,其中dist[i]表示从起点到i结点的最短路
  2. 初始化:dist[1] = 0,其余结点的dist值为无穷大,表示还没有找到最短路
  3. 重复:每次都对所有边进行一次松弛操作
  4. 重复上述操作,直到所有边都不需要松弛为止

最多重复多少次松弛操作?

在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少加1,而最短路的边数至少增加1,而最短边的边数最短为 n - 1.因此整个算法最多执行松弛操作 n - 1 轮。
时间复杂度为O(nm)

cpp 复制代码
vector<pair<int, int>>edge[N];
void bf()
{
	//初始化:
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;

	bool flag = false;//判断循环中有没有进行松弛操作
	for (int i = 1; i <= n; i++)
	{
		flag = false;
		for (int u = 1; u <= n; u++)//遍历所有边
		{
			for (auto& t : edge[u])
			{
				int v = t.first, w = t.second;
				if (dist[u] + w < dist[v])
				{
					dist[v] = dist[u] + w;
					flag = true;
				}
			}
		}
		if (flag)break;
	}
}
spfa算法:

(这是对BF算法进行优化、改良而来的)

在BF算法中,很多时候我们并不需要那么多无用的松弛操作:

**只有上一次被松弛的节点的出边,才有可能引起下一次的松弛操作。**因此,如果用队列来维护"哪些节点可能会引起松弛操作",就能只访问必要的边,时间复杂度就可以降低。
算法流程

  1. 准备工作:创建一个长度为 n 的 dist 数组,其中 dist[i] 表示从起点到 i 结点的最短路;创建一个长度为 n 的bool数组 st,其中 st[i] 表示 i 点是否已经在队列中。
  2. 初始化:dist[1] = 0,其余结点的dist值为无穷大,表示还没有找到最短路。
  3. 重复:每次拿出队头元素u,去掉在队中的标记,同时对u所有相连的点v进行松弛操作。如果结点v被松弛,那就放到队列中。
  4. 重复上述操作,直到队列中没有节点为止

代码实现

cpp 复制代码
vector<pair<int, int>>edge[N];
void spfa()
{
	//初始化:
	memset(dist, 0x3f, sizeof dist);
	dist[s] = 0;
	queue<int>q;
	q.push(s);
	st[s] = true;

	while (q.size())
	{
		auto a = q.front(); q.pop();
		st[a] = false;

		for (auto& t : edge[a])
		{
			int b = t.first, c = t.second;

			if (dist[a] + c < dist[b])
			{
				dist[b] = dist[a] + c;
				if (!st[b])//如果这个点能松弛且不在队列中
				{
					q.push(b);
					st[b] = true;
				}
			}
		}
	}
}

注意:虽然大部分情况下spfa算法跑的很快,但其极限情况下时间复杂度为O(nm),且将他卡到整个复杂度也表示很难,所以,没有负边权的情况下最好使用Dijkstra算法。

判断负环:

BF算法判断负环:
  1. 执行 n 轮松弛操作
  2. n 轮结束之后在判断一下第 n 轮是否存在松弛操作,如果有,则存在负环

代码实现

cpp 复制代码
vector<pair<int, int>>edge[N];
bool bf()
{
	memset(dist, 0x3f, sizeof dist);
	dist[s] = 0;

	bool flag = false;
	for (int i = 1; i <= n; i++)
	{
		flag = true;
		for (int u = 1; u <= m; u++)
		{
			for (auto& t : edge[u])
			{
				int a = t.first, b = t.second;
				if (dist[a] > dist[u] + b)
				{
					dist[a] = dist[u] + b;
					flag = true;
				}
			}
			if (!flag)break;
		}
	}
	if (flag)return false;//说明有负环
	else return true;
}
spfa算法判断负环:
  1. 维护一个 cnt 数组,表示从起点走到前点,经过了多少条边
  2. cnt[i] >= n,说明有环(cnt[i] 表示从起点走到 i 节点时经过了多少条边)

代码实现

cpp 复制代码
vector<pair<int, int>>edge[N];
bool spfa()
{
   //初始化:
   memset(dist, 0x3f, sizeof dist);
   memset(st, false, sizeof st);
   memset(cnt, 0, sizeof cnt);

   dist[s] = 0;
   queue<int>q;
   q.push(s);
   st[s] = true;
   cnt[s] = 0;
   
   while (q.size())
   {
   	int a = q.front(); q.pop();

   	for (auto& t : edge[a])
   	{
   		int b = t.first, c = t.second;
   		if (dist[b] > dist[a] + c)
   		{
   			dist[b] = dist[a] + c;
   			cnt[b] = cnt[a] + 1;
   			if (cnt[b] >= n)return true;//有负环

   			if (!st[b])
   			{
   				q.push(b);
   				st[b] = true;
   			}
   		}
   	}
   }
   return false;
}
相关推荐
崎岖Qiu2 小时前
leetcode380:RandomizedSet - O(1)时间插入删除和获取随机元素(数组+哈希表的巧妙结合)
java·数据结构·算法·leetcode·力扣·散列表
安卓开发者2 小时前
鸿蒙NEXT中SQLite数据库全面实战指南
数据库·sqlite·harmonyos
ZLRRLZ2 小时前
【数据结构】图
数据结构·算法·图论
Ada_疯丫头3 小时前
杰伊·温格罗教我数据结构与算法
算法
小白开始进步3 小时前
机器人集群调度算法简介与实现思路
算法·机器人
ajassi20003 小时前
开源 C++ QT Widget 开发(十六)程序发布
linux·c++·qt·开源
xuejianxinokok3 小时前
PostgreSQL 18 新功能:虚拟生成列
数据库·后端
好易学·数据结构3 小时前
可视化图解算法60: 矩阵最长递增路径
数据结构·算法·leetcode·力扣·递归·回溯算法·牛客
SamsongSSS3 小时前
JavaScript逆向SM国密算法
javascript·算法·逆向