最短路算法
在图中,我们常常会需要知道某一点到其余点最短路或者各点之间的最短路,此时我们就需要借助最短路算法,其中,Dijkstra算法和Floyd算法,Bellman-Ford算法,spfa算法都是是最为常用的最短路算法
朴素Dijkstra算法
Dijkstra算法非常重要的解决单源最短路的算法,用于处理某一点到其余各点最短路,时间复杂度为O(n^2)
缺点:无法处理带负权边的最短路
算法步骤:
假设我们希望求出1号点到其余各点之间的距离最小值
1.我们定义一个dist数组,初始化dist[1]=0,其余均为正无穷
2.我们设想一个集合S,表示我们已经用过确定了最小值的点
3.我们每次找出一个不在S中的点,但它满足到S的距离最小的点k,并用它来更新dist数组
4.如果上一步中出现了dist[k] >dist[i] +g[i] [k];我们就更新dist的值
5.反复执行上面的3、4两步,直到每一个点都被用于确定一次最值过
代码实现:
例题:给定点数n,边数m,要求n号点到1号点最短路,若无法到达输出-1,注意:可能有重边
AC Code:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=510;
int g[N][N];
int n,m;
int dist[N];
bool st[N];
int dijkstra(){
memset(dist,inf,sizeof dist);
dist[1]=0;
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])){
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]==inf) return -1;
return dist[n];
}
int main(){
cin>>n>>m;
memset(g,inf,sizeof g);
while(m--){
int a,b,c;
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c);
}
int t=dijkstra();
cout<<t<<endl;
return 0;
}
上面是一种朴素的Dijkstra算法,我们下面看看堆优化后的Dijkstra算法
堆优化Dijkstra算法
优化方式:
我们使用优先队列来简化寻找距离最近点的操作,对于数据范围比较大的图,比较适用
上一题如果使用这种方式来写,代码如下:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int,int> pii;
const int N=2e6+10;
int h[N],w[N],e[N],ne[N],idx;
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> >heap;
heap.push({0,1});
while(heap.size()){
auto t=heap.top();
heap.pop();
int ver=t.second;
int dt=t.first;
if(st[ver])continue;
st[ver]=1;
for(int i=h[ver];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dt+w[i]){
dist[j]=dt+w[i];
heap.push({dist[j],j});
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
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);
}
int t=dijkstra();
cout<<t<<endl;
return 0;
}
Floyd算法
Floyd算法是解决多源最短路问题的最为常用的算法,其时间复杂度较高,是O(n^3),但也正因如此,其代码比较简单,思路比较清晰
算法步骤:
Floyd算法原理是动态规划,其状态表示为d(k,i,j),意思是i到j,只经过1到k这些中间点的路径长度
其状态转移为:d(k,i,j)=d(k-1,i,k)+d(k-1,k,j)
其中k是可以优先枚举的,因此我们用二维数组即可
注意: 无法处理含有负权回路的图(这样的图最短路径不一定存在)
代码实现:
#include<iostream>
#include<cstring>
using namespace std;
int n,m,k;
const int N=210;
const int inf=0x3f3f3f3f; //0x3f3f3f3f没有1e9大,也没有INT_MAX大,后面二者会溢出int
int g[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++){
g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
}
}
}
}
int main(){
cin>>n>>m>>k;
memset(g,inf,sizeof g);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j){
g[i][j]=0; //自环的处理
}
}
}
while(m--){
int a,b,c;
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c); //重边的处理
}
floyd();
while(k--){
int x,y;
cin>>x>>y;
if(g[x][y]>=inf/2) cout<<"impossible"<<endl;
else cout<<g[x][y]<<endl;
}
return 0;
}
Bellman-Ford算法
Bellman-Ford算法常用于解决带有负权边的最短路问题,算法非常简单,并且对于边的存储方式要求很低,可以判断负环(不常用)
时间复杂度:O(mn)
算法步骤:
进行n次循环,每次循环遍历所有的边,假设边是a->b,权重为w,那么更新dist[b]=min(dist[b],dist[a]+w);
这种更新的操作也被叫做松弛操作,
注意: 存在负权回路不一定有最短路
Bellman-Ford算法通常用于解决有边数限制的带负权边的最短路问题,并且其允许合理的负环
代码实现:
例题:给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
注意:图中可能 存在负权回路 。
输入:第一行包含三个整数 n,m,k
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
点的编号为 1∼n。
输出:输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出 impossible
#include<iostream>
#include<cstring>
using namespace std;
const int N=510,M=100010;
int dist[N],last[N];
struct node{
int x,y,w;
}edges[M];
int n,m,k;
void bellman()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i ++ ) //最多经过k条边,所以只更新k次
{
memcpy(last, dist, sizeof dist); //last数组作用是防止新更新的值的影响,有点类似数据库事务里的脏读错误
for (int j = 0; j < m; j ++ )
{
auto e = edges[j];
dist[e.y] = min(dist[e.y], last[e.x] + e.w);
}
}
}
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};
}
bellman();
if(dist[n]>=0x3f3f3f3f/2) puts("impossible");
else cout<<dist[n]<<endl;
return 0;
}
SPFA算法
SPFA算法同样可以解决带负权的最短路问题,同时还可判断是否存在负环,该算法比Bellman-Ford更为优秀
算法步骤:
1.建一个队列,将起点放进去,队列存所有dist变小的节点
2.只要队列不空,取出队列头t,并且删除,更新t的所有出边,如果成功,就把另一头的点加进队列(不重复加入)
我们不难看出,SPFA优化了Bellman-Ford算法,避免了更新不必要的边
代码实现:
例题1: 同上一题,只是没有边数限制
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int,int> pii;
const int N=2e6+10;
int h[N],w[N],e[N],ne[N],idx;
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++;
}
void spfa(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
queue<int>q;
q.push(1);
st[1]=true;
while(!q.empty()){
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;
}
}
}
}
}
int main(){
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);
}
spfa();
if(dist[n]==0x3f3f3f3f)puts("impossible");
else cout<<dist[n]<<endl;
return 0;
}
我们不难看出,SPFA求最短路的实现与Dijkstra很是相似
例题2:判断负环
SPFA算法适用于判断负环,并且实现简单,是最为常用的判断负环的算法
题目与上一题一致,只是输出改为输出是否有负环
算法思路:
我们另外建一个cnt数组表示最短路的边数,假设边是t->x,更新dist[x]时候,我们把cnt[x]=cnt[t]+1
如果发现cnt[i]>=n,那么一定存在负环,因为如果是正环,我们不会去更新到这里
代码实现:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int,int> pii;
const int N=2e6+10;
int h[N],w[N],e[N],ne[N],idx;
int dist[N]
bool st[N];
int cnt[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++;
}
bool spfa(){
queue<int>q;
for(int i=1;i<=n;i++){ //起点此时不一定是1,要把所有点都考虑进去
st[i]=true; //我们只关心是否有负环,所有dist数组不需要初始化为inf
q.push(i);
}
while(!q.empty()){
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;
if(cnt[j]>=n){
return true;
}
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
return false;
}
int main(){
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);
}
bool res=spfa();
if(res)cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}