算法刷题笔记 有边数限制的最短路(带有对贝尔曼福特算法的超级详细介绍,以及注释特别详细易懂的C++实现代码)

70 篇文章 3 订阅
63 篇文章 1 订阅

题目描述

  • 给定一个n个点m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
  • 请你求出从1号点到n号点的最多经过k条边的最短距离,如果无法从1号点走到n号点,输出 impossible
  • 注意:图中可能存在负权回路 。

输入格式

  • 第一行包含三个整数n,m,k
  • 接下来m行,每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z
  • 点的编号为1∼n

输出格式

  • 输出一个整数,表示从1号点到n号点的最多经过k条边的最短距离。
  • 如果不存在满足条件的路径,则输出impossible

数据范围

  • 1 ≤ n,k ≤ 500,
  • 1 ≤ m ≤ 10000,
  • 1 ≤ x,y ≤ n
  • 任意边长的绝对值不超过10000

基本思路

  • 算法中有向边的表示
    • 每一条有向边都包含三个要素,分别是起点a、终点b和边长(权重)w
    • 对于该算法,只需要使得每一轮外层迭代中都可以实现对图中所有有向边的遍历即可,因此在代码实现上,可以不使用邻接表来存储边,而是直接使用一个简单的有向边结构体数组。
  • 算法流程
    • 假设一个有向图中包含n个点和m条边。
    • 贝尔曼福特算法首先进行n次迭代,每一次迭代中又迭代该有向图中所有的边,对从起点到每一个终点的最短路径长度进行更新,更新公式为:最短路径长度(b) = min(最短路径长度(b), 最短路径长度(a) + ab边长)。更新最短路径路径长度的过程被称为贝尔曼福特算法中的“松弛操作”。
    • 需要注意的是,每一轮外层迭代中,不能使用当前一轮迭代中一些点更新后的最短路径长度来更新其他的点的最短路径长度,否则会发生“串联问题”。因此,需要每一轮迭代都使用上一轮迭代完成后的所有点的最短路径长度数组来更新当前一轮中所有点的最短路径长度数组,也就是每一次外层迭代完成后都需要对最短路径长度数组进行一次拷贝。在C++中,可以使用memcpy函数来将一个数组的内容复制到另外一个数组中,下面对该函数的语法进行介绍:
      • 使用该函数需要包含头文件<cstring>
      • 使用语法:memcpy(目标内存块指针,原内存块指针,要复制的字节数量)。目标内存块指针就是目标数组名,原内存块指针就是原数组名,可以用sizeof(原数组名)来获取要复制的字节数量。
  • 算法结果:贝尔曼福特算法证明了完成了所有迭代之后,对于有向图中的任意一条边ab,都满足:终点距离(b) <= 终点距离(a) + ab边长。该不等式被称为贝尔曼福特算法中的“三角不等式”。
  • 算法的适用情况:贝尔曼福特算法可以用于处理带有负权边(也就是边长为负数)的有向图的最短路径问题,而之前学过的Dijkstra算法则不能此类问题。
  • 负权回路
    • 对于存在负权回路的有向图,不一定存在从起点到终点的最短路径。
    • 贝尔曼福特算法可以用于求出有向图中是否存在负权回路,这是因为该算法的外层迭代的迭代次数有实际意义:进行完成k轮迭代后,最短路径长度数组中的值表示从起点开始,经过最多k条边,到达各个顶点的最短路径长度。
    • 因此,在第n轮迭代时如果对某些点的最短路径长度进行了更新,则说明存在一条从起点到这些点的边数为n的路径;由于边数为n的路径包含有n+1个点,但是图中只有n个点,所以根据抽屉原理,一定存在重复的点,也就是这条路径上一定存在环;又因为走环的距离更短才会导致更新,说明该环的权重一定是负数,也就是说该环是一个负权回路。
    • 需要指出的是,尽管贝尔曼福特算法可以找出有向图中存不存在负权回路,但是一般还是使用spfa算法来进行查找。
  • 算法的时间复杂度:贝尔曼福特算法由两层循环构成,外层循环的次数是有向图中点的个数n,内层循环的次数是有向图中边的条数m,因此总体的时间复杂度就是O(n * m)
  • 该算法与spfa算法对比spfa算法在各方面都要优于贝尔曼福特算法。
  • 算法实现的注意事项:在本算法中,不能直接将目前不可达的最短路径长度设置为无穷大。这是因为,如果设置为无穷大,那么该长度作为distances_backup数组的元素值时,加上任意一个正数都会发生溢出,变成一个非常非常小的负数,则此时最短路径长度也会更新为这个值,这是逻辑错误的。因此,为了防止溢出,本题中将除了1号点之外的各个点的最短路径长度初始化为INT_MAX的二分之一。

实现代码

#include <cstdio>
#include <climits>
#include <cstring>
#include <algorithm>
using namespace std;

// 【变量定义】输入的有向图中点的个数、边的条数和边数限制
int n, m, k;
// 【辅助结构体定义】用于表示有向边的结构体,存储的内容包括边的起点、终点和边长(权重)
struct directedEdge
{
    int start;
    int end;
    int weight;
};
// 【变量定义】每一次输入的有向边的起点、终点和边长
int x, y, z;
// 【辅助常量定义】有向图中边数的上限
const int N1 = 10010;
// 【变量定义】表示有向图中所有有向边的结构体数组
directedEdge edges[N1];
// 【变量定义】用于获取最短路径长度结果的函数
int result;
// 【辅助常量定义】有向图中点数的上限
const int N2 = 510;
// 【变量定义】记录起点(1号点)到每一个点的当前最短路径长度
int distances[N2];
// 【变量定义】记录起点(1号点)到每一个点的当前最短路径长度的备份
int distances_backup[N2];

// 【辅助函数定义】对最短路径长度数组进行初始化的函数
void init_distances(void)
{
    // 将1号点到自己的最短路径长度设置为0
    distances[1] = 0;
    // 将1号点到后续各个点的最短路径长度都设置为无穷大(除以2是为了防止后续加法溢出)
    for(int i = 2; i <= n; ++ i) distances[i] = INT_MAX >> 1;
}

// 【函数定义】基于贝尔曼福特算法的求1号点到n号点的最短路径长度的函数
int bellman_ford(void)
{
    // 【初始化】调用辅助函数对最短路径长度数组进行初始化
    init_distances();
    // 【算法主体】外层循环,遍历的次数为边数限制k
    for(int i = 0; i < k; ++ i)
    {
        // 每次进入内层循环前都需要记录上一轮外层循环结束时的最短路径长度的备份,防止出现串联问题
        memcpy(distances_backup, distances, sizeof(distances));
        // 内层循环,通过“松弛操作”来更新每个点的最短路径长度
        for(int j = 0; j < m; ++ j) 
        {
            // 从数组中取出一条有向边
            directedEdge edge = edges[j];
            // 使用松弛操作来更新最短路径长度
            distances[edge.end] = min(distances[edge.end], distances_backup[edge.start] + edge.weight);
        }
    }
    // 【返回结果】将最短距离数组中从1号点到n号点的最短路径长度返回
    return distances[n];
}

int main(void)
{
    // 【变量输入】分别输入有向图中点的个数、边的条数和边数限制
    scanf("%d%d%d", &n, &m, &k);
    // 【变量输入】输入有向图中的每一条边的信息
    for(int i = 0; i < m; ++ i) 
    {
        scanf("%d%d%d", &x, &y, &z);
        // 将有向边信息存入数组中
        edges[i] = {x, y, z};
    }
    // 【计算处理】通过自定义的函数获取最短路径长度
    result = bellman_ford();
    // 【结果判定和输出】如果存在从1号点到n号点的最短路径(返回值不是极限值),则直接输出
    if(result <= 5e6) printf("%d", result);
    // 如果不存在从1号点到n号点的最短路径,则输出"impossible"
    else printf("impossible");
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值