c++图论——最短路之Johnson算法

目录

一,前言

1.几种常见的最短路算法:

2.代码:

3.代码简单介绍

一、es的核心定义

[二、es的对应 C++ 数据类型(两种常见写法)](#二、es的对应 C++ 数据类型(两种常见写法))

[写法 1:使用struct自定义边类型(兼容性更好,低版本 C++ 也支持)](#写法 1:使用struct自定义边类型(兼容性更好,低版本 C++ 也支持))

[写法 2:使用tuple元组(简洁,依赖 C++11 及以上)](#写法 2:使用tuple元组(简洁,依赖 C++11 及以上))

三,为什么之枚举n-1次

二,Johnson算法介绍

1.johnson算法:大致分为以下3步

2.re-weight

[核心原因:Dijkstra 算法的"局限性"------ 无法正确处理负权边(可处理非负权边)](#核心原因:Dijkstra 算法的“局限性”—— 无法正确处理负权边(可处理非负权边))

三,例题训练(本类题用得少,无论是比赛还是实际开发都少,只做思维锻炼即可)

蓝桥杯官网------小e的公路

问题描述

输入描述

输出描述

样例输入1

样例输出1

样例输入2

样例输出2

代码详解

注:本文所有题目均来自蓝桥杯官网公开真题,仅做算法学习,代码皆由本人做出来并附上解析!

一,前言

1.几种常见的最短路算法:

(1)dijkstra单源最短路,仅适用于正权图,时间复杂度o(nlogN)。

(2)Floyd全源最短路,仅适用于正权图,o(n^3)。

(3)还有一种常见的单源最短路算法,叫bellmanford算法,它可以计算负权图的单源最短路,且可以判断负环,但是时间复杂度较高,o(nm)。

Bellmanford算法的思路:松弛:用这条边去更新源点到出点的最短路径。用每条边去做松弛操作,进行n-1轮松弛,因为在n个点的图中一条路径最多由n-1条边组成,如果n-1轮松弛之后还能松弛,就说明存在负权环,否则就可以得到单源最短路。

因为在走最短路径时,遇到了负权环它的路径和就会不断减少,永远都会比之前少,直到负无穷

2.代码:

cpp 复制代码
ll h[N];//h[N]表示源点0到x的最短距离,通过bellmanford计算

bool bellmanford()
{
	//初始化h[]为正无穷
	for (int i = 1; i <= n; i++)h[i] = inf;
	//h[1]=0这个可以忽略
	h[0] = 0;
	//一共n个点,所以松弛n-1次,i仅用于计数
	for (int i = 1; i < n; i++)
	{
		//枚举所有边,尝试用这条边去松弛点x
		for (const auto& [x, y, w] : es)//es:vector
		{
			if (h[x] == inf)continue;
			//如果可以通过这条边松弛
			if (h[x] + w < h[y])
			{
				h[y] = h[x] + w;
			}
		}
	}
	//判断是否存在负权环
	for(const auto &[x,y,w]:es)
	{
		if (h[x] == inf)continue;
		//如果还有边可以松弛,那么说明存在负环(边权之和为负)返回false
		if (h[x] + w < h[y])return false;
	}
	//不存在负环,返回true
	return true;
}

3.代码简单介绍

一、es的核心定义

es 是一个存储图中所有边的容器(这里明确是vector向量容器) ,并且每一条边都包含了三个核心信息:x(边的起点)、y(边的终点)、w(这条边的权重,可正可负,对应 Bellman-Ford 算法的场景)。

简单来说,es就是整个图的 "边集合",Bellman-Ford 算法的核心就是通过枚举这个集合中的所有边来进行松弛操作。

二、es的对应 C++ 数据类型(两种常见写法)

代码中使用了 C++17 及以上支持的结构化绑定(const auto& [x, y, w] : es ,对应es的两种常见合法定义:

写法 1:使用struct自定义边类型(兼容性更好,低版本 C++ 也支持)
cpp 复制代码
// 1. 先定义边的结构体
struct Edge {
    int x;  // 起点
    int y;  // 终点
    long long w;  // 权重(用long long避免溢出,对应h[N]的ll类型)
};

// 2. 定义es容器(存储所有边)
vector<Edge> es;
写法 2:使用tuple元组(简洁,依赖 C++11 及以上)
cpp 复制代码
// 直接定义存储三元组的vector,顺序对应x、y、w
// 注意:tuple的元素类型要和x、y、w匹配,ll对应long long
vector<tuple<int, int, long long>> es;

三,为什么之枚举n-1次

Bellman-Ford 算法的n-1次枚举,本质是按 "边数" 逐层推进,逐步确定源点到各节点的最短路径,可以理解为 "迭代解锁":

  1. 第 1 次枚举所有边:只能确定 "源点直接可达" 的节点(即最短路径仅包含 1 条边的节点)的最短距离,这些节点的最短路径被 "解锁",无法确定需要更多边的节点;
  2. 第 2 次枚举所有边:基于第 1 次的结果,能确定 "最短路径包含最多 2 条边" 的节点的最短距离,进一步解锁更多节点;
  3. ...... 以此类推 :每多枚举 1 次,就能把最短路径的边数上限提升 1,直到第k次枚举,能确定最短路径包含最多k条边的节点;
  4. 第 n-1 次枚举所有边 :此时已经覆盖了 "最短路径包含最多n-1条边" 的所有可能情况,所有节点的最短距离都已经被最终确定,后续再枚举边也无法再更新(松弛)任何节点的最短距离了。

这些路径都是从源点开始的。

二,Johnson算法介绍

1.johnson算法:大致分为以下3步

1,设置超级源点,用bellmanford求单源最短路(所有点到超级源点的最短距离)得到"势能"h[],并且判断负环

2,在势能帮助下重新设置每条边的权重

3,跑n次dijkstra算法计算出所有点的单源最短路,即得到了全源最短路

2.re-weight

(图片来自蓝桥杯官网)

RE-weight顾名思义就是重新调整每条边的权重,对于一个图,我们先建立一个"super source node(超级源点)",用0号节点表示,并且将其与所有节点链接起来,边权为0。

接下来用bellmanford算法计算出超级源点的单源距离最短路,这个是很简单的(吗?)

由图,图中存在负权,但是注意,图中有负权并不一定代表图中有负环!

负环是个啥?

负环特指有向图中一个满足"边权之和为负数"的环

会导致:最短路径不存在------ 如果从源点出发,能到达一个负环,那么可以无限次绕这个负环循环 。每绕一圈,路径的总权值就会减少一次,最终路径长度会趋近于 负无穷

但是,有负权会影响什么呢?

核心原因:Dijkstra 算法的"局限性"------ 无法正确处理负权边(可处理非负权边)

这是重赋权(将边权转为非负)的根本原因:

  1. Dijkstra 算法的核心缺陷 :Dijkstra 算法基于 "贪心策略",它假设一旦某个节点被从优先队列中取出并标记为已访问(vis[x]==true),就已经找到了从源点到该节点的最短路径,后续无需再更新该节点的距离
  2. 负权边会打破这个假设:如果图中存在负权边,当一个节点被标记为已访问后,后续可能会通过一条包含负权边的路径,找到到该节点的更短距离,此时 Dijkstra 算法无法回溯更新,会得出错误的最短路径结果。
  3. 非负权边是 Dijkstra 算法的安全保障:当所有边权都是非负数时,贪心策略完全成立 ------ 被取出的节点的最短距离已经确定,不会被后续路径更新,算法能够正确求解,且效率极高。

简单来说:我们需要 Dijkstra 算法的高效性,但它无法处理负权边,因此需要将负权边转为非负权边,让 Dijkstra 算法可以安全运行。

所以要先更新所有边权变为非负数,之后才能求出单源最短路。

之后,假如存在负环,现在我们就得到了一个h[]数组,接下来我们令每条边的权重w(u,v)=w(u,v)+h[u]-h[v]

这样一定可以使得所有边权为正权,假如w(u,v)<0,那么必然有h[u]+w(u,v)>=h[v],于是有:w(u,v)+h[u]-h[v]>=0

在此基础上去对每一个点跑一遍dijkstra算法,即可求得对于每一个点的单源最短路

如何理解呢?

由图中的w(1,3)=-5,h[1]=0,h[3]=-5(由路径0-->1-->3可得)。

对w(1,3)进行操作:w(1,3)=w(1,3)+h[1]-h[3]=-5+0+5=0,成功变为非负数了。

求出来的这个距离叫d(u,v),实际上是进行了偏移之后的,要还原为真实的距离还要令:f(u,v)=d(u,v)-h[u]+h[v]

h他不是d,是势能而不是实际距离,例如:

假如有一条路径:u->x1->x2->......->xk->v,那么就有:d(u,v)=[w(u,x1)+h[u]-h[x1]](就是d(u,x1))+......+d(xk,v)

经变换可以得到:d(u,v)=(w(u,x1)+w(x1,x2)+......+w(xk,v))+h[u]-h[v]

即:d(u,v)=f(u,v)+h[u]-h[v],f(u,v)为u,v的真实距离

由此可见,h只与起始点,终点有关,类比于物理学的重力势能!

所以真实距离是我们计算出的d(u,v)-h[u]+h[v],这个真实距离可能是负数,如果要判断两点不存在路径的话,应该判断d而不是f!若不存在路径,则对应的d应该是inf(无穷)。

三,例题训练(本类题用得少,无论是比赛还是实际开发都少,只做思维锻炼即可)

蓝桥杯官网------小e的公路

问题描述

有一座城市非常特殊,所有道路都是单行道,并且每条道路的距离可能是负数。在该市有 n 个景点,m 条道路,请问对于每个景点,它到所有景点的最短距离与编号的乘积之和是多少?

输入描述

第一行输入两个整数 n, m (1 ≤ n ≤ 10^3, 1 ≤ m ≤ 2 × 10^3)。接下来 m 行,每行输入三个整数 u, v, w (1 ≤ u, v ≤ n, -10^9 ≤ w ≤ 10^9),表示存在一条从 u 到 v 的距离为 w 的道路。

输出描述

输出共 n 行,第 i 行 ansi​=∑ j=1n​j×dis[i][j]。

dis[u][v] 表示 u 到 v 的最短距离,如果无法到达则认为距离是 10^9。

如果图中存在负权环,则只输出一行 −1。

样例输入1

复制代码
6 8
1 2 4
2 6 -10
2 4 7
2 5 10
2 3 -2
3 6 -3
5 3 4
6 7 9

样例输出1

复制代码
92
1000000012
11999999982
17000000000
7000000018
15000000000

样例输入2

复制代码
6 8
1 2 4
2 6 -10
2 1 -7
2 5 10
2 3 -2
3 6 -3
5 3 4
6 7 9

样例输出2

复制代码
-1

代码详解

cpp 复制代码
#include <iostream>
#include<queue>
#include<bitset>
#include<vector>
using namespace std;
using ll=long long;
const int N=3e3+9;
const ll inf=1e9;//为什么设置为更大的数不行???????试了我半天
ll h[N],dis[N][N];//分别求势能,dis[i][j]表示i-->j的实际距离
int n,m;

struct edge
{
  ll u,v,w;
};
vector<edge>es;

struct Node
{
  ll v,w;
  //重载<,使得优先队列为小根堆
  bool operator<(const Node &u)const
  {
    return w==u.w?v<u.v:w>u.w;//按照w降序
  }
};

vector<Node>g[N];

bool bellmanford()
{
  //初始化;
  for(int i=1;i<=n;i++) h[i]=inf;
  h[0]=0;
  //此时有n+1个点,所以松弛n次:
  for(int i=1;i<=n;i++)
  {
    for(const auto &[u,v,w]:es)
    {
      //跳过无法从源点达到的
      if(h[u]==inf) continue;
      if(h[u]+w<h[v]) h[v]=h[u]+w;
    }
  }
  //判断负环;
  for(const auto &[u,v,w]:es)
  {
    if(h[u]==inf) continue;
    //假设还能松弛,说明存在负环
    if(h[u]+w<h[v]) return false;
  }
  return true;
}

void dijkstra(ll st)
{
  priority_queue<Node>pq;
  bitset<N>vis;
  static ll d[N];//d不能是全局变量,因为每次st(原点)都不同
  //初始化距离
  for(int i=1;i<=n;i++) d[i]=inf;
  pq.push({st,d[st]=0});
  while(pq.size())
  {
    ll v=pq.top().v;
    //要先出队
    pq.pop();
    //ll w=pq.top().w;
    if(vis[v]) continue;
    vis[v]=true;
    //pq.pop();
    for(const auto &[y,dw]:g[v])
    {
      if(d[v]+dw<d[y]) d[y]=d[v]+dw;
      pq.push({y,d[y]});
    }
  }
  //求最短距离(还原真实距离)
  for(int i=1;i<=n;i++)
  {
    if(d[i]==inf) dis[st][i]=d[i];
    else dis[st][i]=d[i]-h[st]+h[i];
  }
}

void solve()
{
  cin>>n>>m;
  for(int i=1;i<=m;i++)
  {
    ll u,v,w;cin>>u>>v>>w;
    es.push_back({u,v,w});//bellmanford
    g[u].push_back({v,w});//dijkstra
  }
  //先建立超级源点,之后再跑bellmanford求出势能
  for(int i=1;i<=n;i++) es.push_back({0,i,0});//表示从超级源点到i点的距离都是0
  //判断负环:
  if(!bellmanford())
  {
    cout<<-1<<endl;
    return;//程序终止
  }
  //此时求出了势能h
  //重新设置边权(在领接表中)
  for(int x=1;x<=n;x++)
  {
    for(auto &[y,w]:g[x]) w=w+h[x]-h[y];
  }
  //跑n次dijkstra,求出每个点的单源最短路(即所有点的全源最短路)
  for(int i=1;i<=n;i++) dijkstra(i);
  //得出最终距离:
  for(int i=1;i<=n;i++)
  {
    ll ans=0;
    for(int j=1;j<=n;j++) ans += 1ll * j * dis[i][j];
    cout<<ans<<endl;
  }
}

int main()
{
  ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
  solve();
  return 0;
}

点赞关注,脱单暴富

相关推荐
“抚琴”的人2 小时前
C#上位机观察者模式
开发语言·观察者模式·c#·上位机
思成Codes2 小时前
Go语言的多返回值是如何实现的?
开发语言·后端·golang
北极糊的狐2 小时前
MQTT报错:Exception in thread main java.lang.at io.github.pnoker.common.sdk.utils.ParseUtils.decodeHex
java·开发语言
Grassto2 小时前
Go 是如何解析 `import path` 的?第三方包定位原理
开发语言·golang·go module·go import
福大大架构师每日一题2 小时前
go-zero v1.9.4 版本发布详解:云原生适配升级与稳定性性能全面提升
开发语言·云原生·golang
蒙奇D索大2 小时前
【数据结构】排序算法精讲 | 交换排序全解:交换思想、效率对比与实战代码剖析
数据结构·笔记·考研·算法·排序算法·改行学it
sin_hielo2 小时前
leetcode 1351
数据结构·算法·leetcode
byte轻骑兵2 小时前
【安全函数】memmove_s ():C 语言内存安全迁移的守护者与 memmove 深度对比
c语言·开发语言·安全
睡醒了叭2 小时前
图像分割-传统算法-边缘分割
图像处理·opencv·算法·计算机视觉