最短路问题是图论里非常经典的一个考点
接下来着重讲述五种求最短路的算法:朴素版dijkstra算法、堆优化版的dijkstra算法、bellman-ford算法、spfa算法、floyd算法
总体思维导图:
总体思路:
最短路分为两大类
{
在以下给出的时间复杂度中n表示图中的点数,m表示图中的边数
源点就是起点,汇点就是终点
①单源最短路{
求一个点到其它所有点的最短距离,(比如:从1号点到n的最短路问题)。
①{
1、所有边权都是正的
{
一、朴素的Dijkstra算法(O(n^2))
二、堆优化的Dijkstra算法(O(mlogn))
如果图上的边数越稠密(当m和n^2差不多的时候即为稠密图),那么堆优化版的要比朴素版的时间复杂度要高。反之如果边是稀疏的,那么尽量使用堆优化般的。
稠密图用邻接矩阵(矩阵),稀疏图用邻接表(链表)
朴素的Dijkstra板子:
void dijkstra()
{memset(dist, 0x3f, sizeof dist);dist[1] = 0;for (int i = 0; i < n - 1; i ++ ){int t = -1; // 在还未确定最短路的点中,寻找距离最小的点for (int j = 1; j <= n; j ++ )if (!st[j] && (t == -1 || dist[t] > dist[j]))t = j;// 用t更新其他点的距离for (int j = 1; j <= n; j ++ )dist[j] = min(dist[j], dist[t] + g[t][j]);st[t] = true;}}
堆优化的Dijkstra板子:
int dijkstra()
{dist[1] = 0;priority_queue<PII, vector<PII>, greater<PII>> heap;heap.push({0, 1}); // first存储距离,second存储节点编号while (heap.size()){auto t = heap.top();heap.pop();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];if (dist[j] > distance + w[i]){dist[j] = distance + w[i];heap.push({dist[j], j});}}}}
}
2、存在负权边
{
一、Bellman-Ford算法 O(nm)
二、SPFA 一般情况下O(m),最坏O(nm)。SPFA算法也就是对Bellman-Ford算法的优化,
若对边数进行了限制就不能使用SPFA算法,只能使用Bellman-ford算法。
一般情况下,题目中不会对边数进行限制,所以99%的情况SPFA算法要比Bellman-ford算法好用的多。
Bellman-Ford板子:备份
struct Edge
{//a,b,w从a走向b的边,权重是wint a,b,w;
}edges[M];int ballman_ford()
{memset(dist,0x3f,sizeof dist);dist[1]=0;//最多经历k次for(int i=0;i<k;i++){//先复制一层,防止串联memcpy(backup,dist,sizeof dist);for(int j=0;j<m;j++){//调用结构体auto e=edges[j];//等于上一个点+权重的值 与 当前这个点到原点的距离最小值dist[e.b]=min(backup[e.a]+e.w,dist[e.b]);}}
SPFA板子:每个被更新的节点都用队列存储进来,注意使用st[ ]来优化,防止重复进入
void spfa()
{dist[1] = 0;queue<int> q;q.push(1);st[1] = true;while (q.size()){auto 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]){dist[j] = dist[t] + w[i];if (!st[j]) // 如果队列中已存在j,则不需要将j重复插入{q.push(j);st[j] = true;}}}}
}
}
}
}
②多源汇最短路{
不会像单源最短路那样只有一个起点。多源汇最短路可能有多个起点,对于多次询问,从其中一个点走到另外一个点的最短路问题,起点和终点都是不确定的
Floyd算法 O(N^3)
Floyd板子:三重循环
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]);
}
}
}
朴素版的dijkstra: O(MN)
题目:849. Dijkstra求最短路 I - AcWing题库
本题思路:
到起点的距离:dist[i]。已经确定了最短距离的点的集合:s[i]
第一步 先初始化距离:dist[1]=0,其余的dist[i]= +∞(比较大的数)
因为一开始只有起点的距离是被确定的了,其余的所有点都是不确定的
1号点到n号点最近的距离最近也就是n号点到1号点的距离最近,而由于这里1号点的到起点的距离已经确定所以一开始更新迭代就利用dist[1]已知的条件,来更新其他点到1号点的距离dist[x],然后再更新其他点到点x的距离即可。
这期间如果1号点到n号点的距离已经求出dist[n],且dist[x]<dist[n],那么dist[x]的距离就是确定了的(继续以这点迭代下去)。
如果不小于dist[n],那就没必要继续迭代下去,因为按照x的这条路径的长度会增加,那么就更一定不小于dist[n],所以更新到n号点时,那么n号点到1号点的距离也就最近了
第二步 是一个迭代循环的过程:
for(int i=0,i<n;i++)
{
t=不在s中的所有点中距离起点最近的点(距离最近的点)
s⬅t (如果有的话就把t加到s里面去)
更新dist[i](用t更新,即如果dist[i]>dist[t]+w(w是权重)就用t更新)
}
模拟:
在图论中寻找最短路的大体思维就是这样的,后面的算法也是如此,不在给出
代码
#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];int dijkstra()
{memset(dist,0x3f,sizeof dist);dist[1]=0;//找到当前没有确定的点当中,距离起点最近的那个点(然后以这个点更新迭代之后的点)//如果已经确定了,那么就和dist[n]进行比较,来确定下一步是继续迭代还是结束这条路径for(int i=0;i<n;i++){int t=-1;for(int j=1;j<=n;j++){if(!st[j] && (t==-1 || dist[t] >dist[j]))//确定dist[j];t=j;}//已经确定了dist[t];st[t]=true;//更新每个点到1号点的距离for(int j=1;j<=n;j++){//使其等于(j号点到1号点的距离)与(t号点到1号点的距离+t号点到j号点距离的最小值)dist[j]=min(dist[j],dist[t]+g[t][j]);}}if(dist[n]==0x3f3f3f3f) return -1;else return dist[n];
}int main()
{cin >> n >> m;//初始化所有点之间的距离都为一个较大的数memset(g,0x3f,sizeof g);while(m--){int a,b,c;cin >> a >> b >> c;//防止重边g[a][b]=min(g[a][b],c);}cout << dijkstra();return 0;
}
堆优化版的dijkstra: O(mlogn)
题目:850. Dijkstra求最短路 II - AcWing题库
这里是稀疏图,使用邻接表来存
代码:
/*
n表示图中的点数,m表示图中的边数
一、朴素的Dijkstra算法(O(n^2))
二、堆优化的Dijkstra算法(O(mlogn))使用朴素版的dijkstra必定会爆掉,所以采用堆优化
由于点数和边数大致差不多,所以在这里是稀疏图→使用邻接表来存
这里采用优先队列。
*/#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>using namespace std;typedef pair<int,int> PII;
const int N=150010;
int n,m;
int h[N],w[N],e[N],ne[N],idx;
int dist[N];
bool st[N];void add(int a,int b,int c)
{e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}int dijkstra()
{dist[1]=0;//这里是小根堆,因为是从一号点开始priority_queue<PII,vector<PII>,greater<PII>> heap;
//这里先放距离,后放点的原因是pair<int,int>类的排序,是先比较的first,如果first相同才比较second//一号点,距离为0先放入队列中heap.push({0,1});while(heap.size()){auto t=heap.top();heap.pop();int ver=t.second,distance=t.first;//如果这个点已经使用过了的话,直接跳过if(st[ver]) continue;st[ver]=true;//使用这个点,以这个点为起点向下遍历//向下遍历,走到ver能走到的所有点for(int i=h[ver];i!=-1;i=ne[i]){//找出点坐标int tt=e[i];/*假设上一个点为x,下一个点为y。如果下一个点到原点的距离(dist[tt])大于(上一个点到原点的距离distance+x到y的距离(w[i]))时,才有更新的价值。*/if(dist[tt]>distance+w[i]){//符合,更新当前点到原点的距离dist[tt]=distance+w[i];//放入队列heap.push({dist[tt],tt});}}}if(dist[n]==0x3f3f3f3f) return -1;return dist[n];
}int main()
{scanf("%d%d",&n,&m);memset(dist,0x3f,sizeof dist);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;
}
Bellman-ford算法
题目:853. 有边数限制的最短路 - AcWing题库
代码:
#include<iostream>
#include<algorithm>
#include<cstring>using namespace std;//点数,边数
const int N=510,M=10010;
int n,m,k;int dist[N],backup[N];struct Edge
{//a,b,w从a走向b的边,权重是wint a,b,w;
}edges[M];int ballman_ford()
{memset(dist,0x3f,sizeof dist);dist[1]=0;//最多经历k次for(int i=0;i<k;i++){
//复制一份,防止串联memcpy(backup,dist,sizeof dist);for(int j=0;j<m;j++){auto e=edges[j];dist[e.b]=min(backup[e.a]+e.w,dist[e.b]);}}if(dist[n] > 0x3f3f3f3f /2) cout << "impossible" << endl;else cout << dist[n];
}int main()
{cin >> n >> m >> k;for(int i=0;i<m;i++){int a,b,c;cin >> a >> b >> c;edges[i]={a,b,c};}ballman_ford();return 0;
}
SPFA算法:
题目:851. spfa求最短路 - AcWing题库
模拟:
代码:
/*
每次将变小了的节点入队,那么用此节点更新后面的节点时才有可能将后面的数值变小
否则,若此节点都没变小,那么后面的节点不一定会因为被此节点更新而变小
即优化的是Bellman-ford算法里的这一步:for (int i = 0; i < n; i ++ )for (int j = 0; j < m; j ++ )
这里并不能保证每一次的j循环都有节点被更新,从而增加时间复杂度(公司的总效益涨了,员工的工资才有可能会涨,即前面的点的dist数值减小了,后面的数值才可能会减小)
省去了因为没有被更新而重复的操作
但是如果出题人卡你,时间复杂度和bellman-ford算法一致O(nm),最好的情况为O(m);
*/#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>using namespace std;int n,m;
const int N=1e5+10;
int e[N],ne[N],h[N],idx,w[N];
queue<int> q;
bool st[N];
int dist[N];
//使用链表进行储存
void add(int a,int b,int c)
{e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}void spfa()
{memset(dist,0x3f,sizeof dist);dist[1]=0;q.push(1);//使用过1号节点,则为truest[1]=true;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]){dist[j]=dist[t]+w[i];if(!st[j]){q.push(j);st[j]=true;}}}}//由于每次更新都是由队列里的数来更新,所以如果抵达不到n的话,那么n号点一定没有被更新if(dist[n]==0x3f3f3f3f) cout << "impossible" << endl;else cout << dist[n];
}int main()
{memset(h,-1,sizeof h);cin >> n >>m;while(m--){int a,b,c;cin >> a >> b >> c;add(a,b,c);}spfa();return 0;
}
题目:852. spfa判断负环 - AcWing题库
这里再寻找最短路时,如若存在负环,那么会一直绕着这个负环来循环(因为这样可以使距离无限变小)
代码:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>using namespace std;const int N=10010;
int n,m;
int e[N],ne[N],h[N],w[N],idx;
//cnt[]表示当前最短路的边数
int dist[N],cnt[N];
bool st[N];
queue<int> q;void add(int a,int b,int c)
{e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}bool spfa()
{//负环所在的位置可能是头节点抵达不到的,所以一开始将所有点全部放进队列中for(int i=1;i<=n;i++){//标记为true,表示该节点已经被放进了队列里st[i]=true;q.push(i);}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]){dist[j]=dist[t]+w[i];cnt[j]=cnt[t]+1;//如果不存在负环的话,那么从一个节点出发后所遍历的边数一定小于n//否则的话就一定存在负环if(cnt[j]>=n) return true;if(!st[i]){q.push(j);st[j]=true;}}}}return false;
}int main()
{memset(h,-1,sizeof h);cin >> n >> m;while(m--){int a,b,c;cin >> a >> b >> c;add(a,b,c);}if(spfa()) puts("Yes");else puts("No");return 0;
}
Floyed算法:
这里的证明使用dp来论证的,建议直接背板子
题目:854. Floyd求最短路 - AcWing题库
代码:
/*
②多源汇最短路{不会像单源最短路那样只有一个起点。多源汇最短路可能有多个起点,对于多次询问,从其中一个点走到另外一个点的最短路问题,起点和终点都是不确定的Floyd算法 O(N^3)}用邻接矩阵存储所有的边d[][]for(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的最短路径
*/#include<iostream>
#include<algorithm>
#include<cstring>using namespace std;const int N=210,INF=1e9;
int d[N][N];
int n,m,Q;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 >> Q;//初始化for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){//自环的距离初始化为0if(i==j) d[i][j]=0;//其余的初始化为正无穷else d[i][j]=INF;}}while(m--){int a,b,c;cin >> a >> b >> c;//对于重边来说取一个最小值即可d[a][b]=min(d[a][b],c);}floyd();while(Q--){int a,b;cin >> a >> b;//存在负权边时距离的大小不等于INFif(d[a][b]>INF/2) cout << "impossible" << endl;else cout << d[a][b] << endl;}return 0;
}
tips:
图论这一节重在代码实现,很多板子都是互相重复的。
至于该思路如何实现的,可以课下钻研