Bellman_ford 队列优化算法(又名 SPFA)
不能有负权回路。
思想:
- 很多边做的都是无用功(是默认的初始值 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 之判断负权回路
可以有负权回路。
思路:
- 对于一般的 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 之单源有限最短路
允许负权回路。
限制中间最多只能经过 k 个节点。
原理:
松弛一次:从源点出发,与源点一条边相连的节点的最短距离。
松弛两次:从源点出发,与源点两条边相连的节点的最短距离。
松弛 n - 1 次:将所有点到源点的最短距离都更新完毕。
思路:
- 中间最多只能经过 k 个节点,也就是求最多 k + 1 条边与源点相连的最短距离。那么松弛 k + 1 次 。
易错点:
- 更新 minDist 数组时,不能根据它当前轮更新时前面已更新的边。而要根据上一轮 minDist 数组的值来更新。(可以理解为被污染了。第一轮更新时就把与源点两条边相连的节点距离更新了。)需要设置一个 copyMinDist 来备份、保存上一轮的数组。
- 什么时候用 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 算法小总结
- Dijkstra 更像是贪心算法;Bellman_ford 更像是动态规划算法,所以能找负权值的和限制途经节点个数的。
- Dijkstra 和 Bellman_ford 的队列优化算法有点像。
- 前者是找最小距离的节点,后者是找相连并更新的节点。
- 前者没有用到队列,后者用到队列。
- 二者都是找到节点后就更新节点到源点的距离。
- 二者都要设置 minDist 数组,并把 minDist[start] = 0。

被折叠的 条评论
为什么被折叠?



