c++图论————最短路之Floyd&Dijkstra算法

目录

一,Floyd算法

1)简介及代码

2)例题

蓝桥杯官网------蓝桥公园

题目描述

输入描述

输出描述

输入输出样例

[示例 1](#示例 1)

代码详解:

二,Dijkstra算法

1)简介

2)代码以及相关细节详细解释

3)补充一个困扰我比较久的问题(虽然是个小问题):补充一个困扰我比较久的问题(虽然是个小问题):)

[图的存储里为什么用vector g[N]而不是vector g???](#图的存储里为什么用vector g[N]而不是vector g???)

4)例题

蓝桥杯官网------蓝桥王国

问题描述

输入描述

输出描述

输入输出样例

[示例 1](#示例 1)

代码详解:

点赞关注,脱单暴富!

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

一,Floyd算法

1)简介及代码

先普及一下:

啥是单源最短路?啥是全源最短路?

1.单源最短路(SSSP):指定一个固定的"起点节点",计算该起点到图中所有其他节点的最短路长度及路径本身

2.全源最短路(APSP):计算图中每一对节点(u,v)之间的最短路径

1.Floyd:适合稠密图(点少,边多)

Floyd算法是一种求解"多源最短路"问题的算法,在Floyd算法中,图一般用领接矩阵来存储(n<=500),边权可正可负(但不允许负环),负环的定义:负环是指图中存在一个环(从某个顶点出发,经过若干条边后能回到该顶点),且构成这个环的所有边的权重之和为负数。若出现负环,则会一直绕圈,直至无穷小。

利用动态规划思想,逐步求解出任意两点之间的最短距离,我们需要准备的东西很少,只要一个d数组:int d[N][N][N],初始为正无穷。

d[k][i][j]表示路径(除去起点和终点)中编号最大的点编号<=k的情况下,点i到点j的最短距离,

但是实际上k这一维度是可以优化掉的,所以直接用int d[i][j];表示考虑到当前情况下,点i到点j的最短距离

简单概述:如果从 A 到 B 经过中间点 C 比直接走更近,那就更新 A 到 B 的距离。

具体来说,它的思路很像我们平时找路:

先记下所有点之间的直接距离(比如 A 到 B 直接走要 10 分钟),然后尝试用第一个点当 "中转站",看看有没有更短的路(比如 A-->X-->B 只要 8 分钟,就把 A 到 B 的距离改成 8)

再换第二个点当中转站,继续检查所有路线是否能更短。

把每个点都当一次中转站后,最后得到的就是所有点之间的最短距离。

这个算法的好处是简单直接,不管图里的边是正数还是负数(只要没有绕一圈反而距离变短的 "负环"),都能算出结果。缺点是效率不算太高,适合处理点比较少的图(比如几百个点)。

在这张图中,就表示中转编号不超过4的1-->6最短路径(可以是1->2->4->6,也可以是1->3->6)

就相当于中转编号不超过3的1-->3-->6最短路径和中转编号不超过4的1-->2-->4-->最短路径取最小的。

代码非常简单:

cpp 复制代码
for (int k = 1; k <= n; k++)//枚举中转点
{
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; i <= n; j++)
		{
			d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
		}
	}
}

在 Floyd 算法中,k(中转点)必须放在外层循环,核心原因是由算法的动态规划逻辑决定的

------ 它要保证:当用k作为中转点时,所有 "经过前k-1个点" 的最短路径已经计算完成。对于每一个中转站k点,都要遍历所有的i,j起始点。

通过这段代码我们不难看出,Floyd 算法的时间复杂度为 O (n^3),是比较高的,所以一般只能处理 n <= 500 的问题。这也是一个提示,当遇到 n<=500 的图论问题时,可以考虑使用 Floyd 算法。

2)例题

蓝桥杯官网------蓝桥公园

题目描述

小明喜欢观景,于是今天他来到了蓝桥公园。已知公园有 N 个景点,景点和景点之间一共有 M 条道路。小明有 Q 个观景计划,每个计划包含一个起点 st 和一个终点 ed,表示他想从 st 去到 ed。但是小明的体力有限,对于每个计划他想走最少的路完成,你可以帮帮他吗?

输入描述

输入第一行包含三个正整数 N,M,Q第 2 到 M+1 行每行包含三个正整数 u,v,w,表示 u↔v 之间存在一条距离为 w 的路。第 M+2 到 M+Q−1 行每行包含两个正整数 st,ed,其含义如题所述。

1≤N≤400,1≤M≤N×(N−1)/2​,Q≤10^3,1≤u,v,st,ed≤n,1≤w≤10^9

输出描述

输出共 Q 行,对应输入数据中的查询。若无法从 st 到达 ed 则输出 −1。

输入输出样例

示例 1

输入:

复制代码
3 3 3
1 2 1
1 3 5
2 3 2
1 2
1 3
2 3

输出:

复制代码
1
3
2

代码详解:

cpp 复制代码
#include <iostream>
using namespace std;
using ll=long long;
const ll N=409;
const ll inf=2e18;
ll dp[N][N];

int main()
{
  ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
  int n,m,q;cin>>n>>m>>q;
  //初始化距离为无穷大
  for(int i=1;i<=n;i++)
  {
    for(int j=1;j<=n;j++)
    {
      dp[i][j]=inf;
    }
  }
  //自己到自己的距离是0
  for(int i=1;i<=n;i++) dp[i][i]=0;
  //更新单边距离
  for(int i=1;i<=m;i++)
  {
    //int u,v,w;cin>>u>>v>>w;数据类型要设置为相同的ll!!!
    ll u,v,w;cin>>u>>v>>w;
    dp[u][v]=min(dp[u][v],w);
    dp[v][u]=min(dp[v][u],w);
  }
  //更新所有点的距离:
  for(int k=1;k<=n;k++)
  {
    for(int i=1;i<=n;i++)
    {
      for(int j=1;j<=n;j++)
      {
        dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
      }
    }
  }
  while(q--)
  {
    int x,y;cin>>x>>y;
    cout<<(dp[x][y]==inf?-1:dp[x][y])<<endl;//注意题目要求!
  }
  return 0;
}

二,Dijkstra算法

1)简介

2,Dijkstra:用于稀疏图(点多,边少)

Dijkstra,迪杰斯特拉,又译做迪杰斯彻,荷兰计算机科学家,1972年图灵奖获得者。

Dijkstra 算法是一种高效的处理非负边权的 "单源最短路" 问题的算法,例如求出所有点距离源点 1 的距离。可以说 Dijkstra 是最重要的图论算法之一。

Dijkstra 算法利用了贪心和动态规划的思想,也有多个版本:朴素版、堆优化版。

这里直接讲解 Dijkstra 算法的堆优化版本 (priority_queue 优先队列实现,最常用,最高效)。朴素版相比堆优化版没有任何优势。

堆优化版本的时间复杂度为 O (nlogn),Dijkstra 算法中,图采用邻接表的方式存储。

2)代码以及相关细节详细解释

具体代码:

cpp 复制代码
// 算法需要准备的东西:
int d[N];//d[i]表示距离源点的最短距离
bitset<N>vis;//表示某个点是否走过,按照dijkstra的贪心思想,第一次走到的时候得到的距离一定是最短距离,所以一个点
//不可能走第二次
struct Node
{
	int x, w;//x表示点编号,w表示源点到x的最短距离
	/*重载<运算符:
核心目的:适配 C++ 优先队列(priority_queue)的排序规则,实现小顶堆效果。
C++ 优先队列默认是大顶堆(按<运算符排序,堆顶是最大元素),而 Dijkstra 算法需要每次取出 "当前距离源点最近的顶点"(小顶堆,堆顶是最小元素),因此需要重载<运算符改变排序逻辑:
排序规则:先按w(距离)降序排列(w > u.w),若距离相等,则按顶点编号x升序排列(x < u.x,此为补充规则,不影响算法核心逻辑)。
效果:优先队列中,w越小的Node越靠近堆顶,每次pq.top()能快速取出当前距离源点最近的顶点。*/
	bool operator<(const Node& u)const
	{
		return w == u.w ? x<u.x : w>u.w;//按照w降序,在优先队列中w最小的作为堆顶
	}
};
priority_queue<Node>pq;
//本质:priority_queue 底层是堆(二叉堆),不是普通队列,不遵循 FIFO 规则;
//在 C++ 中,priority_queue(优先队列)的核心特性就是:
//每次弹出(pop() 操作)的元素必然是当前优先队列的堆顶元素,这是优先队列的默认行为,不会改变。
//C++ 优先队列的堆顶默认是大顶堆(最大值堆),即堆顶是队列中 "最大" 的元素;但堆顶的定义(是最大值还是最小值)并非固定,
//而是由我们定义的排序规则(比如Node结构体中重载的 < 运算符)决定。
void dijkstra(int st)
{
	//初始化源点到i的距离为无穷
	for (int i = 1; i <= n; i++)
	{
		d[i] = inf;
	}
	pq.push({ st,d[st]=0});//源点到源点的距离为0
	while (pq.size())//只要队列不为空
	{
		int x = pq.top().x;
		int w = pq.top().w;
		pq.pop();//取出队头元素
		//用c++17的绑定化语法:auto[x,w]=pq.top();pq.pop();
		if (vis[x])continue;//如果走过,直接跳过
		vis[x] = true;//标记为走过
		for (const auto& [y, dw] : g[x])//x->y,边权为dw的边
		{
			if (d[x] + dw < d[y])//关键步骤,说明找到了一条更近的路
			{
				d[y] = d[x] + dw;
				pq.push({y,d[y]});
			}
		}
		/*在 C++17 之前,结构化绑定(structured binding)auto&[y,dw]的语法并不存在,
			需要显式地访问结构体的成员。对于你的代码,可以这样修改:
			for(const auto& edge : g[x])  // 遍历g[x]中的每个元素,用edge引用
		{
		  ll y = edge.x;             // 显式获取节点
		  ll dw = edge.w;            // 显式获取权重
		  if(d[x] + dw < d[y])
		  {
			d[y] = d[x] + dw;
			pq.push({y, d[y]});
		  }
		}*///*********非常重要!!!!!!!!!!!!!!!!!!!!!
	}
}

3)补充一个困扰我比较久的问题(虽然是个小问题):

图的存储里为什么用vector<int> g[N]而不是vector<int> g???

核心原因是图的结构特性决定了需要 "按顶点分组存储邻接边",直接说清楚:

  1. 先明确:这里的vector<int> g[N]是「邻接表」的存储方式

图的邻接表存储,本质是为每个顶点单独维护一个列表,记录该顶点的所有邻接顶点 / 边信息。

g[N]中的N:代表图的顶点数量(比如顶点编号是1~N)。

每个g[x](x是顶点编号):是一个vector,专门存储 "以x为起点的所有邻接边 / 顶点"。

  1. 为什么不能用vector<int> g?

如果只用一个vector<int> g(单个动态数组),无法区分 "哪些边属于哪个顶点"------ 所有顶点的邻接边会被混在一起存储,后续无法快速找到 "顶点x的邻接边"。

比如:

用g[N]:要找顶点3的邻接边,直接访问g[3]即可,时间复杂度 O (1) 定位。

用单个vector:需要遍历整个数组筛选 "属于顶点 3 的边",时间复杂度 O (m)(m 是边数),效率极低,完全无法支撑 Dijkstra 这类图算法的高效运行。

4)例题

蓝桥杯官网------蓝桥王国

问题描述

蓝桥王国一共有 N 个建筑和 M 条单向道路,每条道路都连接着两个建筑,每个建筑都有自己编号,分别为 1∼N 。(其中皇宫的编号为 1)

问:皇宫到每个建筑的最短路径是多少?

输入描述

输入第一行包含两个正整数 N,M。

第 2 到 M+1 行每行包含三个正整数 u,v,w,表示 u-->v 之间存在一条距离为 w 的路。

11≤N≤3×10^5,1≤m≤10^6,1≤ui​,vi​≤N,0≤wi​≤10^9。

输出描述

输出仅一行,共 N 个数,分别表示从皇宫到编号为 1∼N 建筑的最短距离,两两之间用空格隔开。(如果无法到达则输出 −1)

输入输出样例

示例 1

输入:

复制代码
3 3 
1 2 1
1 3 5
2 3 2

输出:

复制代码
0 1 3

代码详解:

cpp 复制代码
#include <iostream>
#include<queue>
#include<vector>
#include<bitset>
using ll=long long;
using namespace std;
const ll N=3e5+9;
const ll inf=2e18;
ll d[N],n,m;

struct Node
{
  //编号和到源节点的距离
  ll x,w;
  //重载>,编写排序规则
  bool operator <(const Node& u)const
  {
    return w==u.w?x<u.x:w>u.w;
  }
};
//vector<ll>g[N];注意vector存放的数据类型也是Node!
vector<Node>g[N];

void dijkstra(ll x)
{
  //初始化
  for(ll i=1;i<=n;i++) d[i]=inf;
  bitset<N>vis;
  priority_queue<Node>pq;
  //源节点入队
  pq.push({x,d[x]=0});
  //弹出节点
  while(pq.size())
  {
    ll x=pq.top().x;
    ll w=pq.top().w;
    pq.pop();//不要忘了出对
    //判断x是否已经走过
    if(vis[x]) continue;
    vis[x]=true;
    //拓展/松弛
    for(const auto&[y,dw]:g[x])
    {
      if(d[x]+dw<d[y]) d[y]=d[x]+dw;
      //将y入队
      pq.push({y,d[y]});
    }
  }
}

int main()
{
  ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=m;i++)
  {
    ll u,v,w;cin>>u>>v>>w;
    g[u].push_back({v,w});
  }
  dijkstra(1);
  cout<<'\n';
  for(int i=1;i<=n;i++)
  {
    cout<<(d[i]>=inf?-1:d[i])<<' ';
  }
  return 0;
}

点赞关注,脱单暴富!

相关推荐
WBluuue2 小时前
AtCoder Beginner Contest 437(ABCDEF)
c++·算法
郝学胜-神的一滴2 小时前
Linux 下循环创建多线程:深入解析与实践指南
linux·服务器·c++·程序人生·算法·设计模式
superman超哥2 小时前
仓颉语言中异常处理入门的深度剖析与工程实践
c语言·开发语言·c++·python·仓颉
ss2732 小时前
线程池优雅关闭:线程池生命周期管理:四种关闭策略的实战对比
java·jvm·算法
天呐草莓2 小时前
热传导方程
算法·matlab
wxdlfkj2 小时前
从坐标系重构到算法收敛:以高性能LTP传感器突破圆周分布孔组位置度的即时检测瓶颈
算法·重构
不能只会打代码2 小时前
蓝桥杯--生命之树(Java)
java·算法·蓝桥杯·动态规划·贪心
MobotStone3 小时前
三步高效拆解顶刊论文
算法
CreasyChan3 小时前
unity射线与几何检测 - “与世界的交互”
算法·游戏·3d·unity·数学基础