对于一个有向图或是无向图,要求我们求一个点到另一个点的最短距离就叫做最短路。
对于一个图,我们通过判断是稀疏图还是稠密图来选择用什么方式存储图中的点之间的关系,一般来说边数越多越稠密。如果边数M与点数N的大小差不多的时候我们可以认为是稀疏图,边数M与N^2的大小差不多时可以认为时稠密图。
先看图,对于不同的情况我们要选取合适的算法:
下面一一介绍各个算法是如何实现的:
目录
二,堆优化 的dijkstra算法 时间复杂度为O(Mlog N)M边数,N点数】
四,spfa算法 时间复杂度一般为O(M),最坏为O(NM)
一,dijkstra朴素算法 时间复杂度为O(N^2)
对于稠密图我们一般选择dijkstra朴素算法
dijkstra算法是基于贪心的思想,从源点开始,我们每次选取距离源点最近点,然后以这个点去更新接下来的点,直至全部点都更新完,初始时要将每个点距离源点的距离初始化为极大值,最后如果还为极大值表示从源点无法走到这个点
dijkstra算法不能用于解决有负权边的情况
用邻接矩阵得方法实现得代码如图;
//给定n个点和m条边以及源点s求源点到第n个点的最短路径
#include<iostream> //dijkstra朴素算法求最短路径,时间复杂度为O(N^2),N为点的数量
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1010;
//dist表示每个点到源点的最短路径,v表示每个点是否被访问过
int dist[N],v[N];
int a[N][N];
//s表示源点,m表示边数,n表示点数
int n, s,m;
void dijkstra()
{
v[s] = 1;
dist[s] = 0;
//以源点开始更新离的最近的点的最短路径
for (int i = 0; i < n; i++)
dist[i] = min(dist[i], a[s][i]);
//遍历n次,将每个点都搜索一遍
for (int i = 0; i < n; i++)
{
int max1 = 0x3f3f3f3f;
int t = -1;
//找到离源点最短路径的点
for (int i = 1; i <= n; i++)
{
if (!v[i] && dist[i] < max1)
{
t = i;
max1 = dist[i];
}
}
if (t == -1)
break;
v[t] = 1;
//以这个点更新其他还没走过的点到源点的最短路径
for (int i = 1; i <= n; i++)
dist[i] = min(dist[i], dist[t] + a[t][i]);
}
}
int main()
{
scanf("%d%d%d", &n,&m, &s);
//要将每个点距离源点得距离和每个点到其他点得距离初始化为正无穷
memset(dist, 0x3f, sizeof dist);
memset(a, 0x3f, sizeof a);
for (int i = 0; i < m; i++)
{
int u, v,w;
cin >> u >> v >> w;
a[u][v] = min(a[u][v], w);
}
dijkstra();
printf("%d", dist[n]);
return 0;
}
二,堆优化 的dijkstra算法 时间复杂度为O(Mlog N)M边数,N点数】
对于稀疏图我们一般选择堆优化的dijkstra算法
对于朴素算法,我们要得到每个点距离源点的最近距离,要遍历全部的点,那就是O(N)的时间复杂度,对于这个地方我们就可以进行优化,用堆来存储每个点距离源点的最短距离,想得到最小的点,那就是O(logN)得
用邻接表得方法实现得代码如图:
#include<iostream> //堆优化得dijkstra算法,时间复杂度为O(MlogN)M为边数,N为点数
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
//first表示路径长度,second表示点
typedef pair<int, int>pii;
const int N = 2e5 + 10;
int n, m, s;
//w记录边得权值
int h[N], e[N], ne[N], w[N],idx;
//dist表示该点距离源点得最短距离,v表示该点是否被访问
int dist[N], v[N];
void add(int a, int b, int x)
{
//idx用于给边编号
//e[idx]表示第idx条边指向b
e[idx] = b;
ne[idx] = h[a];
//w表示第idx条边得权值为x
w[idx] = x;
h[a] = idx++;
}
void heap_dijkstra()
{
//建立一个小根堆,堆顶返回的点路径长度最小
priority_queue<pii, vector<pii>, greater<pii>>heap;
//将源点插入堆中
heap.push({ 0,s });
dist[s] = 0;
while (!heap.empty())
{
auto p = heap.top();
heap.pop();
int t = p.second;
if (v[t])
continue;
v[t] = 1;//已经找过的点要标记,后面不能再用了
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
heap.push({ dist[j], j });
}
}
}
}
int main()
{
scanf("%d%d%d", &n, &m, &s);
memset(dist, 0x3f, sizeof dist);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i++)
{
int u, v, x;
scanf("%d%d%d", &u, &v, &x);
add(u, v, x);
}
heap_dijkstra();
for (int i = 1; i <= n; i++)
cout << dist[i] << " ";
return 0;
}
三,bellman_ford算法 时间复杂度O(NM)
bellman_ford可以用于解决有负权边以及判断是否有负环的情况,同时对于求解在k条边内找到源点到目标点的最短距离这一类问题时,只有bellman_ford可以解决。
bellma_ford算法的核心思想就是遍历每条边,用遍历的边去更新每个点距离源点的最短路距离
代码如下:
//给定n个点m条边,限制在k条边内从点1到点n得最短路径
#include<iostream> //bellman_ford算法求最短路径
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 510, M = 100010;
//因为只用遍历每条边,所以可以直接用一个结构体来存储每条边,方便计算
//结构中表示从点a到点b有一条权值为w的边
struct stu
{
int a, b, w;
}edge[M];
int n, m, k;
//backup用于备份,dist表示每个点距离源点的最短距离
int backup[N], dist[N];
int bellman_ford()
{
dist[1] = 0;
//遍历k次表示源点到各个点经过k条边的最短距离
for (int i = 0; i < k; i++)
{
//每次遍历前都要备份一遍dist数组,这么做保证每次以一个点去更新后面的点是经过了k条边
memcpy(backup, dist, sizeof dist);
for (int j = 0; j < m; j++)
{
int a = edge[j].a, b = edge[j].b, w = edge[j].w;
dist[b] = min(dist[b], backup[a] + w);
}
}
//因为存在负权边,假设从点1到达不了点b和点n,但是从点b到点n存在负权边
//所以点n的权值会被更新,可能会比0x3f3f3f3f要小,至于这个界限取多少依据题意来设
if (dist[n] > 0x3f3f3f3f / 2)
return -1;
return dist[n];
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
memset(dist, 0x3f, sizeof dist);
for (int i = 0; i < m; i++)
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edge[i] = { a,b,w };
}
int t = bellman_ford();
if (t == -1)
puts("impossible");
else
printf("%d\n", t);
return 0;
}
四,spfa算法 时间复杂度一般为O(M),最坏为O(NM)
spfa算法就是用队列优化后的bellma_ford算法,一般情况下相对于bellman_ford和dijkstra都要快很多,但很容易被出题人将时间复杂度卡成O(NM),这时我们就要选择其他的算法了,spfa可用的情况很多,可用于有负权边的图,以及可以判断负环(当一个点被用过n次就可以判断有负环)
但是,spfa不能解决带有负权回路的图!!!!
需要判断负环是我们一般选取spfa算法,因为spfa比较高效相较于bellma_ford,在bellman_ford算法中,我们要更新每一条边,但其实不一定每一条边都会更新,这时我们就可以用一个队列来存储需要更新的点
代码如图:
#include<iostream> //spfa算法求最短路,可用于解决有负权边的图,但一定不能解决带有负环的图!!
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 2e5 + 10;
int n, m;
int h[N], e[N], ne[N], w[N], idx;
//dist表示距离源点的距离,v该点表示是否被访问过,cnt记录每个点距离源点的最短路所经过的边数
int dist[N], v[N],cnt[N];
//q存储更新了距离的点
queue<int>q;
//邻接表存储图
void add(int a, int b, int x)
{
e[idx] = b;
ne[idx] = h[a];
w[idx] = x;
h[a] = idx++;
}
void spfa()
{
q.push(1);
dist[1] = 0;
//判断负环时要将每个点都加入队列
//for (int i = 1; i <= n; i++)
//{
// v[i] = 1;
// q.push(i);
//}
while (!q.empty())
{
//取队头点
int t = q.front();
q.pop();
//将点出队,设置为未访问,方便下次继续入队更新其他点
v[t] = 0;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
//更新点
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
//如果没访问过就将这个点入队
if (!v[j])
{
cnt[j] ++;
//如果一个点入队出队超过了n次就说明有负环
if (cnt[j] > n)
{
printf("YES\n");
return;
}
v[j] = 1;
q.push(j);
}
}
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
memset(dist, 0x3f, sizeof dist);
for (int i = 0; i < m; i++)
{
int u, v, x;
scanf("%d%d%d", &u, &v, &x);
add(u, v, x);
}
spfa();
//点1距离每个点的最短距离
for (int i = 2; i <= n; i++)
cout << dist[i]<<endl;
return 0;
}
五,floyd算法 时间复杂度 O(N^3)
floyd算法可以求多源最短路,即任意两点的最短路径。同时floyd也可以解决有负权边的情况,但是同样不能解决有负环的问题。
floyd的核心思想就是遍历每个点,以每个点作为中转点时,从点a到点b的最短距离为多少。
代码实现很短,如图:
#include<iostream> //floyd算法求最短路
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 200;
int n, m;
//a为邻接矩阵,存储图,dist表示每个点距离源点的最短距离
int a[N][N],dist[N];
void floyd()
{
//三重循环,表示以第k个点为中转点时,点i到点j的最短距离
for (int k = 1; k <= n; k++)
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
a[i][j] = min(a[i][j], a[i][k] + a[k][j]);
a[j][i] = min(a[j][i], a[j][k] + a[k][i]);
}
}
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(dist, 0x3f, sizeof dist);
//初始化
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
if (i == j)
a[i][j] = 0;
else
a[i][j] = 0x3f3f3f3f;
}
}
//输入数据,用邻接矩阵存储
for (int i = 0; i < m; i++)
{
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
a[u][v] = min(a[u][v], w);
a[v][u] = min(a[v][u], w);
}
floyd();
//输出答案
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
return 0;
}