代码随想录算法训练营第 60 天 | Bellman_ford 队列优化算法(又名 SPFA)、Bellman_ford 之判断负权回路、Bellman_ford 之单源有限最短路

Bellman_ford 队列优化算法(又名 SPFA)

94. 城市间货物运输 I

不能有负权回路。

思想:

  • 很多边做的都是无用功(是默认的初始值 max)。要保证取出的边都是被前面的节点松弛过的。
  • 设置一个队列,先把节点 1 放进去,再把与它相连的节点放进队列(放进队列的条件:1. 松弛这些节点的值,比原来的距离要小。 2. 节点不在队列中),直到队列为空。

使用邻接表更合适。

小优化:已经在队列的的元素没必要再加到队列里了。(放进去也没事,但不放更能提高效率。)

时间复杂度:

  • 最坏情况 O(n^2)。满稠密图。每个节点都与其他节点双向连接。O(n * (n - 1)) = O(n^2)
  • 平均情况 O(n * k)。k 为每个节点的平均出度。

图越稠密,则 SPFA 的效率越接近与 Bellman_ford。
反之,图越稀疏,SPFA 的效率就越高。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int k = sc.nextInt();

        List<List<int[]>> graph = new ArrayList<>(); // 邻接表。int[] {to, value}

        for (int i = 0; i < n + 1; i++) {
            graph.add(new ArrayList<>());
        }

        for (int i = 0; i < k; i++) {
            int s = sc.nextInt();
            int t = sc.nextInt();
            int val = sc.nextInt();
            graph.get(s).add(new int[] {t, val});
        }

        int start = 1, end = n;
        int[] minDist = new int[n + 1];
        Arrays.fill(minDist, Integer.MAX_VALUE);
        minDist[start] = 0;

        Queue<Integer> queue = new ArrayDeque<>(); // 创建一个队列
        boolean[] inQueue = new boolean[n + 1]; // 判断节点在不在队列里

        queue.offer(start); // 把节点 1 放进队列
        inQueue[start] = true; // 标记节点 1 在队列中

        while (!queue.isEmpty()) {
            int cur = queue.poll(); // 取出队首节点
            inQueue[cur] = false; // 标记节点不在队列中

            for (int[] edge : graph.get(cur)) { // 遍历节点相连的节点
                int from = cur;
                int to = edge[0];
                int value = edge[1];
                if (minDist[from] + value < minDist[to]) { // 松弛
                    minDist[to] = minDist[from] + value;
                    if (!inQueue[to]) { // 节点不在队列中才添加到队列中
                        queue.offer(to);
                        inQueue[to] = true; // 标记节点 to 在队列中
                    }
                }
            }
        }

        if (minDist[end] != Integer.MAX_VALUE) {
            System.out.println(minDist[end]);
        } else {
            System.out.println("unconnected");
        }
    }
}

Bellman_ford 之判断负权回路

95. 城市间货物运输 II

可以有负权回路。

思路:

  • 对于一般的 Bellman_ford 算法:松弛 n 次,判断第 n 次松弛,minDist 是否发生改变。如果改变,说明有负权回路。
  • 对于队列优化的 Bellman_ford 算法:首先,一个节点的入度最多为 n - 1。队列只会把入度连接的节点添加到队列。所以每个节点最多加入 n-1 次队列。(还是在都松弛成功且队列无该节点的情况。)如果出现负权回路,那么队列永远不为空。那么肯定会有某个时刻,某个节点添加到队列的次数超过 n - 1,标记就可以了。

    感觉像是找到一个侧面的论点。

一般的 Bellman_ford 算法(推荐,因为一般判断负权回路的题不会卡数据量):

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int k = sc.nextInt();

        List<int[]> graph = new ArrayList<>(); // 采用朴素法,int[] 存 s、t、val

        for (int i = 0; i < k; i++) {
            int s = sc.nextInt();
            int t = sc.nextInt();
            int val = sc.nextInt();
            graph.add(new int[] {s, t, val});
        }

        int start = 1, end = n;
        int[] minDist = new int[n + 1];
        Arrays.fill(minDist, Integer.MAX_VALUE);
        minDist[start] = 0; // 必须初始化为 0

        boolean flag = false; // 是否出现负权回路

        for (int i = 1; i < n + 1; i++) { // 松弛 n 轮
            for (int[] edge : graph) {
                int from = edge[0];
                int to = edge[1];
                int val = edge[2];

                if (i < n) { // 前 n - 1 轮
                    if (minDist[from] != Integer.MAX_VALUE && minDist[from] + val < minDist[to]) {
                        minDist[to] = minDist[from] + val;
                    }
                } else { // 第 n 轮
                    if (minDist[from] != Integer.MAX_VALUE && minDist[from] + val < minDist[to]) {
                        flag = true;
                    }
                }
            }
        }

        if (flag) { // 如果出现负权回路,输出“circle”
            System.out.println("circle");
        } else if (minDist[end] != Integer.MAX_VALUE) {
            System.out.println(minDist[end]);
        } else {
            System.out.println("unconnected");
        }
    }
}

队列优化的 Bellman_ford 算法(不推荐,还没想明白为什么判断 count[to] == n 要在 !inQueue[to] 的条件):

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int k = sc.nextInt();

        List<List<int[]>> graph = new ArrayList<>(); // 邻接表。int[] {to, value}

        for (int i = 0; i < n + 1; i++) {
            graph.add(new ArrayList<>());
        }

        for (int i = 0; i < k; i++) {
            int s = sc.nextInt();
            int t = sc.nextInt();
            int val = sc.nextInt();
            graph.get(s).add(new int[] {t, val});
        }

        int start = 1, end = n;
        int[] minDist = new int[n + 1];
        Arrays.fill(minDist, Integer.MAX_VALUE);
        minDist[start] = 0;

        Queue<Integer> queue = new ArrayDeque<>();
        boolean[] inQueue = new boolean[n + 1];

        int[] count = new int[n + 1]; // 统计每个节点添加进队列的次数
        boolean flag = false; // 判断有没有出现负权回路

        queue.offer(start);
        inQueue[start] = true;
        count[start]++; // 节点 1 添加到队列的次数 + 1



        while (!queue.isEmpty()) {
            int cur = queue.poll();
            inQueue[cur] = false;

            for (int[] edge : graph.get(cur)) {
                int from = cur;
                int to = edge[0];
                int value = edge[1];
                if (minDist[from] + value < minDist[to]) {
                    minDist[to] = minDist[from] + value;
                    if (!inQueue[to]) {
                        queue.offer(to);
                        inQueue[to] = true; // 标记节点 to 在队列中
                        count[to]++; // 每次节点添加到队列后,就把统计节点的次数 + 1
                        if (count[to] == n) {
                            flag = true;
                            queue.clear(); // 为了跳出外层循环,把队列清空,这样就不满足外层循环条件了
                            break; // 跳出内层循环
                        }
                    }
                }
            }
        }

        if (flag) { // 如果出现负权回路,输出“circle”
            System.out.println("circle");
        } else if (minDist[end] != Integer.MAX_VALUE) {
            System.out.println(minDist[end]);
        } else {
            System.out.println("unconnected");
        }
    }
}

Bellman_ford 之单源有限最短路

96. 城市间货物运输 III

允许负权回路。

限制中间最多只能经过 k 个节点。

原理:

松弛一次:从源点出发,与源点一条边相连的节点的最短距离。
松弛两次:从源点出发,与源点两条边相连的节点的最短距离。
松弛 n - 1 次:将所有点到源点的最短距离都更新完毕。

思路:

  • 中间最多只能经过 k 个节点,也就是求最多 k + 1 条边与源点相连的最短距离。那么松弛 k + 1 次 。

易错点:

  1. 更新 minDist 数组时,不能根据它当前轮更新时前面已更新的边。而要根据上一轮 minDist 数组的值来更新。(可以理解为被污染了。第一轮更新时就把与源点两条边相连的节点距离更新了。)需要设置一个 copyMinDist 来备份、保存上一轮的数组。
  2. 什么时候用 copyMinDist 数组,什么时候用 minDist 数组。
if (copyMinDist[from] != Integer.MAX_VALUE && copyMinDist[from] + val < minDist[to]) { // 注意!用的是 minDist[to]
    minDist[to] = copyMinDist[from] + val; // 修改改的是本轮,用的数据是上一轮
}

代码:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt(); // 共 m 条边

        List<int[]> graph = new ArrayList<>();

        for (int i = 0; i < m; i++) {
            int s = sc.nextInt();
            int t = sc.nextInt();
            int val = sc.nextInt();
            graph.add(new int[] {s, t, val});
        }

        int start = sc.nextInt(); // 起点城市
        int end = sc.nextInt(); // 终点城市
        int k = sc.nextInt(); // 经过的城市数量限制

        int[] minDist = new int[n + 1]; // 距节点 start 的最短距离
        Arrays.fill(minDist, Integer.MAX_VALUE);
        minDist[start] = 0; // start 到 start 距离为 0

        int[] copyMinDist = new int[n + 1]; // 用于备份、保存上一轮的 minDist 数组

        for (int i = 1; i <= k + 1; i++) { // 松弛 k + 1 轮
            copyMinDist = minDist.clone(); // 备份一份作为上一轮的 minDist 数组
            for (int[] edge : graph) {
                int from = edge[0];
                int to = edge[1];
                int val = edge[2];
                if (copyMinDist[from] != Integer.MAX_VALUE && copyMinDist[from] + val < minDist[to]) { // 注意!用的是 minDist[to]
                    minDist[to] = copyMinDist[from] + val; // 修改改的是本轮,用的数据是上一轮
                }
            }
        }

        if (minDist[end] != Integer.MAX_VALUE) {
            System.out.println(minDist[end]);
        } else {
            System.out.println("unreachable"); // k + 1 条边不能连接到,输出 unreachable
        }
    }
}

Dijkstra 算法和 Bellman_ford 算法小总结

  1. Dijkstra 更像是贪心算法;Bellman_ford 更像是动态规划算法,所以能找负权值的和限制途经节点个数的。
  2. Dijkstra 和 Bellman_ford 的队列优化算法有点像。
    • 前者是找最小距离的节点,后者是找相连并更新的节点。
    • 前者没有用到队列,后者用到队列。
    • 二者都是找到节点后就更新节点到源点的距离。
    • 二者都要设置 minDist 数组,并把 minDist[start] = 0。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值