最短路径算法简介

最短路径算法是在图中求两点(或多点)之间的最短路径,我们最常见的最短路径算法有四种:Bellman-ford、Dijkstra、SPFA、Floyd。

Bellman-ford算法可以用于有负边权的图,如果途图中有负环,算法也可以检验出来,时间复杂度为O(VE)。

Dijkstra算法只能用于边权为正的图中,时间复杂度为O(n^2)。

SPFA算法是Bellman-ford算法的优化算法,和Bellman-ford算法应用差不多,而且可以用邻接表和队列优化,时间复杂度为O(KE),SPFA的时间复杂度有常数,有的比赛可能会卡常,所以建议求图上最短路的时候用Dijkstra算法。

Floyd可以用于有负权的图中,即使有负环,算法也可以检测出来,可以求任意点的最短路径,有向图和无向图的最小环和最大环。时间复杂度O(n^3)。


 这篇博客主要介绍Dijkstra算法


 

Dijkstra算法

Dijkstra算法是我求图上单元最短路最常用的方法,他可以求出从图上一个点到其他所有点的最短路径,他可以用优先队列(堆)优化,我们先来介绍一下Dijkstra算法不优化的做法。

Dijkstra算法(不优化)

大体思路:先遍历一下还没有在最短路中的点,选出一个距离 已经在最短路集合中的点 距离最近的点,并把它加入到最短路中,并且更新所有点的最短路,直到所有的点都加入到最短路中。

下面我们来详细模拟一下Dijkstra算法的实现过程:

变量名:d[i]表示从起始点到节点i的最短路径  v[i]表示节点i是否被访问过

初始化:所有最短路赋值为+∞,并且使d[1]=0,v[1]=1,因为任何节点到它本身的距离为0,标记1被访问过。

 我们来求一下下面这张图从①到其他节点的最短路径

步骤①:遍历与节点1相连的所有节点,找到距离最近的一个,把这个节点标记为访问过,并更新最短路。因为新加入一条边,可能有很多的最短路会发生改变,所以要更新 所有与最短路径包含的点 相连的点的最短路。由图可知,10>5>1,所以我们把节点4标记为访问过,v[4]=1,并且更新最短路——>d[4]=1。

 

步骤②:遍历与 最短路包含的点 相连的节点,找到距离最近的。2<5<10,所以把节点3加入最短路,并且标记为访问过—— d[3]=3,v[3]=1。

步骤③:遍历与 最短路包含的点 相连的节点,找到距离最近的。5<6<10,所以把节点2加入最短路,并且标记为访问过—— d[2]=5,v[2]=1。

步骤④:遍历与 最短路包含的点 相连的节点,找到距离最近的。6<10,所以把节点5加入最短路,并且标记为访问过—— d[5]=9,v[5]=1。

这样,这张图的最短路径就求出来了,是不是很好理解呢!

下面是代码

#include<iostream>
#include<cstdio>
#define MAXN 2147483647
using namespace std;
int n,m,s;
struct G{
    int u;
    int v;
    int w;
    int next;
}e[500001];
int head[500001];
int d[500001];
int read(){
    int num=0;
    char ch=getchar();
    while(ch<'0' || ch>'9') ch=getchar();
    while(ch>='0' && ch<='9'){
        num=num*10+ch-'0';
        ch=getchar();
    }
    return num;
}
bool v[500001];
int main(){
    int i,j;
    n=read();
    m=read();
    s=read();
    for(i=1;i<=m;i++){
        int a=read(),b=read(),c=read();
        e[i].u=a;
        e[i].v=b;
        e[i].w=c;
        e[i].next=head[a];
        head[a]=i;
    }
    for(i=1;i<=n;i++) d[i]=MAXN; //初始化
    d[s]=0;
    for(i=1;i<=n;i++){
        int k=0,minn=MAXN;
        for(j=1;j<=n;j++) //寻找距离最小的点
          if(minn>d[j] && !v[j]){
              minn=d[j];
              k=j;
          }
        v[k]=1; //标记为访问过
        for(j=head[k];j!=0;j=e[j].next)
            if(!v[e[j].v] && d[e[j].v]>d[e[j].u]+e[j].w) //更新节点
            d[e[j].v]=d[e[j].u]+e[j].w;
    }
    for(i=1;i<=n;i++) cout<<d[i]<<" ";
    return 0;
}

(存图的时候,建议用邻接表,方便,而且速度比数组快)

到此,我们最基础的Dijkstra算法就已经讲完了,下面我们来讲一下堆优化。


 Dijkstra算法堆优化


堆优化用到了优先队列(priority_queue),如果不会优先队列,可以先看一下这个博客学习一下 https://blog.csdn.net/c20182030/article/details/70757660。

我们来回顾一下算法的核心部分:先找最小距离,再更新。在不优化的时候,我们是每次通过循环来寻找最小距离的,这样的话就会有很多不必要的时间浪费掉,所以我们想到了用优先队列优化,因为优先队列会自动按照优先度排序,这样就会省掉寻找最小距离的循环,从而节省一部分时间。

然后我们再想一想优先队列里存放什么东西。我们来看一下不优化之前,在搜索最小距离的时候需要记录的东西——最小距离和节点的编号,所以我们在优先队列里面就放最小距离和节点的编号。为方便操作,我们就申请一个struct或者pair的优先队列,struct的话就申请两个变量,而pair的话(不了解pair的可以看一下这个博客 https://www.cnblogs.com/lvchaoshun/p/7769003.html),就在第一个位置里放距离,第二个位置里放id,因为优先队列默认先按照第一关键字排序,我们的目的就是要快速的找到最小距离,所以按照距离排序。优先队列默认从大到小排序,所以我们要使优先队列从小到大排序,或者是在加入新的二元组的时候,把距离变成负的。

其余的部分和不优化差别不是很大,下面给出代码:

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pi;
const int MAXN=2e6+10;
struct EDGE{
    int u;
    int v;
    int w;
    int next;
}edge[MAXN];
int head[MAXN],tot;
int t,ans;
int n,m;
char a[1000][1000];
int dis[MAXN];
bool vis[MAXN];
priority_queue<pi,vector<pi>,greater<pi> > q;
pi tmp;
int read(){
    int x=0;
    char c=getchar();
    while(c<'0' || c>'9') c=getchar();
    while(c>='0' && c<='9'){
        x=x*10+c-'0';
        c=getchar();
    }
    return x;
}
void add(int x,int y,int z){
    edge[++tot].u=x;
    edge[tot].v=y;
    edge[tot].w=z;
    edge[tot].next=head[x];
    head[x]=tot;
    edge[++tot].u=y;
    edge[tot].v=x;
    edge[tot].w=z;
    edge[tot].next=head[y];
    head[y]=tot;
}
int main(){
    register int i,j;
    int id,x,nd;
    t=read();
    while(t--){
        for(i=1;i<=tot;i++) edge[i].next=edge[i].u=edge[i].v=edge[i].w=head[i]=0;
        tot=0;
        memset(dis,0x3f,sizeof dis);
        memset(vis,0,sizeof vis);
        n=read(),m=read();
        for(i=1;i<=n;++i)
          for(j=1;j<=m;++j){
              cin>>a[i][j];
              if(a[i][j]!='/') add((i-1)*(m+1)+j,i*(m+1)+j+1,0),add((i-1)*(m+1)+j+1,i*(m+1)+j,1);
            else  add((i-1)*(m+1)+j,i*(m+1)+j+1,1),add((i-1)*(m+1)+j+1,i*(m+1)+j,0);
          }
        if((m+n)%2!=0){
            printf("NO SOLUTION\n");
            continue;
        }
        q.push(make_pair(0,1));
        while(!q.empty()){
            tmp=q.top();
            q.pop();
            id=tmp.second,nd=tmp.first;
            if(vis[id]) continue;
            vis[id]=1;
            for(i=head[id];i;i=edge[i].next){
                if(vis[edge[i].v]) continue;
                if(dis[edge[i].v]>nd+edge[i].w){
                    dis[edge[i].v]=nd+edge[i].w;
                    q.push(make_pair(dis[edge[i].v],edge[i].v));
                }
            }
        }
        printf("%d\n",dis[(n+1)*(m+1)]);
    }
    return 0;
}

(洛谷原题链接https://www.luogu.org/problemnew/show/P2243)

(本博客部分内容摘自:https://blog.csdn.net/qq_36386435/article/details/77403223)