欢迎来到CJY的博客|

wenli7363

园龄:3年3个月粉丝:7关注:6

【Acwing】最短路算法

0 前言

最短路算法包含以下五种,注意区分

1

1 Dijkstra算法

该算法使用于单源最短路、无负边权的图

单源最短路就是指,从一个指定点出发到指定终点 的最短路径

该算法有两种写法,第一种是朴素版本Dijkstra,复杂度o(n^2),适合稠密图

另一个版本是采用堆优化的

适用于稀疏图,边少的时候

因为朴素版本最复杂的一步是在找最小距离的点,于是我们将dist用堆来存储,在找最小距离的点的时候,直接输出堆顶元素就好了,此时查找的复杂度是O(1)的。

于是算法的复杂度受限于更新边,总的算法复杂度为O(mlogn)

1.1 朴素版

  1. 849. Dijkstra求最短路 I

朴素版Dijkstra的思路就是两重遍历。参考这个视频的过程,
外层循环就是n次循环,每次从所有未标记的点中找到路径中的一个节点,当找完这n个点就找到了整个图的最短路径(相当于更新完毕n个点的dist数组)

内层循环则是 从当前所有未被标记的点中,选择距离最小的点。(按照视频中的,我们只需要处理出边的节点,但是这里直接遍历所有节点也能达到同样的效果,复杂度虽然没有那么完美,但是写法简单)

// dist[i]表示从起点到点i的最小距离之和,st[]是标记数组
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存储当前访问的点,t=-1初始化一个不存在的点
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];
}

1.2 堆优化版Dijkstra算法

堆优化版本更加符合这个视频里描绘的过程

Dijkstra求最短路 II

// 稀疏图加边
void add(int x, int y, int z)
{
e[idx] = y; // e存当前节点的下标
w[idx] = z; // w存边的权重
ne[idx] = h[x]; // ne存下一个节点下标
h[x] = idx++; // h[x] 表示x为起点连线的终点下标
}
int dijkstra(){
memset(dist,0x3f,sizeof dist); // 距离初始为无穷大
dist[1] = 0; // 起点距离为0,注意dist数组存的是 起点 到 当前点 的 最短距离
// 定义一个小根堆
priority_queue<PII,vector<PII>,greater<PII>> heap;
heap.push({0,1}); // 用pair存,第一个是dist,第二个是点的下标,因为小根堆排序的时候先first,再second
while(heap.size())
{
PII t = heap.top(); // 每次取距离起点最小的点
heap.pop();
if(st[t.second]) continue; // 如果该点已被访问过,跳过
st[t.second] = true;
for(int i = h[t.second]; i != -1; i = ne[i]) // 更新所有出边(朴素版是处理所有的边,但实际上只需要处理出边就好了)
{
int j = e[i]; // 注意h[t.second]和i存的是下标
if(dist[j] > t.first + w[i]){
dist[j] = t.first + w[i]; // 如果更新了节点,就要加入堆中,下次要从这些点里面找最小的距离点
heap.push({dist[j],j});
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h); //稀疏图初始化时,h全部置为-1
while(m--)
{
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
cout<<dijkstra();
return 0;
}

2 Bellman-ford、SPFA

Bellman-ford算法适合处理带有负权边的图,SPFA是对这个算法的优化。

对于负权图的最短路径问题,如果存在负权回路则无解,所以一般题目不会有这种负权环的存在。可以借助这两个算法去判断一个回路有没有负权回路。

一般情况下,对于这种问题的最短路,Bellmon_ford要进行n-1次迭代,就能得到最终结果(n个点,我只需要确定了n-1条边就能找出最短路径)

bellman-ford算法适合解决有边数限制的最短路问题

2.1 Bellman-ford算法

有边数限制的最短路

int bellman_ford() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i++) { //k次循环,表示最多经过k条边
memcpy(back, dist, sizeof dist); // back数组保存上次迭代结束后的结果,如果不保存,可能在迭代中,数组已经发生变化了,就会导致结果错误(串联效应)
for (int j = 0; j < m; j++) { //遍历所有边
int a = e[j].a, b = e[j].b, w = e[j].w;
dist[b] = min(dist[b], back[a] + w); // relax操作
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1; // 为什么要大于0x3f3f3f3f/2, 因为负权边的存在,可能出现INF+或-一个很小的数,但是实际上这种情况还是不可达的状态
else return dist[n];
}

2.2 SPFA算法

在relax操作中:dist[b] = min(dist[b], back[a] + w);,我们发现,只有back[a]变化的时候,dist[b]才会发生变化,所以我们可以用一个队列来存,只有当一个点的dist发生变化的时候,我们把这个点压入队列中,然后更新他的所有出边即可。

参考这篇题解,但是这里第四点说错了

第四点中,一般用SPFA的题目不会有负权回路,有负权回路就无解,虽然Bellman-ford算法能在负权回路的图中运行,但是结果是错的。你想想每次经过负权回路都会让你整条路径的权重减少,那么我算法就会无限走负权回路,你整条路径最终结果为-∞,显然不可能。

//
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true; // st表示已经入队了,避免重复入队
while (q.size())
{
int t = q.front(); // 取队头
q.pop();
st[t] = false; // 可以再次入队了
for (int i = h[t]; i != -1; i = ne[i]) // 更新所有出边,所以要用邻接表存储
{
int j = e[i];
if (dist[j] > dist[t] + w[i]) // relax操作
{
dist[j] = dist[t] + w[i];
if (!st[j]) // 如果还没入队,则入队
{
q.push(j);
st[j] = true;
}
}
}
}
return dist[n];
}

3 多源最短路 Floyd算法

多源最短路就是:每一个点到图中其他顶点的最短路

floyd算法中,d矩阵一开始存图,当算法结束后,这个d矩阵就会被更新为点与点之间的最短距离,所以你可以询问图中任意x,y两点的最短距离,即d[x][y]

// 核心代码
void floyd()
{
for (int k = 1; k <= n; k ++ ) // 一定是k先循环
for (int i = 1; i <= n; i ++ ) //i,j循环谁先谁后无所谓
for (int j = 1; j <= n; j ++ )
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
posted @   wenli7363  阅读(31)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起