最短路四种算法
小技巧:稠密图用邻接矩阵来存储,稀疏图用邻接表存储
一 Dijkstra算法
1.朴素Dijkstra算法(适合于稠密图,边权都是整数)
算法步骤:时间复杂度为o(n^2)
①初始化距离,初始化第一个点到起始点的距离为0,其余各点到起始点的距离为正无穷
②用for循环循环n次,每次求出当前没有确定最短距离的点当中距离的最短的那个点,用它来更新其他各点到起始点的最短距离。具体做法为:可以用一个标记数组来标记这个点是否已经确定了最短距离,定义一个变量t,初始化为-1,表示当前没有确定最短路的点当中距离最短的那个点,枚举所有点,如果这个点没有确定最短路并且如果t为-1或者t到起点的距离不是最小的话,就更新t为j。然后标记t,表示已经确定了最短路。然后再用t去更新一下其余各点到起始点的最短距离。更新操作(用1到t的距离加上t到j这条边来更新1到j这条边,求最小值)。
实例:
输入:
3 3
1 2 2
2 3 1
1 3 4
输出:3
代码实例:
#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e3 + 10, null = 0x3f3f3f3f,M=2*N;
int n,m;
int g[N][N];//邻接矩阵,用来存储所有边,适合于稠密图
int d[N];//存储每个点到起始点的距离
bool vis[N];//标记数组,标记这个点是否已经确定了最短路
int djk()
{
memset(d,0x3f,sizeof d);//初始化距离为正无穷
d[1]=0;//初始化第一个点距离为0
for(int i=0;i<n;i++)//循环n次
{
int t=-1;
for(int j=1;j<=n;j++)
{
if(!vis[j]&&(t==-1||d[t]>d[j]))//如果j这个点没有确定最短路并且如果t为-1或者或者t不是最短的,就更新一下t为j
t=j;
}
vis[t]=true;//将这个点标记一下,表示已经确定了最短路
for(int j=1;j<=n;j++)
{
d[j]=min(d[j],d[t]+g[t][j]);//更新一下所有点的最短路,用1到t的距离加上t~j这条边来更新一下1~j这条边
}
}
if(d[n]==0x3f3f3f3f) return -1;//如果最后一个点到起始点的距离为无穷的话,说明之间不存在路径
else return d[n];
}
int main()
{
ios;
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);//由于a到b之间可能存在多条变,因此只保留最短的那条边
}
cout<<djk()<<endl;//直接输出即可
return 0;
}
2.堆优化版的Djkstra算法 (适合于稀疏图)
算法思想:
采用优先队列进行优化,即采用小根堆,将每个点的到起始点的距离及其编号插入到优先队列当中,保证每次堆顶元素就是当前没有确定的最短路的点当中距离最短的那个点。
由于一个点到起始点的距离和这个点的编号一共有两个值,故可用pair类型进行存储,首先将第一个点到起始点的距离及其编号插入到优先队列当中,只要队列不空,每次取出对头元素,即是当前没有确定最短路的点当中距离最短的那个点,然后将这个已经确定最短路的点直接进行出队操作,然后再用这个点去更新其余各点到起始点的距离,并且将更新后点的距离及其编号入队。(这样便可以保证每次队列当中的点都是没有确定最短路的点了,并且优先级最高的元素即堆顶元素就是没有确定最短的点当中距离最近的那个点。)
typedef pair<int,int> PII;
queue<PII,vector<PII>,greater<PII>> heap;//创建一个小根堆,堆中的元素类型为pair类型
代码实例:
#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e3 + 10, null = 0x3f3f3f3f,M=2*N;
int n,m;
int h[N],e[N],w[N],ne[N],idx;
int d[N];
bool vis[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
int djk()
{
memset(d,0x3f,sizeof d);
d[1]=0;
priority_queue<PII,vector<PII>,greater<PII>> heap;//建立优先队列,数字小的优先级越大
heap.push({0,1});//先把第一个点存入到队列中
while(heap.size())
{
auto ans=heap.top();//取出队头,队头就是距离最小的点,
heap.pop();
int ver=ans.second;//距离最小的点的编号
int dis=ans.first;//距离最小的点的距离
if(vis[ver]) continue;//如果这个点已经确定最短路的话,就不用对它进行处理了
vis[ver]=true;
for(int i=h[ver];i!=-1;i=ne[i])//遍历距离最小的点的所有邻边
{
int j=e[i];//求出链表中结点在图中的编号
if(d[j]>dis+w[i]) d[j]=dis+w[i],heap.push({d[j],j});//更新1~j这条边的距离,求最小值
}
}
if(d[n]==0x3f3f3f3f) return -1;
else return d[n];
}
int main()
{
ios;
cin>>n>>m;
memset(h,-1,sizeof h);//初始化所有元素为-1,表示每个链表为空
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
cout<<djk()<<endl;
return 0;
}
二 Bellman-ford算法(可以用来处理有负权边的图,保证没有负权回路,适用于有边限制的图)
算法思想:用结构体来存储每条边,结构体数组来存储所有边,数组d来存储每个点到起始点的最短距离,初始化为正无穷,初始化第一个点到起始点的距离为0,数组bac来备份数组d,用for循环循环k次,每次注意要备份一下(采用memcpy()函数备份,防止串联),每次枚举一下所有边,更新一下最短距离即可(用1到a的距离加上a到b这条边去更新1到b的距离)。
实例:求1号点到n号点最多不超过k条边的最短距离。
输入:
3 3 1
1 2 1
2 3 1
1 3 3
输出:3
代码实例:
#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e3 + 10, null = 0x3f3f3f3f,M=2*N;
int n,m,k;
int d[N];//存储每个点到起始点的最短距离
int backup[N];//备份数组d
typedef struct edge
{
int a,b,w;
}edge;
edge edges[N];//用一个结构体数组来存储所有边,每条表采用一个结构体来表示
int bel()
{
memset(d,0x3f,sizeof d);//初始化所有点到起始点的距离为正无穷
d[1]=0;//第一个点到起始点的距离为0
for(int i=0;i<k;i++)//循环k次,一般题目要求不超过k条边
{
memcpy(backup,d,sizeof d);//备份,将数组d的元素复制到数组backup中
for(int j=0;j<m;j++)//枚举所有边
{
int a=edges[j].a;
int b=edges[j].b;
int w=edges[j].w;
d[b]=min(d[b],backup[a]+w);//更新最短距离
}
}
if(d[n]>0x3f3f3f3f/2) return -1;
else return d[n];
}
int main()
{
ios;
cin>>n>>m>>k;
for(int i=0;i<m;i++)
{
int a,b,w;
cin>>a>>b>>w;
edges[i]={a,b,w};//将所有边存储到结构体数组中
}
int t=bel();
if(t==-1) cout<<"impossible"<<endl;
else cout<<t<<endl;
return 0;
}
三 SPFA算法(适用于存在负权边但没有负环的图)
一般情况下:既可以过有负权边的图,又可以过无负权边的图,并且比djkstra算法要快
算法思想:
可以采用队列进行优化(bfs的思想,与djkstra算法比较像),用一个标记数组来判断点是否在队列中,数组d来存储每个点到起始点的距离,利用邻接表存储方式,将所有点到起始点的距离置为正无穷,并且标记这些点不在队列当中,第一个点到起始点的距离置为0,标记这个点在队列当中,首先将第一个点放置到队列中,只要队列不空,每次取出队头元素,并且将队头元素删掉,标记一下队头元素不在队列中,遍历一下队头元素的所有邻边,每次求出每个节点在图当中的编号,更新一下最短距离,如果该点不在队列中,则入队,同时这个点标记在队列中。
输入:
3 3
1 2 5
2 3 -3
1 3 4
输出:2
代码实例:
#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e3 + 10, null = 0x3f3f3f3f,M=2*N;
int n,m;
int h[N],e[N],ne[N],w[N],idx;//邻接表存储法
int d[N],q[N];//数组d存储每个点到起始点的距离,q为队列,用来存储距离最短的点
bool vis[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(d,0x3f,sizeof d);//初始化所有点到起始点的距离为0,第一个点到起始点的距离为0
d[1]=0;
int hh=0,tt=0;
q[0]=1;//将第一个点放入队列中
while(hh<=tt)//只要对列表不空
{
int t=q[hh++];//取出队头元素,队头指针加一
vis[t]=false;//标记该元素不在队列中
for(int i=h[t];i!=-1;i=ne[i])//枚举队头元素的所有出边
{
int j=e[i];//求出链表中该结点在图中的编号
if(d[j]>d[t]+w[i]) //如果j到起点的距离不是最短的,更新一下即可
{
d[j]=d[t]+w[i];
if(!vis[j])//如果该元素不在队列中
{
q[++tt]=j;//将元素j入队
vis[j]=true;//标记为在队列中
}
}
}
}
if(d[n]==0x3f3f3f3f) return -1;//如果最后一个点到起始点的距离为无穷,则说明路径不存在,返回-1即可
else return d[n];//返回最后一个点到起始点的距离
}
int main()
{
ios;
cin>>n>>m;
memset(h,-1,sizeof h);//初始化所有元素的值为-1,表示链表为空
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);//建立a~b这条边,权值为c
}
if(spfa()==-1) cout<<"impossible"<<endl;
else cout<<spfa()<<endl;
return 0;
}
1.SPFA算法判断负环;
算法思想:重新定义一个数组cnt,表示最短路的边数,将所有点都放到队列中去,并且标记该点在队列中,此外不需要初始化数组d,因为并没有要求负环是从起点开始的,每次更新最短距离的时候,更新一下cnt数组,同时判断数组的值是否大于等于n,如果大于等于的n的话,则说明存在负环,否则说明不存在负环。
输入:
3 3
1 2 -1
2 3 4
3 1 -4
输出:Yes
代码实例:
#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e3 + 10, null = 0x3f3f3f3f,M=2*N;
int n,m;
int h[N],e[N],w[N],ne[N],idx;
int d[N],q[N],cnt[N];
bool vis[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c;
ne[idx]=h[a],h[a]=idx++;
}
bool spfa()
{
int hh=0,tt=-1;
for(int i=1;i<=n;i++)
{
q[++tt]=i;
vis[i]=true;
}//将所有点都放置到队列中,并且标记该点在队列中
while(hh<=tt)
{
int t=q[hh++];
vis[t]=false;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(d[j]>d[t]+w[i])
{
d[j]=d[t]+w[i];
cnt[j]=cnt[t]+1;//更新一下最短路边数
if(cnt[j]>=n) return true;//如过边数大于等于n的话,说明存在负环
if(!vis[j])
{
q[++tt]=j;
vis[j]=true;
}
}
}
}
return false;
}
int main()
{
ios;
cin>>n>>m;
memset(h,-1,sizeof h);
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
if(spfa()) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
四 floyd算法(求多元汇最短路,可以处理带负权边的图,但不允许有负环)
算法思想:
首先初始化邻接矩阵中的元素为正无穷(矩阵中存储的是各个边的权值),由于可能出现重边的情况,因此只保留最小的那条边即可。
输入:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出:
impossible
1
代码实例:
#include<iostream>
#include<algorithm>
#include<iostream>
#include<algorithm>
#include<vector>
#include<set>
#include<stack>
#include<queue>
#include<string>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<ctype.h>
#include<iomanip>
#include<fstream>
using namespace std;
#define endl '\n'
#define ios ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
typedef unsigned long long ULL;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 1e3 + 10, null = 0x3f3f3f3f,M=2*N;
int n,m,q;
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()
{
ios;
cin>>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]=null;
}
}//初始化
/*
memset(d,0x3f,sizeof d);
*/
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;
if(d[a][b]>null/2) cout<<"impossible"<<endl;//如果a到b的最短距离大于一个较大的数,则说明不存在路径,输出impossible
else cout<<d[a][b]<<endl;
}
return 0;
}