最短路入门(Dijkstra、Floyd)
1 最短路问题是什么?
最短路问题(Shortest Path Problem, SPP
)是图论中的一个经典问题,旨在找到图中两点之间的最短路径。这个问题在计算机科学、运筹学以及网络设计等领域有着广泛的应用。
1.1 定义
给定一个图 G = (V,E),其中 V 是顶点集合,E 是边集合,每条边都有一个相应的权重 w (e∈E)。最短路问题就是要找到图中两个顶点 u 和 v 之间的最短路径,即权重之和最小的路径。
1.2 分类
最短路问题可以分为两大类:
-
无向图的最短路问题:在这种情况下,图中每条边都没有方向,w(e) 通常表示边 e 的长度或成本。
-
有向图的最短路问题:在有向图中,边具有方向,从一个顶点指向另一个顶点,此时 w(e) 可能表示沿边 e 行驶的距离或时间等。
2 最短路问题怎么做?
由于无向图的最短路操作可通过转换有向图的最短路操作得到,所以我们在这里只谈论有向图的最短路操作。(其实是本蒟蒻太菜了 [哭])
2.1 单源最短路(Dijkstra)
2.1.1 是什么?
单源最短路径(Single-Source Shortest Path)问题是指在一个加权图中,找出从单一源点(起始点)到图中所有其他顶点(目标点)的最短路径。这里的“最短”通常指的是路径的总权重最小,即经过的边数最少或者所花费的时间最短,取决于图的加权方式。
2.1.2 Dijkstra
定义:
Dijkstra算法是一种具有贪心、dp
思想的算法,主要用于解决加权图中从单一源顶点到所有其他顶点的最短路径问题。
它通过不断地选择未访问顶点中距离最小的顶点,并更新其所有邻接点的最短路径长度,直至找到所有顶点的最短路径。
该算法保证了得到的最优解,时间复杂度为O(V^2)
【其中V是顶点的数量;这是因为对于每个顶点,我们需要遍历整个顶点集合来找到下一个最短路径候选顶点】;但如果使用优先队列可以优化到O(E + V*logV)
【其中E是边的数量;这是因为对于每个顶点,我们可能需要处理最多E条边,每处理一条边,我们可能会在优先队列中进行VlogV
次的操作(因为每个顶点最多被弹出VlogV
次)】。
实现:
步骤(文字描述):
-
初始化:首先,给图中的所有顶点赋一个初始距离值,通常将起点设置为0,其他所有的顶点的距离设置为无穷大inf。
-
选择最近顶点:在遍历数组中找到距离最小的顶点。
-
更新邻接顶点的距离:将已访问顶点的所有邻接顶点的距离 更新为 MIN(当前已访问顶点的距离 + 当前邻接顶点与已访问顶点之间的边权值,该邻接顶点的距离)。
-
重复步骤2和3,直至所有顶点都被访问到,此时算法结束,得到了从起点到所有顶点的最短路径。
提供两道模板题:
朴素的Dijkstra,数组循环实现。
代码如下:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 9;
// Dijkstra
// 单源最短路.
// d[i]:表示st到i的最短距离
// 思想:greedy:每次找最近点进行拓展
// dp:选取最优方案
// ,每个点只拓展一次,且拓展时已为最短距离
struct Edge
{
int x,w;
};
vector<Edge> g[N];
ll d[N];
int n,m;
void dijkstra(int st)
{
memset(d,0x3f,sizeof(ll) * (n + 1));
d[st] = 0;
bitset<N> vis; // 表示该点已经拓展过
for(int i = 1;i <= n;++ i)
{
// 找出最小的点(距源点最小的点)
int u = 1;
for(int j = 1;j <= n;++ j)
{
if(vis[u] || (!vis[j] && d[j] < d[u])) u = j;
}
vis[u] = true; // 表示u已经拓展过
// 此时d[u]已为最优的
for(auto &[v, w] : g[u])
{
if(!vis[v] && d[v] > d[u] + w) d[v] = d[u] + w;
}
}
}
void solve()
{
cin >> n >> m;
for(int i = 1;i <= m;i ++)
{
int u,v,w;cin >> u >> v >> w;
if(u != v) g[u].push_back({v,w});
}
dijkstra(1);
cout << (d[n] >= 0x3f3f3f3f ? -1 : d[n]) << '\n';
}
int main(void)
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int _ = 1;// cin >> _;
while(_ --)
{
solve();
}
return 0;
}
使用优先队列进行优化:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 9;
// Dijkstra
// priority_queue
// 单源最短路.
// d[i]:表示st到i的最短距离
// 思想:greedy:每次找最近点进行拓展
// dp:选取最优方案
// ,每个点只拓展一次,且拓展时已为最短距离
struct Edge
{
ll v,w; // 出点 权值/距离
bool operator < (const Edge & u)const
{
return w == u.w ? v < u.v : w > u.w;
}
};
vector<Edge> g[N];
ll d[N]; // 距离
int n,m;
void dijkstra(int st)
{
memset(d,0x3f,sizeof(ll) * (n + 1));
d[st] = 0;
bitset<N> vis; // 表示该点已经拓展过
priority_queue<Edge> pq; // 优先队列默认是大根堆,重载改写,队首就是距离源点最近的点
pq.push({st,d[st]});
while(pq.size()) // 当队列为空,说明所有点的最短路都已找到
{
int x = pq.top().v; // 这就是距离源点最近的点
pq.pop();
if(vis[x]) continue; // 已拓展过的点,跳过,考虑到重边自环
vis[x] = true; // 改为拓展过
for(auto &[y , w] : g[x])
{
if(!vis[y] && d[y] > d[x] + w)
{
d[y] = d[x] + w;
pq.push({y,d[y]});
}
}
}
}
void solve()
{
cin >> n >> m;
// 建图
for(int i = 1;i <= m;i ++)
{
int u,v,w;cin >> u >> v >> w;
if(u != v)g[u].push_back({v,w});
}
dijkstra(1);
// 若有的点距离inf则说明没找到 输出-1
cout << (d[n] >= 0x3f3f3f3f ? -1 : d[n]) << '\n';
}
int main(void)
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int _ = 1;// cin >> _;
while(_ --)
{
solve();
}
return 0;
}
尽管Dijkstra算法适用于各种图结构(包括有向图和无向图),并且对大规模图的处理较为高效(时间复杂度可优化到O(E + VlogV)
);但它不能处理包含负权边的图,而且若要存储每个顶点的最短路径信息,对于大规模图来说,内存消耗较大。
另外需要注意的是,尽管Dijkstra算法不能处理负权边,但对于一些特殊场景,例如存在负权边的图中,可以先将其转化为正权边,再使用Dijkstra算法进行计算。
此外,若想继续了解处理包含负权边的图的最短路算法,可自行查阅Bellman-Ford算法,该算法可以处理包含负权边的图。
2.2 多源最短路(Floyd)
2.2.1 是什么?
多源最短路径问题(Multi-source shortest path problem)是图论中的一个经典问题,它要求在一个加权图中找到从一个给定的多个源点到其它所有顶点的最短路径。与单源最短路径问题(如Dijkstra算法解决的问题)不同,多源最短路径问题考虑的是从多个起点出发的情况。
Floyd-Warshall算法的时间复杂度是O(V^3),其中V是图中顶点的数量。这是因为算法需要对每一对顶点计算最短路径,并且每次计算可能涉及到所有顶点。因此,对于大型图,Floyd-Warshall算法会非常慢。
2.2.2 Floyd
定义:
Floyd-Warshall算法是一个计算图中所有顶点对之间最短路径的算法。它适用于加权图中,包括有向图和无向图,并且可以处理图中包含负权边的特殊情况(然而,如果图中存在负权回路,即总权重为负的环,Floyd-Warshall算法可能会报告不存在最短路径,因为在负权回路中,路径可以无限地变短)
实现:
Floyd-Warshall算法的基本思想是逐步推算出更长距离的最短路径。它考虑了所有顶点对之间的路径,并更新这些路径的最短距离,也就是dp。
步骤(文字描述):
-
初始化:创建一个二维数组d,其中d[i] [j]表示从顶点i到顶点j的距离,如果i和j之间没有直接连接的边,则d[i] [j]设置为无穷大。
-
逐步推算:对于图中的每一个顶点k,通过考虑顶点i到顶点k再到顶点j的路径,比较并更新d[i] [j]的值,如果d[i] [k] + d[k] [j] < d[i] [j],则将d[i] [j]更新为d[i] [k] + d[k] [j]。
-
重复步骤2,直到所有的顶点对之间的最短路径都被计算出来。
代码如下:
这里提供一道模板题:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 333;
// Floyd
// 多源最短路
// 思想:dp
ll d[N][N]; // d[i][j]表示i到j的最短路
int n,m,q;
void solve()
{
cin >> n >> m >> q;
memset(d,0x3f,sizeof(d));
// 建图
for(int i = 1;i <= m;i ++)
{
ll u,v,w;cin >> u >> v >> w;
d[u][v] = min(d[u][v],w);
}
// 初始化,考虑到自环,重边
for(int i = 1;i <= n;i ++) d[i][i] = 0;
// 注意循环一定是严格按照 “中转点-起点-终点” 进行loop
for(int k = 1;k <= n;k ++) // 中转点
{
for(int i = 1;i <= n;i ++) // 起点
{
for(int j = 1;j <= n;j ++) // 终点
{
d[i][j] = min(d[i][j],d[i][k] + d[k][j]);
}
}
}
while(q --)
{
int u,v;cin >> u >> v;
cout << (d[u][v] >= 0x3f3f3f3f ? -1 : d[u][v]) << '\n';
}
}
int main(void)
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int _ = 1;// cin >> _;
while(_ --)
{
solve();
}
return 0;
}
对于Floyd的状态转移为何严格按照“中转点-起点-终点” 进行loop,本蒟蒻这里不进行严格证明推导(本蒟蒻太菜了),说不明白。请您移步。
Floyd-Warshall算法可以一次计算出图中所有顶点对之间的最短路径;并且十分灵活,可以处理带有负权边的图,即使是那些权重大于零的边,也可以使用该算法;而且如果图中存在负权回路,即总权重为负的环,Floyd-Warshall算法能够检测到这一点,并报告不存在最短路径。
但是,Floyd-Warshall算法的的时间复杂度为O(V^3),对于大型图,这个算法可能会非常慢;而由于算法需要计算所有顶点对之间的最短路径,所以当图稀疏时,即顶点之间的边很少时,算法的效率会很低。
写在最后:本蒟蒻目前大一,目前是在StarryCoding学习算法相关知识,该平台是个面向计算机专业学生的综合学习与刷题平台,每周还有多个比赛,希望大家来学习进步!!!
官网:starrycoding点com