Bellman-Ford 队列优化算法(SPFA)精讲
题目描述
某国共有 n 个城市,通过 m 条单向道路连接。每条道路的权值为运输成本减去政府补贴。要求找出从城市 1 到城市 n 的最低运输成本路径,若成本为负则表示盈利,若无路径则输出 “unconnected”。
输入包含 n 和 m,接着 m 行每行三个整数 s、t、v,表示从 s 到 t 的道路权值为 v。输出为最低成本或 “unconnected”。
输入输出示例
输入:
6 7
5 6 -2
1 2 1
5 3 1
2 5 2
2 4 -3
4 6 4
1 3 5
输出:
-4
解题思路
算法原理
SPFA(Shortest Path Faster Algorithm)是 Bellman-Ford 算法的队列优化版本,用于求解单源最短路径问题,尤其适用于存在负权边的图。其核心思想是利用队列来存储需要松弛的节点,减少不必要的松弛操作,提高算法效率。
关键优化点
- 队列存储:使用队列来存储需要进行松弛操作的节点,每次取出队首节点,松弛其所有出边,并将受影响的节点加入队列。
- 避免重复入队:通过布尔数组记录节点是否在队列中,避免同一节点重复入队,减少冗余操作。
代码实现
import java.util.*;
public class Main {
// 定义边的类,包含起点、终点和权值
static class Edge {
int from;
int to;
int val;
public Edge(int from, int to, int val) {
this.from = from;
this.to = to;
this.val = val;
}
}
public static void main(String[] args) {
// 输入处理
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 城市数量
int m = sc.nextInt(); // 道路数量
List<List<Edge>> graph = new ArrayList<>();
// 初始化邻接表
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
// 读取每条道路的信息并构建邻接表
for (int i = 0; i < m; i++) {
int from = sc.nextInt();
int to = sc.nextInt();
int val = sc.nextInt();
graph.get(from).add(new Edge(from, to, val));
}
// 初始化最短距离数组,minDist[1] = 0,其余为最大值
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
minDist[1] = 0;
// 队列用于存储需要松弛的节点
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
// 记录节点是否在队列中,避免重复入队
boolean[] isInQueue = new boolean[n + 1];
isInQueue[1] = true;
// 开始松弛过程
while (!queue.isEmpty()) {
int curNode = queue.poll();
isInQueue[curNode] = false; // 标记当前节点已出队
// 遍历当前节点的所有出边
for (Edge edge : graph.get(curNode)) {
// 如果可以通过当前边更新终点的最短距离
if (minDist[edge.to] > minDist[edge.from] + edge.val) {
minDist[edge.to] = minDist[edge.from] + edge.val;
// 如果终点不在队列中,则加入队列
if (!isInQueue[edge.to]) {
queue.offer(edge.to);
isInQueue[edge.to] = true;
}
}
}
}
// 输出结果
if (minDist[n] == Integer.MAX_VALUE) {
System.out.println("unconnected"); // 无法到达终点
} else {
System.out.println(minDist[n]); // 输出最短距离
}
}
}
代码注释
- Edge类:用于表示图中的边,包含起点、终点和权值。
- 输入处理:读取城市数量和道路数量,构建邻接表存储图结构。
- 初始化:创建 minDist 数组存储源点到各节点的最短距离,初始时所有距离设为最大值,源点到自身的距离设为0。
- 队列与标记数组:使用队列存储需要松弛的节点,布尔数组 isInQueue 用于记录节点是否在队列中,避免重复入队。
- 松弛过程:从队列中取出节点,遍历其所有出边,尝试更新终点的最短距离。若更新成功且终点不在队列中,则将终点加入队列。
- 结果输出:检查终点的最短距离是否仍为最大值,若是则说明无法到达,否则输出该距离。
总结
SPFA 算法通过队列优化了 Bellman-Ford 算法的松弛过程,减少了不必要的松弛操作,提高了算法效率。其时间复杂度在最坏情况下为 O(n * m),但一般情况下表现更优,尤其在稀疏图中效果显著。理解队列的使用和松弛操作的优化是掌握该算法的关键。
Bellman-Ford 算法之判断负权回路
题目描述
某国共有 n 个城市,通过 m 条单向道路连接。每条道路的权值为运输成本减去政府补贴。要求找出从城市 1 到城市 n 的最低运输成本路径,同时检测是否存在负权回路。若存在负权回路,输出 “circle”;若无路径,输出 “unconnected”;否则输出最低成本。
输入输出示例
输入:
4 4
1 2 -1
2 3 1
3 1 -1
3 4 1
输出:
circle
解题思路
算法原理
Bellman-Ford 算法可以通过松弛操作检测负权回路。在标准的 Bellman-Ford 算法中,对所有边进行 n-1 次松弛后,如果还能继续松弛,则说明图中存在负权回路。
SPFA 优化
使用 SPFA(队列优化的 Bellman-Ford)算法,通过记录每个节点入队次数来判断是否存在负权回路。如果某个节点入队次数超过 n-1 次,则说明存在负权回路。
代码实现
import java.util.*;
public class Main {
// 定义边的类,包含起点、终点和权值
static class Edge {
int from;
int to;
int val;
public Edge(int from, int to, int val) {
this.from = from;
this.to = to;
this.val = val;
}
}
public static void main(String[] args) {
// 输入处理
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 城市数量
int m = sc.nextInt(); // 道路数量
List<List<Edge>> graph = new ArrayList<>();
// 初始化邻接表
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
// 读取每条道路的信息并构建邻接表
for (int i = 0; i < m; i++) {
int from = sc.nextInt();
int to = sc.nextInt();
int val = sc.nextInt();
graph.get(from).add(new Edge(from, to, val));
}
// 初始化最短距离数组,minDist[1] = 0,其余为最大值
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
minDist[1] = 0;
// 队列用于存储需要松弛的节点
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
// 记录每个节点入队次数
int[] count = new int[n + 1];
count[1] = 1;
// 记录节点是否在队列中,避免重复入队
boolean[] isInQueue = new boolean[n + 1];
isInQueue[1] = true;
// 标记是否存在负权回路
boolean hasNegativeCycle = false;
// 开始松弛过程
while (!queue.isEmpty()) {
int curNode = queue.poll();
isInQueue[curNode] = false;
// 遍历当前节点的所有出边
for (Edge edge : graph.get(curNode)) {
// 如果可以通过当前边更新终点的最短距离
if (minDist[edge.to] > minDist[edge.from] + edge.val) {
minDist[edge.to] = minDist[edge.from] + edge.val;
// 如果终点不在队列中,则加入队列
if (!isInQueue[edge.to]) {
queue.offer(edge.to);
count[edge.to]++;
isInQueue[edge.to] = true;
// 如果某个节点入队次数超过 n 次,说明存在负权回路
if (count[edge.to] == n + 1) {
hasNegativeCycle = true;
queue.clear(); // 清空队列提前终止循环
break;
}
}
}
}
}
// 输出结果
if (hasNegativeCycle) {
System.out.println("circle"); // 存在负权回路
} else if (minDist[n] == Integer.MAX_VALUE) {
System.out.println("unconnected"); // 无法到达终点
} else {
System.out.println(minDist[n]); // 输出最短距离
}
}
}
代码注释
- Edge类:用于表示图中的边,包含起点、终点和权值。
- 输入处理:读取城市数量和道路数量,构建邻接表存储图结构。
- 初始化:创建 minDist 数组存储源点到各节点的最短距离,初始时所有距离设为最大值,源点到自身的距离设为0。
- 队列与标记数组:使用队列存储需要松弛的节点,布尔数组 isInQueue 用于记录节点是否在队列中,避免重复入队。
- 入队次数统计:使用 count 数组记录每个节点的入队次数,若某个节点入队次数超过 n 次,则说明存在负权回路。
- 松弛过程:从队列中取出节点,遍历其所有出边,尝试更新终点的最短距离。若更新成功且终点不在队列中,则将终点加入队列,并统计入队次数。
- 结果输出:根据是否存在负权回路、是否可达终点输出相应结果。
总结
通过 SPFA 算法,我们可以在求解单源最短路径的同时检测图中是否存在负权回路。该算法在稀疏图中效率较高,且能够处理存在负权边的情况。理解节点入队次数与负权回路的关系是掌握该算法判断负权回路功能的关键。
Bellman-Ford 算法之单源有限最短路
题目描述
某国共有 n 个城市,通过 m 条单向道路连接。每条道路的权值为运输成本减去政府补贴。要求计算在最多经过 k 个城市的条件下,从城市 src 到城市 dst 的最低运输成本。
输入包含城市数量 n、道路数量 m,接着 m 行每行三个整数 s、t 和 v,表示从 s 到 t 的道路权值为 v。最后一行包含 src、dst 和 k,表示从 src 到 dst 最多经过 k 个城市。输出为最低运输成本,若无法在限制条件下到达则输出 “unreachable”。
输入输出示例
输入:
6 7
1 2 1
2 4 -3
2 5 2
1 3 5
3 5 1
4 6 4
5 6 -2
2 6 1
输出:
0
解题思路
算法原理
Bellman-Ford 算法通过松弛操作求解单源最短路径问题。本题要求最多经过 k 个城市,即最多经过 k+1 条边。因此,对所有边进行 k+1 次松弛操作即可得到符合条件的最短路径。
关键点
- 松弛操作:每次松弛操作更新源点到各节点的最短距离。
- 限制松弛次数:最多经过 k 个城市,因此松弛次数为 k+1。
- 记录上一轮松弛结果:使用辅助数组 minDist_copy 保存上一轮的松弛结果,避免当前轮次的更新影响后续计算。
代码实现
import java.util.*;
public class Main {
// 定义边的类,包含起点、终点和权值
static class Edge {
int from;
int to;
int val;
public Edge(int from, int to, int val) {
this.from = from;
this.to = to;
this.val = val;
}
}
public static void main(String[] args) {
// 输入处理
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(); // 城市数量
int m = sc.nextInt(); // 道路数量
List<Edge> graph = new ArrayList<>();
// 读取每条道路的信息
for (int i = 0; i < m; i++) {
int from = sc.nextInt();
int to = sc.nextInt();
int val = sc.nextInt();
graph.add(new Edge(from, to, val));
}
int src = sc.nextInt(); // 起点
int dst = sc.nextInt(); // 终点
int k = sc.nextInt(); // 最多经过的城市数
// 初始化最短距离数组,minDist[src] = 0,其余为最大值
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
minDist[src] = 0;
// 松弛 k + 1 次
for (int i = 1; i <= k + 1; i++) {
int[] minDistCopy = Arrays.copyOf(minDist, n + 1); // 保存上一轮的结果
for (Edge edge : graph) {
int from = edge.from;
int to = edge.to;
int val = edge.val;
// 使用上一轮的结果进行松弛
if (minDistCopy[from] != Integer.MAX_VALUE && minDist[to] > minDistCopy[from] + val) {
minDist[to] = minDistCopy[from] + val;
}
}
}
// 输出结果
if (minDist[dst] == Integer.MAX_VALUE) {
System.out.println("unreachable"); // 无法到达终点
} else {
System.out.println(minDist[dst]); // 输出最短距离
}
}
}
class Edge {
public int u; // 边的端点1
public int v; // 边的端点2
public int val; // 边的权值
public Edge() {
}
public Edge(int u, int v) {
this.u = u;
this.v = v;
this.val = 0;
}
public Edge(int u, int v, int val) {
this.u = u;
this.v = v;
this.val = val;
}
}
/**
* SPFA算法(版本3):处理含【负权回路】的有向图的最短路径问题
* bellman_ford(版本3) 的队列优化算法版本
* 限定起点、终点、至多途径k个节点
*/
public class SPFAForSSSP {
/**
* SPFA算法
*
* @param n 节点个数[1,n]
* @param graph 邻接表
* @param startIdx 开始节点(源点)
*/
public static int[] spfa(int n, List<List<Edge>> graph, int startIdx, int k) {
// 定义最大范围
int maxVal = Integer.MAX_VALUE;
// minDist[i] 源点到节点i的最短距离
int[] minDist = new int[n + 1]; // 有效节点编号范围:[1,n]
Arrays.fill(minDist, maxVal); // 初始化为maxVal
minDist[startIdx] = 0; // 设置源点到源点的最短路径为0
// 定义queue记录每一次松弛更新的节点
Queue<Integer> queue = new LinkedList<>();
queue.offer(startIdx); // 初始化:源点开始(queue和minDist的更新是同步的)
// SPFA算法核心:只对上一次松弛的时候更新过的节点关联的边进行松弛操作
while (k + 1 > 0 && !queue.isEmpty()) { // 限定松弛 k+1 次
int curSize = queue.size(); // 记录当前队列节点个数(上一次松弛更新的节点个数,用作分层统计)
while (curSize-- > 0) { //分层控制,限定本次松弛只针对上一次松弛更新的节点,不对新增的节点做处理
// 记录当前minDist状态,作为本次松弛的基础
int[] minDist_copy = Arrays.copyOfRange(minDist, 0, minDist.length);
// 取出节点
int cur = queue.poll();
// 获取cur节点关联的边,进行松弛操作
List<Edge> relateEdges = graph.get(cur);
for (Edge edge : relateEdges) {
int u = edge.u; // 与`cur`对照
int v = edge.v;
int weight = edge.val;
if (minDist_copy[u] + weight < minDist[v]) {
minDist[v] = minDist_copy[u] + weight; // 更新
// 队列同步更新(此处有一个针对队列的优化:就是如果已经存在于队列的元素不需要重复添加)
if (!queue.contains(v)) {
queue.offer(v); // 与minDist[i]同步更新,将本次更新的节点加入队列,用做下一个松弛的参考基础
}
}
}
}
// 当次松弛结束,次数-1
k--;
}
// 返回minDist
return minDist;
}
public static void main(String[] args) {
// 输入控制
Scanner sc = new Scanner(System.in);
System.out.println("1.输入N个节点、M条边(u v weight)");
int n = sc.nextInt();
int m = sc.nextInt();
System.out.println("2.输入M条边");
List<List<Edge>> graph = new ArrayList<>(); // 构建邻接表
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
while (m-- > 0) {
int u = sc.nextInt();
int v = sc.nextInt();
int weight = sc.nextInt();
graph.get(u).add(new Edge(u, v, weight));
}
System.out.println("3.输入src dst k(起点、终点、至多途径k个点)");
int src = sc.nextInt();
int dst = sc.nextInt();
int k = sc.nextInt();
// 调用算法
int[] minDist = SPFAForSSSP.spfa(n, graph, src, k);
// 校验起点->终点
if (minDist[dst] == Integer.MAX_VALUE) {
System.out.println("unreachable");
} else {
System.out.println("最短路径:" + minDist[n]);
}
}
}
代码注释
- Edge类:用于表示图中的边,包含起点、终点和权值。
- 输入处理:读取城市数量、道路数量以及每条道路的信息,构建边列表。
- 初始化:创建 minDist 数组存储源点到各节点的最短距离,初始时所有距离设为最大值,源点到自身的距离设为0。
- 松弛操作:对所有边进行 k+1 次松弛,每次松弛使用上一轮的结果(minDist_copy)来更新当前轮次的最短距离。
- 结果输出:检查终点的最短距离是否仍为最大值,若是则说明无法到达,否则输出该距离。
总结
本题通过限制 Bellman-Ford 算法的松弛次数来解决单源有限最短路问题。使用辅助数组记录上一轮松弛结果是关键,确保每次松弛基于正确的前驱状态。理解松弛操作的含义和次数限制是掌握该算法的核心。