AcWing 853. 有边数限制的最短路(Bellman_ford算法)

题目链接点击查看

题目描述

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。

注意:图中可能 存在负权回路 。

输入输出格式

输入

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

输出

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

输入输出样例

输入

3 3 1
1 2 1
2 3 1
1 3 3

输出

3

题目分析

Bellman_ford算法常用于解决存在边权为负的单源最短路问题,特别是有边数限制的单源最短路问题。Bellman_ford算法与Dijkstra算法有异有同,同是每一次外层循环都能找到一个点到点1的最短路,异是Dijkstra算法知道这个点是哪个,而Bellman_ford算法却不知哪个点,且Dijkstra算法不能处理图中有负权边的情况,而Bellman_ford算法却可以,另外Bellman_ford算法还可以检测图中是否有负权回路。

因此,Bellman_ford算法思想为:外层进行k次for循环,每一次外层循环内部同样用for循环将所有边进行一次松弛操作,最后得到的是从点1(起始点)走k条边的最短路径。具体实现为 : 首先,我们用一个结构体struct Edge{int v1, v2, w;}来表明图中边的情况,赋值时具体要创建一个结构体数组edges[M]来储存每一条边的情况。我们还需要距离数组dist[i]储存点 i 到点 1 的最短距离,backup[N]备份数组用来记录每一次循环刚开始时dist数组的状态。调用bellman_ford()函数,在函数内部将dist数组初始化为无穷大(0x3f3f3f3f),dist[1] 赋值为 0。然后开始我们的外层循环 for(int i = 0; i < k ; i ++ ) 这里k为输入的限制边数,每一次外层循环得到的都为走 i + 1 条边所到达点的最短路径。在外层循环内部,我们同样用一个for循环 for(int j = 1; j <= m; j ++ ) 且在其内部进行dist[edge[j].v2] = min(dist[edge[j].v2],dist[ backup[j]. v1 ]+ edge[j].w)边松弛操作,这样内部的for循环结束后,假设外部i = 0则可得到源点经过最多1条边到的点的最短路,注意每一轮松弛只得到源点最多经过i + 1条边的最短路,控制得到的最短路的范围是经过最多i + 1条边,这也是我们要用backup[i]备份数组用来记录循环初始时dist数组状态的原因,如不用backup进行松弛操作中的替换,会发生串联作用,如下图

在这里插入图片描述
首先假设k == 1,然后在内部for循环中进行松弛操作,第一步我们更新从节点1到节点2的距离为2,即此时dist[2] = 1,然后我们更新从1到3的距离为4,此时dist[3] = 4,但有三条边嘛,我们循环并没有结束,我们会用dist[2]对dist[3]进行松弛,若是我们没有用dist数组的初始状态,由于2 + 1 = 3 < 4 所以dist[3] 被替换成 3 。但别忘了,k == 1,即要求的是最多走1条边到达节点n的最短距离(按理说应该是4),但是在此走了两条边得到了3,因此需要用一个备份数组backup来控制走的距离,防止发生串联作用。

在所有循环都结束的时候,我们用需要判断走k步能否到达最终节点n,在此判断条件为if(dist[n] > 0x3f3f3f3f / 2),为啥不是直接if(dist[n] == 0x3f3f3f3f),由于图中可能存在负权回路的情况,在松弛操作的时候,可能本来走不到的边,从0x3f3f3f3f被替换成0x3f3f3f3f - 2(或其他某个负数),这个最终可能也走不到,但表示无穷大的值,已经发生改变了,为了防止这种情况发生,我们将条件写为if(dist[n] > 0x3f3f3f3f / 2),若走k步能走到n点,则直接输出dist[n]即可。详见如下代码。

代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edge {
	int v1, v2, w;
}edges[M];
int n, m, k;
int dist[N], backup[N];//backup数组用于对上一次遍历数组备份、dist[i]表示从1节点到节点i的最短距离 
void bellman_ford() {//backup作用是防止更新数组串联 
	memset(dist, 0x3f, sizeof(dist));
	dist[1] = 0;
	for (int i = 0; i < k; i ++ ) {
		memcpy(backup, dist, sizeof(dist));
		for (int j = 1; j <= m; j ++ ) {
			auto e = edges[j];
			dist[e.v2] = min(dist[e.v2], backup[e.v1] + e.w);
		} 
	}
}
int main() {
	cin >> n >> m >> k;
	for (int i = 1; i <= m; i ++ ) {
		int v1, v2, w;
		cin >> v1 >> v2 >> w;
		edges[i] = {v1, v2, w};
	}
	bellman_ford();
	if (dist[n] > 0x3f3f3f3f / 2) cout << "impossible" << endl; 
	else cout << dist[n];
	return 0;
} 

拓展 : Bellma_ford算法也能判断图中是否有负环(即负权回路)的情况,我们将k == m + 1表示得到最多走m + 1条边得到的最短路,由于图中一共有m条边,如图中不存在负环,在第m次遍历时所有点都已经找到,则在进行m + 1遍历时,不会发生任何变化。但存在负环的情况,每一次遍历都会都会使得最短路减小,此时,由抽屉原理可知一定存在一个节点dist[v2] > dist[v1] + w,若如此,则存在负环。其实也可以这样做,在k == m 的循环结束后,我们再次遍历图中所有节点,若存在dist[v2] > dist[v1] + w,则存在负环。


下面给出bellman-ford算法的相关模板。

时间复杂度 O(nm), n 表示点数,m 表示边数
nt n, m;       // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge     // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if (dist[b] > dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

在森林中麋了鹿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值