关于最短路有很多很多的算法,针对于不同的情境,点边范围,有很多算法思路。大致如下图:
在记忆的时候使用此图,可以跟好的理解,不会记得特别的乱。
一:朴素Dijkstra算法
朴素Dijkstra算法的时间复杂度为o(n^2),主要是用于单源最短路权值都为正的稠密图中。
首先给定起始点和终点,求起始点到终点的最短路径。
Dijkstra算法的做法是:
- 在所有顶点里找到距离起点最近的点,将它放入集合S。
- 用这个顶点来更新其它顶点到起点的距离。
- 重复1,2步,直到所有顶点都在集合S里,此时,终点存的距离就是终点到起点的最短距离。
模板题目
先观察题目,很明显n远远小于m,所以用稠密图,其次,题目说可能存在自环和重边,如果是自环的话,因为题目说了边权都为正,所以至少这道题,不会出现自环,重边的话,因为我们求的是最短路,因此我们在输入的时候min一下,保留最小边权值的那个边就好了。
代码如下:
#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]; //True表示已经确定最短路 属于s集合
int dijkstra() {
//所有节点距离起点的距离初始化为无穷大
memset(dist, 0x3f, sizeof dist);
//起点距离自己的距离为零
dist[1] = 0;
//迭代n次,每次可以确定一个点到起点的最短路
for (int i = 0; i < n; ++i) {
int t = -1;
//t的作用?
for (int j = 1; j <= n; ++j) {
//不在s集合,并且
//如果没有更新过,则进行更新, 或者发现更短的路径,则进行更新
if (!st[j] && (t == -1 || dist[j] < dist[t])) {
t = j;
}//这个地方为什么这样写涉及到数学的证明,不必太过于拘泥于这个地方如何实现。
}
//加入到s集合中
st[t] = true;
//找到了距离最小的点t,并用最小的点t去更新其他的点到起点的距离
for (int j = 1; j <= n; ++j) {
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
// 如果起点到达不了n号节点,则返回-1
if (dist[n] == 0x3f3f3f3f) return -1;
// 返回起点距离n号节点的最短距离
return dist[n];
}
int main() {
cin >> n >> m;
//所有节点之间的距离初始化为无穷大
memset(g, 0x3f, sizeof g);
// 0x3f 0x3f3f3f3f 的区别?
while (m--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = min(g[a][b], c); //如果有重边,请保留权值最小的一条边
}
cout << dijkstra() << endl;
return 0;
}
二:堆优化版的Dijkstra算法
此算法用于边和点的数量级一致的时候大概就是10的5次方级别的时候,用优先队列去优化,以便降低时间复杂度到o(mlogn)。这种情况下,是稀疏图,要用到邻接表去存图。
模板题目
看得出必须是优化版的才能过所有数据了,代码模板如下:
#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算
1.什么是Bellman - Ford算法?
Bellman - ford 算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
(通俗的来讲就是:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新)
2.bellman - ford算法的具体步骤
for n次
for 所有边 a,b,w (松弛操作)
dist[b] = min(dist[b],back[a] + w)
注意:back[] 数组是上一次迭代后 dist[] 数组的备份,由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点
3、在下面代码中,是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可
4、Bellman - Ford算法擅长解决有边数限制的最短路问题
时间复杂度 O(nm)
模板题目
代码:这道题目前我还是不理解,这个算法还不会用
AcWing 853. 有边数限制的最短路 - AcWing 这个大佬讲的很不错。
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int dist[N],backup[N];
int k,n,m;
struct edge{
int a;int b;int w;
}edge[N];
int bellman_ford()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=1;i<=k;i++)
{
memcpy(backup,dist,sizeof dist);
for(int j=1;j<=m;j++)
{
int a=edge[j].a,b=edge[j].b,w=edge[j].w;
dist[b]=min(dist[b],backup[a]+w);
}
}
return dist[n];
}
int main()
{
cin>>n>>m>>k;
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
edge[i].a=a,edge[i].b=b,edge[i].w=c;
}
int t=bellman_ford();
if(t>=0x3f3f3f3f/2)puts("impossible");
else cout<<t<<endl;
}
四:SPFA算法
AcWing 851. SPFA算法 - AcWing 解析看这个
此算法其实是上一个算法的优化,在求解问题上很方便,有些时候可以用它解决Dijkstra算法的问题,时间复杂度为线性,一般为o(m),最坏的时候为o(nm),并且可以判断是否存在负环。
题目:
代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N=100010;
int n,m;
int h[N],e[N],ne[N],w[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;//起点到自己的距离为0
queue<int> q;
q.push(1);//把起点加入队列
st[1]=1;//因为加入了队列,所以标记一下
while(!q.empty())
{
int t=q.front();
q.pop();
st[t]=0;//出来了,那就不在队列里了,就为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(!st[j])//没被标记,就进队
{
q.push(j);
st[j]=1;//进队要标记
}
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));//初始化链表头
for(int i=1;i<=m;i++)
{
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
int res=spfa();
if(res==-1) cout<<"impossible"<<endl;
else cout<<res<<endl;
return 0;
}
五:多源汇最短路--Floyd算法
求解多个点到其他点的最短路问题。
其实floyd很简单,如果时间复杂度允许的情况下简直就不要太爽,但是无奈时间复杂度为o(n^3),有点不太友好。
AcWing 854. Floyd求最短路 - AcWing 解析看这个。
题目:
#include <iostream>
using namespace std;
const int N = 210, M = 2e+10, INF = 1e9;
int n, m, k, x, y, z;
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]);
}
int main() {
cin >> n >> m >> k;
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--) {
cin >> x >> y >> z;
d[x][y] = min(d[x][y], z);
//注意保存最小的边
}
floyd();
while(k--) {
cin >> x >> y;
if(d[x][y] > INF/2) puts("impossible");
//由于有负权边存在所以约大过INF/2也很合理
else cout << d[x][y] << endl;
}
return 0;
}
完结!