最短路之Dijkstra

简介

Dijkstra算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径。它的主要特点是以起始点为中心向外层层扩展(BFS思想),直到扩展到终点为止

Dijkstra不能处理带负权和带有回路的图

算法思想
  1. 设起点为u,引入两个集合S、U,S集合包含已求出的最短路径的点,U集合记录未求出最短路径的点以及到起点的距离

  2. 初始化两个集合,S集合初始时只有起点,U集合初始时为起点到其他节点的距离(详细来说是起点到本身的距离为0,起点未直接连接点的距离为INF)

  3. 从U集合中找出路径最短的点k,加入S集合并从U集合中删去该点

  4. 遍历所有节点,如果某节点v使得u到k再到v的距离小于当前u到v的距离,则更新U中相应的距离

  5. 循环上述3、4步骤,直至遍历结束,得到起点到其他节点的最短路径

简单分析上述思路,我们可以得到下列伪代码(紫书p359)

清除标记数组中所有点的标记
设d[s]=0,其他d[i]=INF,s是起点
循环n次{
	在所有未标记节点中选出d值最小的节点k
	给节点k标记
	对于从k出发的所有边(k,i),更新d[i]=min{d[i],d[k]+w(k,i)}
}

其中"更新d[i]=min{d[i],d[k]+w(k,i)}"称为边(k,i)的松弛操作

代码实现

邻接矩阵

时间复杂度O(V2),空间复杂度O(V2)

#define INF 0x3f3f3f3f
const int N=1e3+10;
int G[N][N]; //邻接矩阵
int d[N];   //存起点到其他节点的最短路径
bool vis[N]; //显示当前节点的最短路径是否更新
int n;  //节点个数

void Dijkstra(int u){
    memset(d,0x3f,sizeof(d));
    memset(vis,0,sizeof(vis));
    d[u]=0;
    for(int i=1;i<=n;i++){
        int k,m=INF;
        for(int j=1;j<=n;j++)
            if(!vis[j]&&d[j]<=m){
                m=d[j];
                k=j;
            }
        vis[k]=1;
        for(int j=1;j<=n;j++) d[j]=min(d[j],d[k]+G[k][j]);   
    }
    for(int i=1;i<=n;i++) printf("%d%c",d[i],i==n?'\n':' ');
}

邻接表

实际上我们发现上面的邻接矩阵的思路和算法思想还不太一致。但是当我们写邻接表就能发现我们使用的队列相当于集合U

第一种方法是我在学习紫书时学到的,这里并没有开vector<edge>套vector<edge>的二维动态数组,而是直接用了一维的vector<edge> edges保存所有的边,然后使用二维动态数组vector<int> G[N]来保存每一个起点对应的edges数组中的下标。实际上这和链式前向星类似,但是却没有链式前向星节省空间

vector数组保存的是边的编号,有了编号之后就可以从edges数组中查到边的具体信息。这样,“对于从k出发的所有边(k,i),更新d[i]“就变成了"for(int i=0;i<G[u].size();i++) 执行边edges(G[u][i])上的松弛操作”。从整体上看,每条边恰好被检查过一次,因此松弛执行的次数恰好是m次。因此只要想办法找到"未标记的d值最小的节点k”

显然我们需要优先队列维护d[i],即d[i]越小就应该越先出队,因此需要最小堆维护的优先队列priority_queue<int,vector<int>,greater<int> > q;然而我们不但需要最小值,还需要最小值对应的编号,因为我们需要根据编号u从边集中取出G[u][i]。然后我们直接用STL中提供的pair,但是需要注意的是,我们需要make_pair(d[i],i),因为比较时是取最小的d[i],而pair的比较是先比较first再比较second

为了使用方便,下面把该算法封装到一个结构体中

typedef pair<int,int> P;
const int maxn=1e5+10;   //如果需要打印出路径,那么1e5会爆内存
const int maxm=2e5+10;

struct edge{
    int from,to,w;
    edge(int a,int b,int c):from(a),to(b),w(c){}
}

struct Dijkstra{
    int n,m;  //节点数和边数
    vector<edge> edges;
    vector<int> G[maxn]; //记录的是每个from下的所有to边的编号
    bool vis[maxn]; //判断是否标记
    int d[maxn];    //起点到各节点的距离
    //int p[maxn];  //最短路的上一条边
    
    void init(int n){  //初始化结构体
        this->n=n;
        for(int i=0;i<=n;i++) G[i].clear();
    }
    
    void addEdge(int from,int to,int w){
        edges.push_back(edge(from,to,w));
        m=edges.size();         //不断更新边数
        G[from].push_back(m-1); //因为每条边的编号在edges数组都是唯一的,因此只需保存编号即可(编号从0开始,因此需要m-1)
    }
    
    void dijkstra(int s){
    	priority_queue<P,vector<P>,greater<P> > q;
        memset(vis,0,sizeof(vis));
        memset(d,0x3f,sizeof(d));
        d[s]=0;
        q.push(make_pair(0,s)); //先构造一个s到本身的pair,很明显d[s]=0
        while(!q.empty()){
            P pr=q.top();
            q.pop();
            int u=x.second;
            if(vis[u]) continue;
            vis[u]=1;
            for(int i=0;i<G[u].size();i++){
                edge &e=edges[G[u][i]];
                if(d[e.to]>d[u]+e.w){
                    d[e.to]=d[u]+e.w;
                    //p[e.to]=G[u][i]; //记录最短路
                    q.push(make_pair(d[e.to],e.to)); //有多条起点和终点一样的边,都要入队
                }
            }
        }
    }
    
    void Print(){ //打印d[i]
        for(int i=1;i<=n;i++) printf("%d ",d[i]);
    }
    
};

链式前向星

了解过链式前向星我们不难发现实现思路和邻接表一模一样。但是链式前向星无论从时间还是空间都要优于上述邻接表写法,因此我们以后写邻接表就尽量使用链式前向星

下面代码还有一点点变化,这也是紫书上提到的,我们可以不使用vis标记数组,而是用"if(pr.first!=d[u]) continue;",因为我们这里是看队列中取出的d的最小值是否和d[u]相等,如果不相等那么就不用判断了,因为入队后取出的一定是最短路,而我们的d[u]在上一次也更新到了最短路的值

typedef pair<int,int> P;
const int maxn=1e5+10; 
const int maxm=2e5+10;

struct node{
	int to,next,w;
};

struct Dijkstra{
    int n;
    int tot;
    int head[maxn];
	node edge[maxm];
    int d[maxn];

    void init(int n){
        this->n=n;
        tot=0;
    }

    void addEdge(int u,int v,int w){
        tot++;
        edge[tot].w=w;
        edge[tot].to=v;
        edge[tot].next=head[u];
        head[u]=tot;
    }

    void dijkstra(int s){
        priority_queue<P,vector<P>,greater<P> > q;
        memset(d,0x3f,sizeof(d));  //如果d可能超过long long,那么这里需要初始化为1e16
        d[s]=0;
        q.push(make_pair(0,s));
        while(!q.empty()){
            P pr=q.top();
            q.pop();
            int u=pr.second;
            if(pr.first!=d[u]) continue;
            for(int i=head[u];i;i=edge[i].next){
                int v=edge[i].to;
                int w=edge[i].w;
                if(d[v]>d[u]+w){
                    d[v]=d[u]+w;
                    q.push(make_pair(d[v],v));
                }
            }
        }
    }

    void Print(){
        for(int i=1;i<=n;i++) printf("%d ",d[i]);
    }

};
打印最短路径

设置一个数组path[i]记录第i个节点的前驱。我们可能会想为什么不记录后继呢?假设下面是已经得到了最短路的某图,我们很容易看出,对于所有节点来说,其后继可能有多个(尤其是起点),因此后继是保存不了的。那么我们从终点回到起点,显然对于每个终点有且仅有一条路径到起点,那么我们只要保存每个点的前驱,除了起点初始化为0,其他点的节点前驱都不为0,那么就可以只用一个一维数组维护所有的最短路径了

在这里插入图片描述

i1234
path[i]0121

但是由于从后往前只能得到逆序,因此保存路径再逆序输出即可

int path[maxn],ans[maxn];

void dijkstra(){
	......
	for(int i=head[u];i;i=edge[i].next){
        int v=edge[i].to;
        int w=edge[i].w;
        if(d[v]>d[u]+w){
            d[v]=d[u]+w;
            path[v]=u;
            q.push(make_pair(d[v],v));
        }
    }
    ......
}
void PrintPath(){
        int cnt=0;
        for(int i=n;i;i=path[i]) ans[++cnt]=i;
        for(int i=cnt;i;i--) printf("%d%c",res[i],i==1?'\n':' ');
}
例题

下面附上洛谷单源最短路径模板题P4779及参考代码:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;
#define MAXN 0x7fffffff
#define INF 0x3f3f3f3f
typedef pair<int,int> P;
const int maxn=1e5+10;
const int maxm=2e5+10;

struct node{
	int to,next,w;
};

struct Dijkstra{
    int n;
    int tot;
    int head[maxn];
	node edge[maxm];
    int d[maxn];
    //bool vis[maxn];

    void init(int n){
        this->n=n;
        tot=0;
    }

    void addEdge(int u,int v,int w){
        tot++;
        edge[tot].w=w;
        edge[tot].to=v;
        edge[tot].next=head[u];
        head[u]=tot;
    }

    void dijkstra(int s){
        priority_queue<P,vector<P>,greater<P> > q;
        //memset(vis,0,sizeof(vis));
        memset(d,0x3f,sizeof(d));
        d[s]=0;
        q.push(make_pair(0,s));
        while(!q.empty()){
            P pr=q.top();
            q.pop();
            int u=pr.second;
            if(pr.first!=d[u]) continue;
            for(int i=head[u];i;i=edge[i].next){
                int v=edge[i].to;
                int w=edge[i].w;
                if(d[v]>d[u]+w){
                    d[v]=d[u]+w;
                    q.push(make_pair(d[v],v));
                }
            }
        }
    }

    void Print(){
        for(int i=1;i<=n;i++) printf("%d%c",d[i]==INF?MAXN:d[i],i==n?'\n':' ');
    }

};

/*struct edge{
    int from,to,w;
    edge(int a,int b,int c):from(a),to(b),w(c){}
};

struct Dijkstra{
    int n,m; 
    vector<edge> edges;
    vector<int> G[maxn]; 
    bool vis[maxn]; 
    int d[maxn];  
    //int p[maxn];  

    void init(int n){
        this->n=n;
        for(int i=0;i<=n;i++) G[i].clear();
    }

    void addEdge(int from,int to,int w){
        edges.push_back(edge(from,to,w));
        m=edges.size();    
        G[from].push_back(m-1); 
    }

    void dijkstra(int s){
    	priority_queue< P,vector< P>,greater<P> > q;
        memset(vis,0,sizeof(vis));
        memset(d,0x3f,sizeof(d));
        d[s]=0;
        q.push(make_pair(0,s));
        while(!q.empty()){
            pair<int,int> pr=q.top();
            q.pop();
            int u=pr.second;
            if(vis[u]) continue;
            vis[u]=1;
            for(int i=0;i<G[u].size();i++){
                edge &e=edges[G[u][i]];
                if(d[e.to]>d[u]+e.w){
                    d[e.to]=d[u]+e.w;
                    q.push(make_pair(d[e.to],e.to));
                }
            }
        }
    }

    void Print(){
        for(int i=1;i<=n;i++) printf("%d%c",d[i]==INF?MAXN:d[i],i==n?'\n':' ');
    }
};*/

int main(){
    int n,m,a,b,c,x;
    Dijkstra dj;
    scanf("%d%d%d",&n,&m,&x);
    dj.init(n);
    while(m--){
        scanf("%d%d%d",&a,&b,&c);
        dj.addEdge(a,b,c);
    }
    dj.dijkstra(x);
    dj.Print();
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值