一、单源最短路
求从一个点到其他所有点的最短距离。
分为两大类:
1、所有边权都是正数(n 个点,m 条边)
- 朴素版的Dijkstra算法,时间复杂度为 O(n^2) ,适合稠密图(边多,点少边比较多)
- 堆优化版的Dijkstra算法,时间复杂度为 O(mlogn),适合稀疏图(点多,指边相对于点不多,m和n是同一个级别的类型)
2、存在负权边
- Bellman-Ford 算法,时间复杂度为 O(nm)
- SPFA 算法,时间复杂度一般为 O(m),最坏情况为 O(nm),是Bellman-Ford算法的优化
(1)朴素版 Dijkstra算法
稠密图用邻接矩阵,稀疏图用邻接表
1.逐个遍历,找到与起点最近的且未确定最短路径的点,访问加入集合并标记。
2.更新第一个点到起点的最短距离,直到第n个点。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int n, m;//点数和边数
int g[N][N];//邻接矩阵存图
int dist[N];//各个节点到起点的距离
bool st[N];//默认为0,判断是否找到了最短距离
int dijkstra(){
//距离都初始化为无穷大
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;//第一个点,距离初始化为0
//迭代n次
for (int i = 0; i < n; i++){
//t是距离最近的未确定的最短路点的编号
int t = -1;//未加入最短路的点编号初始化为-1
//遍历n个点
//先找到一个未加入集合中的距离最近的点
for (int j = 1; j <= n; j++){
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
}
st[t] = true;//标记为已加入到集合中
//更新到第j个点的最短路
//用1到t的加t到j的距离更新1到j的距离
for (int j = 1; j <= n; j++){
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
//如果为无穷,说明不连通,无法形成最短路
if (dist[n] == 0x3f3f3f3f)
return -1;
return dist[n];
}
int main(){
scanf("%d%d", &n, &m);
//邻接矩阵初始化为无穷大
memset(g, 0x3f, sizeof(g));
while (m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
//存入a和b两点中间的距离(有向图)
g[a][b] = min(g[a][b], c);
//无向图
//g[a][b] = min(g[a][b], c);
//g[b][a] = min(g[a][b], c);
}
int t = dijkstra();
printf("%d\n", t);
return 0;
}
(2)堆优化版的Dijkstra算法
稀疏图改用邻接表的形式存储,可以不需要考虑重边
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
#define PII pair<int, int>
const int N = 1e6 + 10;
int n, m;
//稀疏图用邻接表来存
//依次存储 头节点,权重,值,下一节点,序号
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];//初始为0
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
int dijkstra(){
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
//定义一个小根堆
priority_queue<PII, vector<PII>, greater<PII> > heap;
//这个顺序不能倒,pair排序时是先根据first,再根据second,这里要根据距离排序
heap.push({0,1});
while (heap.size()){
auto t = heap.top();//取不在集合中的距离最短的点
heap.pop();//取出来后弹出队列
//ver为当前节点编号
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i]){
int j = e[i];//e[]中存的是下标为i时所对应的点
//判断最短的距离
if (dist[j] > dist[ver] + w[i]){
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f)
return -1;
return dist[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof(h));
while (m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
printf("%d\n", dijkstra());
return 0;
}
(3)Bellman-ford 算法
1.可以用结构体存储点和边,包括负权边。
2.具体步骤:两重 for 循环,迭代 n-1 次,每次备份一下,每次循环遍历所有边,更新两点之间的最短距离,如点 a->b 的更新方式为(松弛操作)
for n 次
for 所有边 a, b, w (松弛操作)
dist[b] = min(dist[b], backup[a] + w);
backup[ ] 数组是上一次迭代后 dist[ ] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[ ] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点。
3.循环 n-1 次之后,对于所有的点都 一定满足 dist[b] <= dist[a] + w ,该式被称为三角不等式。
4.如果图中存在负权回路,那么最短路可能为负无穷。(不是一定)
5.是否能到达n号点的判断中需要进行 if(dist[n] > INF/2) 判断,而并非是 if(dist[n] == INF) 判断,原因是 INF 是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与 INF 相同数量级的数即可。
6.bellman-ford算法擅长解决有边数限制的最短路问题。
#include <iostream>
#include <cstring>
#include <algoritnm>
using namespace std;
#define INF 0x3f3f3f3f
const int N = 510, M = 10010;
int n, m, k;//点数、边数、限制的最多次查找的边数
int dist[N];//存储权重
int backup[N];//backup存储上一次迭代的结果
struct Edge
{
int a, b, w;//两点及其权重
}edges[M];
int bellman_ford()
{
//初始化
memset(dist, INF, sizeof(dist));
dist[1] = 0;
for (int i = 0; i < k; i++){
//备份一下
memcpy(backup, dist, sizeof(dist));
for (int j = 0; j < m; j++){
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
//更新两点之间的最短距离
dist[b] = min(dist[b], backup[a] + w);
}
}
}
int main(){
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i++){
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
bellman_ford();
if (dist[n] > INF / 2)
puts("impossible");
else
printf("%d\n", dist[n]);
return 0;
}
(4)SPFA算法
1.用队列来存储
2.while queue 不为空,
- 取出作为 t ,t = q.front; q.pop();
- 更新 t 的所有出边,如:t -> b, 把b加入 queue
3.基本步骤
- 建立一个队列,初始时队列里只有起始点
- 再建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到它本身的路径赋为0)
- 再建立一个数组,标记点是否在队列中
- 队头不断出队,计算起始点经过队头到其他点的距离是否变短,如果变短且该点不在队列中,则把该点加入到队尾
- 重复执行直到队列为空
- 在保存最短路径的数组中,就得到了最短路径
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], w[N], ne[N], idx;//邻接表存图
int dist[N];
int st[N];
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int spfa(){
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;//1号到1号的距离为0
queue<int> q;
q.push(1);//从1号开始入队
st[1] = true;//标记1号顶点在队列中
while (q.size()){
int t = q.front();//取出队头顶点赋给t
q.pop();
st[t] = false;//取完队头后,t不在队列中了
//遍历所有和t相连的点
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 (!st[j]){
q.push(j);
st[j] = true;
}
}
}
//后面如此循环,寻找下一个队头顶点,直到队列为空
}
return dist[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof(h));
while (m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);//加入到邻接表
}
int t = spfa();
if (t == 0x3f3f3f3f)//不能到达
puts("impossible");
else
printf("%d\n", t);
return 0;
}
二、多源汇最短路
起点和终点不确定,求多个起点到多个终点的最短距离
Floyd 算法
1.使用邻接矩阵存图
2.三重循环,时间复杂度O(n^3)
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]);
//循环之后 d[i][j] 存储的就是i到j的最短距离
3.示例模板(如果出现SF错误,可以通过 删代码 确定错误地方)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, q;
int d[N][N];
void floyd(){
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]);
//更新i到j的最短距离
}
int main(){
//初始化输入 点数、边数、询问次数
scanf("%d%d%d", &n, &m, &q);
//初始化d[i][j]
for (int i = 1; i <= n; i++){
for (int j = 1; j <= n; j++){
if (i == j)
d[i][j] = 0;
else d[i][j] = INF;
}
}
//存入两点之间的边长
while (m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
//保存最小的边
d[a][b] = min(d[a][b], c);
}
floyd();
//q次询问,询问a点和b点的最短距离
while (q--){
int a, b;
scanf("%d%d", &a, &b);
int t = d[a][b];
//因为有负权边存在
if (t > INF / 2)
puts("impossible");
else
printf("%d\n", t);
}
return 0;
}