朴素Dijkstra(稠密图,O(n^2))
1)算法流程:
集合S为已经确定最短路径的点集。
- 初始化距离 一号结点的距离为零,其他未确定最短路的结点的距离设为无穷大。
- 循环n次,每一次将集合S之外距离源点最短的点X加入到S中去(这里的距离最短指的是距离1号点(源点)最近。点X的路径一定最短,基于贪心)。然后用点X更新X邻接点的距离。
2)时间复杂度
寻找路径最短的点:O(n^2)
加入集合S:O(n)
更新距离:O(m)(实际就是每条边都比较一遍)
使用邻接矩阵存储
#include<bits/stdc++.h>
using namespace std;
const int N=510;
int g[N][N];//点a,b之间的距离
int dist[N];//1~i的距离
bool vis[N];//判断是否加入到集合S中
int n,m;
int Dijkstra(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=1;i<=n;i++){
int t=-1;
for(int j=1;j<=n;j++){//寻找路径最短的点
if(!vis[j]&&(t==-1||dist[t]>dist[j])) t=j;
}
vis[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;
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();
return 0;
}
堆优化Dijkstra(稀疏图,O(mlogn))、
1)算法流程
堆优化版的dijkstra是对朴素版dijkstra进行了优化,在朴素版dijkstra中时间复杂度最高的寻找距离最短的点O(n^2)可以使用最小堆优化。
- 一号点的距离初始化为零,其他点初始化成无穷大。
- 将一号点放入堆中。
- 不断循环,直到堆空。每一次循环中执行的操作为:
弹出堆顶(与朴素版diijkstra找到S外距离最短的点相同,并标记该点的最短路径已经确定)。
用该点更新临界点的距离,若更新成功就加入到堆中。
2)时间复杂度
寻找路径最短的点:O(n)
加入集合S:O(n)
更新距离:O(mlogn)
使用邻接表存储
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
typedef pair<int,int>PII;
int e[N],w[N],ne[N],idx,h[N];//w数组存储路径权值
int dist[N];
bool st[N];
int n,m;
void add(int a,int b,int c){
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
int Dijkstra(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
priority_queue<PII,vector<PII>,greater<PII>>qu;//小根堆
qu.push({0,1});
while(qu.size()){
auto t=qu.top();
qu.pop();
int ver=t.second,dis=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]>dis+w[i]){
dist[j]=dis+w[i];
qu.push({dist[j],j});
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
}
cout<<Dijkstra();
return 0;
}
3)关于Dijkstra不能使用在含负权图中的分析:
Dijkstra每次寻找距离源点最近的点t,在t基础上更新每个点与源点的最短距离,在处理负权边时,可能负权边的出发点(3)与源点之间的路径长度很大,或负权边的指向点(4)与寻找的n号点之间无直接连接,导致无法更新最短路径
Bellman_Ford(O(nm))
1)算法流程:
Bellman - ford 算法原理为连续进行松弛(n次),在每次松弛时把每条边都更新一下(m条边),若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
(通俗的来讲就是:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新)bellman - ford算法擅长解决有边数限制的最短路问题
#include<bits/stdc++.h>
using namespace std;
const int N=510,M=10010;
struct Edge{
int a,b,c;
}edges[M];
int n,m,k;
int dist[N];
int last[N];
void bellman_ford(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=0;i<k;i++){//k是题目限制的最多可经过的路径数量,若无限制应松弛n次,因此即使有负权回路也不会进入死循环
memcpy(last,dist,sizeof last);//防止串联情况的发生
for(int j=0;j<m;j++){
auto e=edges[j];
dist[e.b]=min(dist[e.b],last[e.a]+e.c);
}
}
}
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=0;i<m;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
edges[i]={a,b,c};
}
bellman_ford();
if(dist[n]>0x3f3f3f3f/2) puts("impossible");
else printf("%d",dist[n]);
return 0;
}
2)问题:
(1)为什么需要last[]数组?
为了避免如下的串联情况, 在边数限制为一条的情况下,节点3的距离应该是3,但是由于串联情况,利用本轮更新的节点2更新了节点3的距离,所以现在节点3的距离是2。
正确做法是用上轮节点2更新的距离–无穷大,来更新节点3, 再取最小值,所以节点3离起点的距离是3。
(2)为什么是dist[n]>0x3f3f3f3f/2, 而不是dist[n]>0x3f3f3f3f?
SPFA(一般O(m),最坏O(nm))
1)求最短路:
SPFA算法是由Bellman_Ford算法优化而来,Bellman_Ford算法在每一次松弛中会遍历每一条边,但在每次松弛中只有某点的前驱结点更新才会导致该点的更新,我们使用一个队列承载当前可更新的点,对队列中的每个点的后继结点进行更新并出队同时将能更新路径的点入队,直至队列为空,得到最短路径(BFS优化)
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int e[N],ne[N],w[N],h[N],idx;
int n,m;
int dist[N];
bool vis[N];//判断是否在队列中,防止重复入队
void add(int x,int y,int z){
e[idx]=y;
ne[idx]=h[x];
w[idx]=z;
h[x]=idx++;
}
int spfa(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
queue<int>qu;
qu.push(1);
vis[1]=true;
while(qu.size()){
auto t=qu.front();
qu.pop();
vis[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];//若已经在队列中则只需更新dist即可
if(!vis[j]){
vis[j]=true;
qu.push(j);
}
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;//SPFA算法只会更新与源点之间有路径的点,若n与1之间无路径则不会更新dist[n]
return dist[n];
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
if(spfa()==-1) puts("impossible");
else cout<<spfa();
return 0;
}
2)判断图中是否有负权回路:
SPFA算法不可以存在负权回路,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。
求负环一般使用SPFA算法,方法是用一个cnt数组记录每个点到源点的边数,一个点被更新一次就+1,一旦有点的边数达到了n那就证明存在了负环。
为何初始时将所有点存入队列?
在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。那么原图有负环等价于新图有负环。此时在新图上做spfa,将虚拟源点加入队列中。然后进行spfa的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于视频中的做法了。那么视频中的做法可以找到负环,等价于这次spfa可以找到负环,等价于新图有负环,等价于原图有负环。得证。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int e[N],ne[N],w[N],h[N],idx;
int n,m;
int dist[N],cnt[N];
bool vis[N];//判断是否在队列中
void add(int x,int y,int z){
e[idx]=y;
ne[idx]=h[x];
w[idx]=z;
h[x]=idx++;
}
int spfa(){
queue<int>qu;
for(int i=1;i<=n;i++){//将所有点放入队列中
qu.push(i);
vis[i]=true;
}
while(qu.size()){
auto t=qu.front();
qu.pop();
vis[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;
if(cnt[j]>=n) return true;//说明存在负权回路(n条路径说明有n+1个点,抽屉原理得存在回路)
if(!vis[j]){
vis[j]=true;
qu.push(j);
}
}
}
}
return false;
}
int main(){
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
while(m--){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
if(spfa()) puts("Yes");
else puts("No");
return 0;
}
3)SPFA和堆优化Dijkstra的区别和联系
1] Dijkstra算法中的st数组保存的是当前确定了到源点距离最小的点,且一旦确定了最小那么就不可逆了(不可标记为true后改变为false);SPFA算法中的st数组仅仅只是表示的当前发生过更新的点,且spfa中的st数组可逆(可以在标记为true之后又标记为false)。顺带一提的是BFS中的st数组记录的是当前已经被遍历过的点。
2] Dijkstra算法里使用的是优先队列保存的是当前未确定最小距离的点,目的是快速的取出当前到源点距离最小的点;SPFA算法中使用的是队列(你也可以使用别的数据结构),目的只是记录一下当前发生过更新的点。
Floyd算法(O(n^3))
f [ k ] [ i ] [ j ] f[k][i][j] f[k][i][j]表示只经过前 k k k个点,从点 i i i走到点 j j j的最小距离
状态转移:
f [ k ] [ i ] [ j ] = m i n ( f [ k ] [ i ] [ j ] , f [ k − 1 ] [ i ] [ k ] , f [ k − 1 ] [ k ] [ j ] ) f[k][i][j]=min(f[k][i][j],f[k-1][i][k],f[k-1][k][j]) f[k][i][j]=min(f[k][i][j],f[k−1][i][k],f[k−1][k][j])
#include<bits/stdc++.h>
using namespace std;
const int N=210,INF=0x3f3f3f3f;
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[k][j]+d[i][k]);
}
}
}
}
int main(){
scanf("%d%d%d",&n,&m,&q);
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 x,y,z;
scanf("%d%d%d",&x,&y,&z);
d[x][y]=min(d[x][y],z);
}
Floyd();
while(q--){
int x,y;
scanf("%d%d",&x,&y);
if(d[x][y]>INF/2) puts("impossible");
else printf("%d\n",d[x][y]);
}
return 0;
}