天天看点

dijkstra 算法_【图论与树】最短路径Dijkstra与Floyd算法

在图论中有一个非常常见的问题,就是最短路径的问题。常见的最短路径算法有三种:dijkstra,floyd和SPFA。本文将带你了解前两种最短路算法,他们分别适用于不同场景下的问题。

以下笔记乃是参考西电ACM暑假培训张帆大佬的讲课以及优先队列博客

Dijkstra

首先我们必须了解这个算法的两个局限之处,才能做题

  • 不能适用于负权图
  • 只适用于单源最短路问题

如果权重是负数,那就直接pass这个算法,在这里原因不表。

单源最短路径

就是指只有一个出发点,到其他点的最短路径问题,以下问题也在这个前提下展开。

下面我们就来说说算法流程:

  • S1. 设置dis数组,

    dis[i]

    表示起点start到i的距离。
  • S2. 从点集V中弹出一个dis值最小且未以该点为起点进行松弛操作的点。
  • S3. 从该点松弛与其领接的各个点更新dis数组,返回S2,循环进行。
  • 通过优先队列的操作可以优化S2,之后详细说明。
dijkstra 算法_【图论与树】最短路径Dijkstra与Floyd算法

如果这样说,有点抽象。那就举个例子。这个例子也是洛谷的单源最短路径的模板题,请求出1到各点的最短路?

很显然你用肉眼看,1到本身是0,1到2、3、4的最短路分别为2,4,3。那dijkstra的操作流程是什么呢?

首先我们先开一个dis数组,让数组的值足够大,

dis[i]=0x7fffffff

,从1开始出发,令

dis[1]=0

,发现与1相连的有三个点234,那我们一个个进行松弛操作,比较

if (dis[1]+w[i]<dis[i])

,w表示各边的权重,如果小于,那就让其覆盖本身的dis值,即

dis[i]=dis[1]+w[i]

,这一波更新完后,234的值分别为2,5,4。

然后,我们需要让234全部入队,并选取dis值最小的数即2继续进行松弛操作,发现连接的是3和4,继续更新,这波结束,234的值分别为2,4,3。

接着,是上一轮dis值次小的点4,进行操作,但是4没有出的边,所以不进行操作。

最后就是剩下的一个3了,3和4还有一条权边,但是4最小的dis值依旧是3。

下面我们发现算法到这就截止了,为什么呢,因为S2的一句话,未进行松弛的点,早在第一轮234就已经全部进入过队列并且已经弹出过了,所以之后他们也不会再进入队列,我们可以设置一个bool类型的

vis[i]

数组代表第i个点是否被访问过了,如果访问过了就结束此循环,或者直接不push进入队列。

这就是整个dijkstra的算法,其实很好理解,证明我们就先略过了。

优先队列priority_queue

上面那个算法有个问题,就是怎样才能保证每次弹出的都是dis最小的数呢?如果用普通队列,不能解决这个问题,如果每次都遍历一遍来找,那复杂度直接O(VE),百分百会被T掉。

所以我们这里采用优先队列priority_queue对S2进行堆优化,就变成O((V+E)logV)了,所以做题的时候堆优化+dijkstra都是一起出现的,模板也是一起写的。

优先队列

:按照一定次序的队列结构,升序或降序,从小到大排列,push和pop都遵循其规律。

优先队列的详细用法请参照这篇博客,本文仅描述dijkstra模板下的用法。

首先对于图论中的

每条点

,我们都需建立一个

node

结构体,包含两个内容

num

dis

。分别表示该点的序号以及dis值。

struct node{
    int num,dis;
};
priority_queue <node> q;
           

然而这样建立优先队列是会报错的,因为优先队列无法对结构体进行优先级比较。所以我们就想办法告诉程序,我们的队列是对结构体的哪儿个值进行排序,其实做法有很多,我习惯的方式是采取重载的形式。

struct node{
    int num,dis;
    bool operator < (const node &x)const{
        return x.dis < dis; // dis小的结构体在队列中的优先级高
    }
};
priority_queue <node> q;
           

请重载<号,别重载>会报错的。这里如果不理解的就死记硬背把,讲重载的话又要涉及很多知识。

【模板】Dijkstra算法实现

以洛谷模板题为例,P4779 【模板】单源最短路径(标准版)

这道题目的样例就是我上面所举的例子。上面我们解决了算法思路,以及用优先队列的方式对步骤2弹出dis最小的点进行了堆优化,下面我们还需要解决程序细节的问题。

先是存边的问题,这道题不存在重边等为难我们的限制,所以直接构造结构体

edge

struct edge{
    int to,cost; // to代表下一个点,cost代表该边的权重
};
vector <edge> e[maxn];
           

之后就是存边,然后进行Dijkstra算法,我直接上代码了,我的代码还是新手向的,不会设计复杂的操作。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
const int maxm=2e5+5;
​
struct edge{
    int to,cost;
};
​
struct node{
    int num,dis;
    bool operator < (const node &x)const{
        return x.dis < dis;
    }
};
​
vector<edge> e[maxm];
int dis[maxn];
bool vis[maxn]; // 布尔型vis数组
int n,m,s,cnt=0;
priority_queue <node> q;
​
void add_edge(int u,int v,int w) // 存边
{
    e[u].push_back((edge){v,w});
}
​
void dijkstra()
{
    dis[s]=0;
    q.push((node){s,0});
    while (!q.empty())
    {
        node tmp=q.top();
        q.pop();
        int x=tmp.num;
        if (vis[x]) continue;
        vis[x]=1;
        for (edge k:e[x]) // 遍历边信息,请注意洛谷请选用c++11,否则编译错误
        {
            if (dis[x] + k.cost < dis[k.to])
            {
                dis[k.to]=dis[x]+k.cost;
                if (!vis[k.to]) q.push((node){k.to,dis[k.to]});
            }
        }
    }
}
​
int main()
{
    cin>>n>>m>>s;
    int i;
    for (i=1;i<=n;i++) dis[i]=0x7fffffff;
    for (i=0;i<m;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
        add_edge(u,v,w);
    }
    dijkstra();
    for (i=1;i<=n;i++) cout<<dis[i]<<" ";
    return 0;
}
           

P1608 路径统计

题目地址:P1608 路径统计

这题基本也是个Dijkstra的模板题,大方向都没有任何变化。我们首先先考虑一个比较简单的问题,就是题目中的重边该怎么去除,重边我们当然选择最短的那条边,其实也不复杂。我采取的方法是邻接矩阵的办法,再把邻接矩阵转为边信息,考虑到N最大值为2000,二维矩阵肯定不会炸。(忽略奇怪的缩进

for (i=1;i<=m;i++)
    {
        int p,q,c,tmp;
        cin>>p>>q>>c;
        if (cmp[i][j]==0 || c<cmp[i][j]) cmp[i][j]=c;
    }
    for (i=1;i<=n;i++)
        for (j=1;j<=n;j++)
            if (cmp[i][j]) e[i].push_back((edge){j,cmp[i][j]});
           

这样就把重边给处理了,菜鸡的我也没想到其他方法了。。。

然后就是来处理本题的问题,最短路径非常简单,但是统计个数,还是需要思索一下的。我的办法是准备一个

ans

数组,

ans[i]

表示第i个点的最短路径的条数,好家伙!这样层层递推,就能推到第N个点了。

这样其实没啥问题,但是有可能统计第k个点,我统计了两条最短路径,突然来了一条更短的,这是我们的

ans[k]

从2又变回了1,好说!让k点的值等于最短路径上一个点的ans值就可以了。

所以核心代码如下。

if (dis[k.to]==dis[x]+k.w) ans[k.to]+=ans[x];
if (dis[k.to]>dis[x]+k.w)
{
    dis[k.to]=dis[x]+k.w;
    ans[k.to]=ans[x];
    q.push((node){dis[k.to],k.to});
}
           

就是这样如果有相等的路径就相加,有小于的路径就让其ans值回去。如果最后的

dis[n]=0x7fffffff

,那说明根本没路径能到达它,无解。

最后附上ac代码。

#include<bits/stdc++.h>
using namespace std;
const int maxn=2005;
const int maxm=maxn*(maxn-1)/2;
​
struct edge{
    int to,w;
};
​
struct node{
    int dis,num;
    bool operator < (const node &x)const{
        return x.dis<dis;
    }
};
​
vector <edge> e[maxm];
int n,m;
int dis[maxn];
int ans[maxn];
int cmp[maxn][maxn];
bool vis[maxn];
​
priority_queue <node> q;
​
void dijkstra()
{
    dis[1]=0;
    ans[1]=1;
    q.push((node){0,1});
    while (!q.empty())
    {
        node tmp=q.top();
        q.pop();
        int x=tmp.num;
        if (vis[x]) continue;
        vis[x]=1;
        for (edge k : e[x])
        {
            if (dis[k.to]==dis[x]+k.w) ans[k.to]+=ans[x];
            if (dis[k.to]>dis[x]+k.w)
            {
                dis[k.to]=dis[x]+k.w;
                ans[k.to]=ans[x];
                q.push((node){dis[k.to],k.to});
            }
        }
    }
}
​
int main()
{
    cin>>n>>m;
    int i,j;
    for (i=1;i<=n;i++) dis[i]=0x7fffffff;
    for (i=1;i<=m;i++)
    {
        int p,q,c,tmp;
        cin>>p>>q>>c;
        if (cmp[i][j]==0 || c<cmp[i][j]) cmp[i][j]=c;
    }
    for (i=1;i<=n;i++)
        for (j=1;j<=n;j++)
            if (cmp[i][j]) e[i].push_back((edge){j,cmp[i][j]});
    dijkstra();
    if (dis[n]!=0x7fffffff) cout<<dis[n]<<" "<<ans[n]<<endl;
    else cout<<"No answer"<<endl;
    return 0;
}
           

Floyd

著名的Floyd算法,算法的核心代码非常短,时间复杂度。采用邻接矩阵

arr[i][j]

的方式存图,三层循环不可调换。具体原理可以用dp来解释,在此不表。直接上核心代码。

for (int k=1;k<=N;k++)
    for (int i=1;i<=N;i++)
        for (int j=1;j<=N;j++)
            arr[i][j]=min(arr[i][j],arr[i][k]+arr[k][j]);
           

Floyd算法还需要看看练习题:P1119 灾后重建

也可以看看我关与此题的题解,见个人博客或知乎专栏

继续阅读