最短路算法
前要知识
- n为点数,m为边数
- 在图论中,无向图其实可以看成是一种特殊的有向图,连一条A->B的边,再连一条B->A的边即可
- 稠密图用邻接矩阵来存储,稀疏图用邻接表来存储(根据题目数据范围进行判断,当m与\(n^2\)一个级别时,即为稠密图;当m与n一个级别时即为稀疏图)
知识结构

朴素Dijkstra算法
Dijkstra的整体思路比较清晰:即进行n(n为点的个数)次迭代去确定每个点到起点的最小值,每次迭代中确定一个点,最后输出的终点即为我们要找的最短路的距离。
Dijkstra算法是基于贪心的,有关算法的具体解释可以参考这一篇博客:图最短路径算法之迪杰斯特拉算法(Dijkstra)
题目

思路
对于重边和自环的处理:由于边权值都是正数,所以哪怕存在自环,也不会对结果产生影响,对于重边的处理,可以在输入时进行判断,取最小值即可。
代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 510;
int g[N][N]; // 为稠密阵所以用邻接矩阵存储
int dist[N]; // 用于记录每一个点距离第一个点的距离
bool st[N]; // 用于记录该点的最短距离是否已经确定
int n,m;
int Dijkstra()
{
memset(dist, 0x3f,sizeof dist); // 初始化距离,0x3f代表无限大
dist[1] = 0; // 第一个点到自身的距离为0
for(int i = 0; i < n; i++) // 有n个点所以要进行n次迭代
{
int t = -1; // t存储当前访问的点
for(int j = 1; j <= n; j++) //这里的j代表的是从1号点开始
if(!st[j] && (t == -1 || dist[t] > dist[j])) // 找到当前距离最小的点加入确认点集合中
t = j;
st[t] = true;
for(int j = 1;j <= n; j++) //依次更新每个点所到相邻的点路径值
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
if(dist[n] == 0x3f3f3f3f) return -1; //如果第n个点路径为无穷大即不存在最低路径
return dist[n];
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g); //初始化图 因为是求最短路径,所以每个点初始为无限大
while(m--)
{
int x, y, z;
cin >> x >> y >> z;
g[x][y] = min(g[x][y], z); //如果发生重边的情况则保留最短的一条边
}
cout << Dijkstra() << endl;
return 0;
}
堆优化版Dijkstra算法
题目

思路
参考这俩位大佬的解法笔记:
代码
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 100010; // 把N改为150010就能ac
// 稀疏图用邻接表来存
int h[N], e[N], ne[N], idx;
int w[N]; // 用来存权重
int dist[N];
bool st[N]; // 如果为true说明这个点的最短路径已经确定
int n, m;
void add(int x, int y, int c)
{
w[idx] = c; // 有重边也不要紧,假设1->2有权重为2和3的边,再遍历到点1的时候2号点的距离会更新两次放入堆中
e[idx] = y; // 这样堆中会有很多冗余的点,但是在弹出的时候还是会弹出最小值2+x(x为之前确定的最短路径),并
ne[idx] = h[x]; // 标记st为true,所以下一次弹出3+x会continue不会向下执行。
h[x] = idx++;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap; // 定义一个小根堆
// 这里heap中为什么要存pair呢,首先小根堆是根据距离来排的,所以有一个变量要是距离,其次在从堆中拿出来的时
// 候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点。
heap.push({ 0, 1 }); // 这个顺序不能倒,pair排序时是先根据first,再根据second,这里显然要根据距离排序
while(heap.size())
{
PII k = heap.top(); // 取不在集合S中距离最短的点
heap.pop();
int ver = k.second, distance = k.first;
if(st[ver]) continue;
st[ver] = true;
for(int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i]; // i只是个下标,e中在存的是i这个下标对应的点。
if(dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({ dist[j], j });
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
memset(h, -1, sizeof(h));
scanf("%d%d", &n, &m);
while (m--)
{
int x, y, c;
scanf("%d%d%d", &x, &y, &c);
add(x, y, c);
}
cout << dijkstra() << endl;
return 0;
}
Bellman_Ford算法
题目

思路
-
该算法适合处理带有负权边的题目,尤其是带有最多经过k条边这样条件的题目
-
除此之外,该算法还可以判断图中是否存在负环,具体判断操作如下:
算法经过\(V - 1\)次松弛迭代过后,如果从源点到各点距离还存在变化,那就说明必定存在负环
-
算法最多经过k条边如何在代码中进行体现?只需要让最外层循环次数为k即可!经过模拟,个人认为这个算法的松弛过程有点类似于BFS的逐步扩散过程
-
在每一次松弛过程中,需要先让先前一次迭代过程后的距离数组备份,防止出现串联现象!
可以参考的题解和博客:
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e4 + 10;
int n, m, k;
int dist[N], backup[N]; // 注意定义备份距离数组
// 直接开个结构体来存边
struct
{
int a, b, w;
}edges[N];
void bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < k; i++) // 松弛k轮
{
memcpy(dist, backup, sizeof dist); // 备份
for(int i = 0; i < m; i++)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
if(dist[b] > backup[a] + w) // 迭代松弛
dist[b] = min(dist[b], backup[a] + w);
}
}
// 注意这里为什么是 0x3f3f3f3f / 2 ?
// 因为总共k次松弛,假设某个点一直被它周围的某个点松弛(负权边)并且负权边最小为-10000
// 并且k最大为500,那么最多会减少5000000,还是满足大于0x3f3f3f3f / 2
if(dist[n] > 0x3f3f3f3f / 2) cout << "impossible";
else cout << dist[n];
}
int main()
{
cin >> n >> m >> k;
for(int i = 0; i < m; i++)
{
int x, y, w;
cin >> x >> y >> w;
edges[i] = {x, y, w};
}
bellman_ford();
return 0;
}
SPFA算法
题目

思路
我们注意到Bellman_ford每次都会遍历所有的边来进行更新操作,但是其实我们可以注意到很多遍历其实都没有什么用,因此spfa针对这一步进行了优化,创建一个队列来加入每次距离被更新的结点。
注意:
-
st数组的作用:判断当前的点是否已经加入到队列当中了;已经加入队列的结点就不需要反复的把该点加入到队列中了,就算此次还是会更新到源点的距离,那只用更新一下数值而不用加入到队列当中。
-
Bellman_ford算法里最后的判断条件写的是dist[n]>0x3f3f3f3f/2;而spfa算法写的是dist[n]==0x3f3f3f3f;其原因在于Bellman_ford算法会遍历所有的边,因此不管是不是和源点连通的边它都会得到更新;但是SPFA算法不一样,它相当于采用了BFS,因此遍历到的结点都是与源点连通的,因此如果你要求的n和源点不连通,它不会得到更新,还是保持的0x3f3f3f3f。
-
Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。总结来说,Bellman_ford可以处理任意带负权边和负权环的图,SPFA可以处理带负权边的图,Dijkstra只能处理带正权边的图。
可以参考的题解和博客:
代码
#include<bits/stdc++.h>
#define xx first
#define yy second
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const bool MODE = 1;
const int INF = 0x3f3f3f3f, N = 1e5 + 10;
int n, m;
int dist[N];
bool st[N];
int h[N], e[N], ne[N], idx, w[N];
inline void add(int a, int b, int c)
{
w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void spfa()
{
queue<int> q;
dist[1] = 0;
st[1] = true;
q.push(1);
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false; // 从队列中取出来之后该节点st被标记为false,代表之后该节点如果发生更新可再次入队
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j]) // 当前已经加入队列的结点,无需再次加入队列,即便发生了更新也只用更新数值即可
{
st[j] = true;
q.push(j);
}
}
}
}
if (dist[n] == INF) cout << "impossible" << endl;
else cout << dist[n] << endl;
}
void solve()
{
memset(dist, 0x3f, sizeof dist);
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; ++i)
{
int a, b, c; cin >> a >> b >> c;
add(a, b, c);
}
spfa();
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (MODE) freopen("Debug/input.txt", "r", stdin), freopen("Debug/output.txt", "w", stdout);
solve();
if (MODE) fclose(stdin), fclose(stdout);
return 0;
}
Floyd算法
题目

思路
Floyd算法的核心是基于动态规划,并且经过了滚动数组进行优化。
分析过程如下图所示:

可以参考的题解和博客:
代码
#include<bits/stdc++.h>
#define xx first
#define yy second
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const bool MODE = 0;
const int INF = 0x3f3f3f3f, N = 210;
int dist[N][N], p[N][N]; // p[i][j]代表i-j两点之间最短路径必须经过p[i][j]表示的点
int n, m, t;
void floyd()
{
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (dist[i][j] > dist[i][k] + dist[k][j])
{
dist[i][j] = dist[i][k] + dist[k][j];
p[i][j] = k; // i-j之间必须经过k
}
}
string get_path(int u, int v)
{
if (p[u][v] == -1) return to_string(u) + "->"; // base case: 直接相连的情况
int k = p[u][v];
return get_path(u, k) + get_path(k, v); // 返回左右两端u->k以及k->v的路径
}
void solve()
{
cin >> n >> m >> t;
memset(dist, 0x3f, sizeof dist);
memset(p, 0x3f, sizeof p);
for (int i = 1; i <= n; ++i) dist[i][i] = 0; // 处理自环
for (int i = 0; i < m; ++i)
{
int a, b, c;
cin >> a >> b >> c;
dist[a][b] = min(dist[a][b], c); // 处理重边
p[a][b] = -1; // 如果两点之间有一条直接路径,那么意味着无需经过任何点,我们设为-1
}
floyd();
while (t--)
{
int x, y;
cin >> x >> y;
if (dist[x][y] > INF / 2) cout << "impossible" << endl; // 由于循环过程可能存在负权边更新,所以必须要大于inf/2
else
{
cout << dist[x][y] << endl;
string path = get_path(x, y) + to_string(y);
cout << path << endl;
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
if (MODE) freopen("Debug/input.txt", "r", stdin), freopen("Debug/output.txt", "w", stdout);
solve();
if (MODE) fclose(stdin), fclose(stdout);
return 0;
}

浙公网安备 33010602011771号