Dijkstra--解决非负权边的单源最短路算法

19 篇文章 0 订阅
17 篇文章 0 订阅

前言

这是之前写过的一篇 D i j k s t r a Dijkstra Dijkstra算法博客的重置版,最近正好在复习,发现了一些事实错误(我菜爆了),再加上当时用的是富文本编辑器写的,整体排版不太好看,于是干脆推到重写,以下是原文前言部分:

最近一直忙,没有时间写算法博客(其实是去水数据结构去了),今天睡前写一篇图论的基础算法吧,叫 D i j k s t r a Dijkstra Dijkstra,是由荷兰计算机科学家 E . W . D i j k s t r a E.W.Dijkstra E.W.Dijkstra于1959年提出的,因此叫 D i j k s t r a Dijkstra Dijkstra算法.是求从一个源点到其余各顶点的最短路径算法,即单源最短路算法,解决的是图论中最短路径问题.

原理

对于一个有 m m m条边, n n n个顶点的图,我们先定义如下描述,以表示一个图的某些关系:

源点 s t st st,表示算法所求最短路的出发顶点;

点集 V V V,表示图中所有点构成的集合,其中 ∣ V ∣ = n |V| = n V=n;

边集 E E E,表示途中所有边构成的集合,其中 ∣ E ∣ = m |E| = m E=m;

数组 d i s [ ] dis[] dis[],表示源点 s t st st到其余点的最短路径,数组大小为 n n n;

三元组 &lt; u , v , w &gt; &lt;u,v,w&gt; <u,v,w>,表示有一条从 u u u v v v,权值为 w w w的边,即 u → v = w u \to v = w uv=w;

我们选择一个源点 s t st st,求它到其余所有点的最短路径,算法描述如下:

  1. 初始化: 将源点到的所有顶点的最短距离设为 + ∞ +\infin +,到自身为 0 0 0: ∀ i ∈ V , d i s [ i ] → + ∞ , d i s [ s t ] = 0 \forall i \in V, dis[i] \to + \infin, dis[st] = 0 iV,dis[i]+,dis[st]=0.
  2. 找点松弛边: 寻找一个离源点 s t st st最近的且未使用过的点 i i i,以该点为中间点松弛源点 s t st st到其他点的最短路径,松弛操作描述如下: ∀ &lt; i , v , w &gt; ∈ E , d i s [ v ] = m i n ( d i s [ v ] , d i s [ i ] + w ) \forall &lt;i,v,w&gt; \in E,dis[v] = min(dis[v],dis[i] + w) <i,v,w>E,dis[v]=min(dis[v],dis[i]+w),标记 i i i为用过的点.
  3. 重复2,直到所有点都用过(松弛进行了 n − 1 n-1 n1次).

在算法描述里提到了松弛这个概念,那么,什么是松弛呢?

所谓松弛,即寻找一个顶点 t t t,使得从 s t → e st \to e ste的最短路长度能通过 t t t中转变成 s t → t → e st \to t \to e stte,且长度缩短,那么我们就说 s t → e st \to e ste的最短路通过t这个点松弛成功.比如 1 → 2 1 \to 2 12有长为 9 9 9的路径, 1 → 3 1 \to 3 13有长为 1 1 1的路径, 3 → 2 3 \to 2 32有长为 3 3 3的路径,则通过 3 3 3松弛 1 → 2 1 \to 2 12,使得 1 → 3 → 2 1 \to 3 \to 2 132长度为 4 4 4,即此时 1 → 2 1 \to 2 12有两条路径,最短路是4.

松弛也叫作三角形优化,也许有人有疑问了,三角形两边之和大于第三边啊,为什么通过两边之和反而使得最短路比直接相连的边更短呢?因为三角形是两点之间均是直线,而路径可没有说是直线路径啊,考虑到此优化过程和数学的三角形性质类似,才称为三角形优化,准确地讲,这里的三角形是曲边三角形.

演示

下图模拟了 D i j k s t r a Dijkstra Dijkstra算法的执行流程( 0 0 0为源点,注意这是一个无向图):

两个思考题

思考这样一个问题:为什么离源点 s t st st最近( d i s [ i ] dis[i] dis[i]最小)的点 i i i,源点距其最短路确定呢?

因为如果 s t st st i i i最短路未确定,则一定能通过一个中间点,松弛 s t st st i i i的边,即 ∃ &lt; j , v , w &gt; ∈ E \exist &lt;j,v,w&gt; \in E <j,v,w>E,使得以下等式: d i s [ v ] &lt; d i s [ i ] + w dis[v] \lt dis[i] + w dis[v]<dis[i]+w成立,那么该点一定不可能是离源点最近的点!因此离源点最近的点,源点距其最短路确定.

再思考这一个问题:那么,一共要松弛多少次呢?

一共是 n n n次, n n n为图中顶点数量,因为每松弛一次,就会产生一个新的“中间点”,通过这个点进行下一次松弛操作,包括源点一共有 n n n个“中间点”,因此松弛 n n n次就好了.

负权边?

我们讨论一下边权全为正的情况:当边权全为正时,不可能存在一个点,使得已经松弛过的点最短路变得更短,这个矛盾前面刚讲过,所以松弛过的这些点最短路永远不再改变,因此保证了算法的正确性.

但是, D i j k s t r a Dijkstra Dijkstra不能解决含有负权边的图!为什么?因为当扩展到负权边的时候,源点 s t st st到某些已经使用过的点的最短路可能会发生改变,这就破坏了已经松弛过的点最短路不变的性质,因此负权边不是 D i j k s t r a Dijkstra Dijkstra可以解决的问题.

实现

说了这么多,怎么写 D i j k s t r a Dijkstra Dijkstra算法呢? --我们介绍邻接矩阵和邻接表的形式.

若是对于邻接矩阵和邻接表不太熟悉,可以去看看我写的邻接表和邻接矩阵.

  • 邻接矩阵写 D i j k s t r a Dijkstra Dijkstra:
#include<iostream>
#include<cstring>
#include<algorithm>
#define maxn 10000
#define inf 0x3f3f3f3f
using namespace std;

bool vis[maxn];                  //标记一个点是否已经达到最短路/是否已经使用过;
int e[maxn][maxn],dis[maxn];    //dis[maxn]表示从源点到其它点的距离;

void init(int n) {
    for(int i = 0; i <= n; i++) {
        for(int j = 0; j <= n; j++) {
            if(i == j)
                e[i][j] = 0;
            else
                e[i][j] = inf;
        }
    }
}

void Dijkstra(int n,int st) {      //st为源点;
    for(int i = 0; i <= n; i++) {
        vis[i] = 0;
        dis[i] = inf;
    }
    dis[st] = 0;
    int cur,minn;
    for(int k = 0; k < n; k++) {  //最多松弛n次;
        minn = inf;
        for(int i = 0; i <= n; i++) {   //找到离源点最近的且未被使用过的点;
            if(minn > dis[i] && !vis[i]) {
                minn = dis[i];
                cur = i;
            }
        }
        vis[cur] = 1;               //该点最短路已经确定了;
        for(int i = 0; i <= n; i++) {
            dis[i] = min(dis[i],dis[cur]+e[cur][i]);    //通过该点松弛其它所有点;
        }
    }
}

int main() {
    int n,m,st,u,v,w;
    while(cin>>n>>m>>st) {
        init(n);
        for(int i = 0; i < m; i++) {
            cin>>u>>v>>w;
            e[u][v] = w;
//            e[v][u] = w;  //无向图;
        }
        Dijkstra(n,st);
        for(int i = 0; i <= n; i++) //输出最短路;
            cout<<dis[i]<<" ";
        cout<<endl;
    }
    return 0;
}
  • 邻接表写 D i j k s t r a Dijkstra Dijkstra:
#include<iostream>
#include<cstring>
#include<algorithm>
#define maxn 100010
#define maxm 10000010
#define inf 0x3f3f3f3f
using namespace std;

struct node {
    int v,w,nxt;
}e[maxm];

bool vis[maxn];
int head[maxn],dis[maxn],tot = 0;

void init() {
    tot = 0;
    memset(head,-1,sizeof head);
}

void addedge(int u,int v,int w) {
    e[tot].v = v;
    e[tot].w = w;
    e[tot].nxt = head[u];
    head[u] = tot++;
}

void Dijkstra(int n,int st) {
    for(int i = 0; i <= n; i++) {
        vis[i] = 0;
        dis[i] = inf;
    }
    dis[st] = 0;
    int cur,minn;
    for(int k = 0; k < n; k++) {  //松弛n次即可;
        minn = inf;
        for(int i = 0; i <= n; i++) {      //找到离源点最近的且未被使用过的点;
            if(minn > dis[i] && !vis[i]) {
                minn = dis[i];
                cur = i;
            }
        }
        vis[cur] = 1;                       //该点最短路已经确定;
        for(int i = head[cur]; i != -1; i = e[i].nxt) { //通过该点松弛其它点;
            dis[e[i].v] = min(dis[e[i].v],dis[cur]+e[i].w);
        }
    }
}

int main() {
    int n,m,st,u,v,w;
    while(cin>>n>>m>>st) {
        init();
        for(int i = 0; i < m; i++) {
            cin>>u>>v>>w;
            addedge(u,v,w);
//            addedge(v,u,w);   //无向图;
        }
        Dijkstra(n,st);
        for(int i = 0; i <= n; i++) //输出最短路;
            cout<<dis[i]<<" ";
        cout<<endl;
    }
    return 0;
}
  • 或者用vector写邻接表版本 D i j k s t r a Dijkstra Dijkstra:
#include<iostream>
#include<vector>
#include<algorithm>
#define maxn 100010
#define inf 0x3f3f3f3f
using namespace std;
typedef pair<int,int> pii;

int dis[maxn];
bool vis[maxn];
vector<pii> e[maxn];    //first代表v,second代表w;

void init(int n) {
    for(int i = 0; i <= n; i++)
        e[i].clear();
}

void Dijkstra(int n,int st) {
    for(int i = 0; i <= n; i++) {
        vis[i] = 0;
        dis[i] = inf;
    }
    dis[st] = 0;
    int cur,minn,len;
    for(int k = 0; k < n; k++) {  //松弛n次即可;
        minn = inf;
        for(int i = 0; i <= n; i++) {      //找到离源点最近的且未被使用过的点;
            if(minn > dis[i] && !vis[i]) {
                minn = dis[i];
                cur = i;
            }
        }
        vis[cur] = 1,len = e[cur].size();
        for(int i = 0; i < len; i++) { //通过该点松弛其它点;
            dis[e[cur][i].first] = min(dis[e[cur][i].first],dis[cur]+e[cur][i].second);
        }
    }
}

int main() {
    int n,m,st,u,v,w;
    while(cin>>n>>m>>st) {
        init(n);
        for(int i = 0; i < m; i++) {
            cin>>u>>v>>w;
            e[u].push_back(make_pair(v,w));
//            e[v].push_back(make_pair(u,w));   //无向图;
        }
        Dijkstra(n,st);
        for(int i = 0; i <= n; i++) //输出最短路;
            cout<<dis[i]<<" ";
        cout<<endl;
    }
    return 0;
}

算法优化

还能更好吗?–当然可以!

我们考虑算法的时间复杂度, D i j k s t r a Dijkstra Dijkstra用邻接矩阵存的图,时间复杂度应该是 O ( V 2 ) O(V^2) O(V2)级别的,因为每次需要找点并松弛其余点,这是 O ( V ) O(V) O(V),外层循环套松弛轮数 O ( V ) O(V) O(V),因此综合时间复杂度为 O ( V 2 ) O(V^2) O(V2),而我们用邻接表存图,更新最短距离只需要遍历找到的那个点所有的边即可,但每次还是要枚举所有顶点来找离源点最近且未使用的点以供下次松弛其余点,时间复杂度是 O ( V ) O(V) O(V),因此最后还是 O ( V 2 ) O(V^2) O(V2)的复杂度,那我们考虑缩短找点的时间—-优先队列!

优先队列是一种按优先级自动排序的数据结构,和队列相似,每次均从队首取元素,不同的是取的元素是按照一定优先级最高的元素,堆内部排序算法可以了解一下,时间复杂度为 l o g ( n ) log(n) log(n),我们可以设定一个小根堆,即让小的元素优先级高,这样每次取出元素总是最小的,在之前的步骤中将符合条件的点放入堆中,取出的就是最小的符合条件的顶点,这样取最小值操作的复杂度就变成了 O ( 1 ) O(1) O(1),加上之前的堆排序 O ( l o g E ) O(log E) O(logE),和边更新的最多是 O ( E ) O(E) O(E),所以,总的时间复杂度为 O ( E l o g E ) O(Elog E) O(ElogE),这样的优化对于稀疏图来说,极为有效,注意以上说的 E E E为边数, V V V为顶点数.

于是我们可以写一个,以STL实现的优先队列:

#include<iostream>
#include<cstring>
#include<queue>
#define maxn 100010
#define maxm 10000010
#define inf 0x3f3f3f3f
using namespace std;

typedef pair<int,int> pii;

struct node {
    int v,w,nxt;
}e[maxm];

bool vis[maxn];
int head[maxn],dis[maxn],tot = 0;

void init() {
    tot = 0;
    memset(head,-1,sizeof head);
}

struct cmp {          //比较函数,重载()运算符;
    bool operator()(pii a,pii b) {
        return a.second > b.second;
    }
};

void addedge(int u,int v,int w) {
    e[tot].v = v;
    e[tot].w = w;
    e[tot].nxt = head[u];
    head[u] = tot++;
}

void Dijkstra(int n,int st) {
    for(int i = 0; i <= n; i++) {
        vis[i] = 0;
        dis[i] = inf;
    }
    dis[st] = 0;
    int cur;
    priority_queue<pii,vector<pii>,cmp> q;    //优先队列;
    q.push(make_pair(st,dis[st]));
    while(!q.empty()) {
        cur = q.top().first;
        q.pop();
        if(vis[cur])
            continue;    //每个点只能入队一次;
        vis[cur] = 1;          //队首的元素一定是最小的;
        for(int i = head[cur]; i != -1; i = e[i].nxt) {
            if(dis[e[i].v] > dis[cur] + e[i].w) {
                dis[e[i].v] = dis[cur] + e[i].w;
                q.push(make_pair(e[i].v,dis[e[i].v]));
            }
        }
    }
}

int main() {
    int n,m,st,u,v,w;
    while(cin>>n>>m>>st) {
        init();
        for(int i = 0; i < m; i++) {
            cin>>u>>v>>w;
            addedge(u,v,w);
//            addedge(v,u,w);   //无向图;
        }
        Dijkstra(n,st);
        for(int i = 0; i <= n; i++) //输出最短路;
            cout<<dis[i]<<" ";
        cout<<endl;
    }
    return 0;
}

或者用vector,写一个:

#include<iostream>
#include<cstring>
#include<queue>
#define maxn 100010
#define inf 0x3f3f3f3f
using namespace std;

typedef pair<int,int> pii;

vector<pii> e[maxn];    //first代表v, second代表w;
int dis[maxn],vis[maxn];

void init(int n) {
    for(int i = 0; i <= n; i++)
        e[i].clear();
}

struct cmp {
    bool operator()(pii a,pii b) {
        return a.second > b.second;
    }
};

void Dijkstra(int n,int st) {
    for(int i = 0; i <= n; i++) {
        vis[i] = 0;
        dis[i] = inf;
    }
    dis[st] = 0;
    int cur,len;
    priority_queue<pii,vector<pii>,cmp> q;    //优先队列;
    q.push(make_pair(st,dis[st]));
    while(!q.empty()) {
        cur = q.top().first;
        q.pop();
        if(vis[cur])
            continue;    //每个点只能入队一次;
        vis[cur] = 1,len = e[cur].size();           //队首元素一定是最小的;
        for(int i = 0; i < len; i++) {
            if(dis[e[cur][i].first] > dis[cur]+e[cur][i].second) {
                dis[e[cur][i].first] = dis[cur]+e[cur][i].second;
                q.push(make_pair(e[cur][i].first,dis[e[cur][i].first]));
            }
        }
    }
}

int main() {
    int n,m,st,u,v,w;
    while(cin>>n>>m>>st) {
        init(n);
        for(int i = 0; i < m; i++) {
            cin>>u>>v>>w;
            e[u].push_back(make_pair(v,w));
//            e[v].push_back(make_pair(u,w));   //无向图;
        }
        Dijkstra(n,st);
        for(int i = 0; i <= n; i++) //输出最短路;
            cout<<dis[i]<<" ";
        cout<<endl;
    }
    return 0;
}

以上代码如果有阅读障碍可以去看看我写的C++ priority_queue的自定义比较方式,优先队列主要是掌握这个就好用了,那么接下来建议做一个非常经典的例题–HDU 1874,来实践一下, D i j k s t r a Dijkstra Dijkstra的应用.

堆优化的 D i j k s t r a Dijkstra Dijkstra在时间复杂度上有很大的优势,而且以上两种邻接表在内存占用上也相差无几,喜欢哪种用哪种,可以说是非常实用的算法了,但是千万注意,存在负权边的图,不能用 D i j k s t r a Dijkstra Dijkstra!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值