Graph | 单源最短路径:Bellman-Ford算法 与 Dijkstra算法

单源最短路径问题:在图G<V,E,W>中,计算起始点 s 到图内每个节点 v 的最短路径长度(边权重之和)。


1、问题概述

首先,最短路径具有 OSP 最优子结构性质:

最短路径中的子路径也是最短路径。

🔺有一个特殊点说明:如果图 G 内包含从 s 可以到达的权重为负值的环路(负环),则计算最短路径是无意义的。且不失一般性,我们可以认为最短路径上是没有环路的,即全是简单路径。


关于最短路径的记录,​​可以考虑对每个节点表示为一个结构体(当然,直接弄数组储存也是可的):

  • v.pre:源节点 s 到 v 的最短路径上 v 的前驱节点
  • v.dis:记录源节点 s 到 v 的最短路径值

🔺注:最短路径不一定是唯一的。

struct node {
    int dis;  //记录源节点 s 到 v 的最短路径值
    int pre;  //源节点 s 到 v 的最短路径上 v 的前驱节点的id
    int id;   //节点的编号:对应节点数组V的下标

    /* 重载 struct node 的比较运算符
     * 方便后面建立优先队列的大小定位 */
    friend bool operator < (struct node v1, struct node v2) {
        return v1.dis < v2.dis;
    }
} V[NUM_OF_VERTEX];   //定义一个节点集

图的单源最短路径问题有两类经典算法:

  1. Bellman-Ford 算法:允许图带有负权,且可以判断是否有负环的情况。
  2. Dijkstra 算法:只可处理所有边权均为非负值的图(此图中肯定不含负环喇)

2、松弛操作

三角不等式性质:对于任何边(u,v),均有 v.dis <= u.dis + w(u, v) 。

求最短路径的核心就是松弛操作,其原理就是基于三角不等式性质。松弛操作的对象是边(u,v),过程为:v.dis = min(v.dis, u.dis+w(u,v)),若更新了 v.dis 则同时更新 v 的前驱节点为 u。 

可以发现,松弛操作的效果就是尽量减小 v.dis 的值。松弛是唯一导致v.dis(最短路径估计)和 v.pre(前驱节点)发生改变的操作。

  • Dijkstra 算法 和 用于有向无环图的最短路径算法 对每条边仅松弛 1 次
  • Bellman-Ford 算法 则需要对每条边松弛 |V| -1 次

代码实现:

/* 初始化操作:
 * 传入点集 V,源点 s */
void Initial(struct node V[], struct node s) {
    for(int i = 0; i < NUM_OF_VERTEX; i++) {
        V[i].pre = -1;
        V[i].dis = INT_MAX;
        V[i].id = i;
    }
    s.dis = 0;
}
/* 对边(u,v)实行松弛操作
 * 且传入边的权重集合 w
 * 返回:true进行了更新;
 *      false没有进行更新 */
bool Relax(struct node u, struct node v, int w[][NUM_OF_VERTEX]) {
    if (v.dis > u.dis + w[u.id][v.id]) {
        v.pre = u.id;
        v.dis = u.dis + w[u.id][v.id];
        return true;
    }
    return false;
}

3、Bellman-Ford 算法

此算法既不是动态规划也不是贪心算法,就是简单笨拙的操作,但是它适用范围广呀~边权可以为负,且可以判断负环。

算法的思路:循环 |V| - 1次,每次将图中的每一条边进行松弛。如果还有边可以松弛,则说明图中有负环,否则计算完毕。


如何理解将每条边松弛 |V| - 1 次就可以获得最短值? 

性质:若松弛序列(边的松弛顺序序列)中有子列(顺序不连续地从序列中取出项组成)是沿着 s-v 的最短路,则该松弛序列执行完毕后,v.dis 必为最短值了。

(可以想象成直接沿着 s-v 的最短路一一松弛,那么结果肯定是最短值呀!只不过这里有可能是有间隔地,但是不影响效果)

Bellman-Ford 算法的完整松弛序列就是把边序列重复 |V| - 1 次,由于最短路径都是简单路径即 边数 <=  |V| - 1,那么这个序列的子列中必包含所有最短路径!


代码实现:

/* 计算从源节点s出发到 V中每一个节点的最短路径
 * 且传入边的权重集合 w
 * 返回值:true即不存在负环;
 *        false即存在负环,最短路径的计算无意义 */
bool BellmanFord(struct node V[], int w[][NUM_OF_VERTEX], struct node s) {
    Initial(V, s);   //初始化操作
    /* 循环 |V| - 1 次 */
    for (int t = 0; t < NUM_OF_VERTEX - 1; t++)   
        /* 对所有边进行松弛操作 */
        for (int i = 0; i < NUM_OF_VERTEX; i++)
            for (int j = 0; j < NUM_OF_VERTEX; j++) 
                if (w[i][j] != INT_MAX)   //节点i,j之间存在一条边
                    Relax(V[i], V[j], w);
    /* 检查是否还有负环 */
    for (int i = 0; i < NUM_OF_VERTEX; i++)
        for (int j = 0; j < NUM_OF_VERTEX; j++)
            if (w[i][j] != INT_MAX)   //节点i,j之间存在一条边
                if(V[j].dis > V[i].dis + w[i][j])   //如果还有边可以松弛
                    return false;
    return true;                
}

3、Dijkstra 算法

此算法是贪心算法的应用,执行如下贪心策略:

维持一个节点集合 S:源节点 s 到集合中的点的最短路径已经确定。重复从剩余节点集 V - S 中取出当前 dis 值最小的节点 u,将 u 加到集合 S 中,然后对从 u 出发的所有边进行松弛

可以证明:每次选择的节点 u 来加入集合 S 时,一定有 u.dis 已经是 s 到 u 的最短路径了。


 节点集 V - S 可以是最小堆(优先队列)或者一般数组,其效率取决于图的参数:

  • 稠密图:使用一般数组实现节点集 V - S,时间复杂度为 O(|V|^2)
  • 稀疏图:使用优先队列实现节点集 V - S,时间复杂度为O(|E|log|V|)

应用优先队列的代码实现:

/* 在无负权的图中:
 * 计算从源节点s出发到 V中每一个节点的最短路径
 * 且传入边的权重集合 w */
void Dijkstra(struct node V[], int w[][NUM_OF_VERTEX], struct node s) {
    Initial(V, s);
    priority_queue<struct node> Q;
    Q.push(s);   //初始仅加入源节点s

    bool vis[NUM_OF_VERTEX] = {false};   //记录节点是否被加入S集中
    while (!Q.empty()) {
        struct node cur = Q.top();  //取出 dis 值最小的节点
        Q.pop();

        if (vis[cur.id])
            continue;  //已经在S集中了,略过
        vis[cur.id] = true; //标记已经加入S集中

        for (int i = 0; i < NUM_OF_VERTEX; i++) {
            //如果 i 是 cur 的后继节点
            if (w[cur.id][i] != INT_MAX) {
                if (Relax(cur, V[i], w))  //松弛
                    Q.push(V[i]);  //加入队列
            }
        }
    }
}


完整代码:

//
// Created by A on 2020/4/27.
// 单源最短路径问题:在图 G<V,E,W>中,计算起始点 s 到图内每个节点 v 的最短路径长度(边权重之和)。

#include <climits>
#include <queue>

using namespace std;

#define NUM_OF_VERTEX 12

struct node {
    int dis;  //记录源节点 s 到 v 的最短路径值
    int pre;  //源节点 s 到 v 的最短路径上 v 的前驱节点的id
    int id;   //节点的编号:对应节点数组V的下标

    /* 重载 struct node 的比较运算符
     * 方便后面建立优先队列的大小定位 */
    friend bool operator<(struct node v1, struct node v2) {
        return v1.dis < v2.dis;
    }

} V[NUM_OF_VERTEX];   //定义一个节点集


/* 初始化操作:
 * 传入点集 V,源点 s */
void Initial(struct node V[], struct node s) {
    for (int i = 0; i < NUM_OF_VERTEX; i++) {
        V[i].pre = -1;
        V[i].dis = INT_MAX;
    }
    s.dis = 0;
}

/* 对边(u,v)实行松弛操作
 * 且传入边的权重集合 w
 * 返回:true进行了更新;
 *      false没有进行更新 */
bool Relax(struct node u, struct node v, int w[][NUM_OF_VERTEX]) {
    if (v.dis > u.dis + w[u.id][v.id]) {
        v.pre = u.id;
        v.dis = u.dis + w[u.id][v.id];
        return true;
    }
    return false;
}

/* 计算从源节点s出发到 V中每一个节点的最短路径
 * 且传入边的权重集合 w
 * 返回值:true即不存在负环;
 *        false即存在负环,最短路径的计算无意义 */
bool BellmanFord(struct node V[], int w[][NUM_OF_VERTEX], struct node s) {
    Initial(V, s);   //初始化操作
    /* 循环 |V| - 1 次 */
    for (int t = 0; t < NUM_OF_VERTEX - 1; t++)
        /* 对所有边进行松弛操作 */
        for (int i = 0; i < NUM_OF_VERTEX; i++)
            for (int j = 0; j < NUM_OF_VERTEX; j++)
                if (w[i][j] != INT_MAX)   //节点i,j之间存在一条边
                    Relax(V[i], V[j], w);
    /* 检查是否还有负环 */
    for (int i = 0; i < NUM_OF_VERTEX; i++)
        for (int j = 0; j < NUM_OF_VERTEX; j++)
            if (w[i][j] != INT_MAX)   //节点i,j之间存在一条边
                if (V[j].dis > V[i].dis + w[i][j])   //如果还有边可以松弛
                    return false;
    return true;
}

/* 在无负权的图中:
 * 计算从源节点s出发到 V中每一个节点的最短路径
 * 且传入边的权重集合 w */
void Dijkstra(struct node V[], int w[][NUM_OF_VERTEX], struct node s) {
    Initial(V, s);
    priority_queue<struct node> Q;
    Q.push(s);   //初始仅加入源节点s

    bool vis[NUM_OF_VERTEX] = {false};   //记录节点是否被加入S集中
    while (!Q.empty()) {
        struct node cur = Q.top();  //取出 dis 值最小的节点
        Q.pop();

        if (vis[cur.id])
            continue;  //已经在S集中了,略过
        vis[cur.id] = true; //标记已经加入S集中

        for (int i = 0; i < NUM_OF_VERTEX; i++) {
            //如果 i 是 cur 的后继节点
            if (w[cur.id][i] != INT_MAX) {
                if (Relax(cur, V[i], w))  //松弛
                    Q.push(V[i]);  //加入队列
            }
        }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值