1.卡码网:94. 城市间货物运输 I
题目链接:https://kamacoder.com/problempage.php?pid=1152
文章链接:https://www.programmercarl.com/kamacoder/0094.城市间货物运输I-SPFA.html
思路:
只对 上一次松弛的时候更新过的节点作为出发节点所连接的边 进行松弛就够了,不需要对所有边进行松弛。
基于以上思路,如何记录 上次松弛的时候更新过的节点呢?
用队列来记录。(其实用栈也行,对元素顺序没有要求)
针对:图中边的权值可以有负数,且不存在任何负权回路的情况。(可以为正权回路)
import java.util.*;
public class Main {
// Define an inner class Edge
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) {
// Input processing
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));
}
// Declare the minDist array to record the minimum distance form current node to the original node
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
minDist[1] = 0;
// Declare a queue to store the updated nodes instead of traversing all nodes each loop for more efficiency
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
// Declare a boolean array to record if the current node is in the queue to optimise the processing
boolean[] isInQueue = new boolean[n + 1];
while (!queue.isEmpty()) {
int curNode = queue.poll();
isInQueue[curNode] = false; // Represents the current node is not in the queue after being polled
for (Edge edge : graph.get(curNode)) {
if (minDist[edge.to] > minDist[edge.from] + edge.val) { // Start relaxing the edge
minDist[edge.to] = minDist[edge.from] + edge.val;
if (!isInQueue[edge.to]) { // Don't add the node if it's already in the queue
queue.offer(edge.to);
isInQueue[edge.to] = true;
}
}
}
}
// Outcome printing
if (minDist[n] == Integer.MAX_VALUE) {
System.out.println("unconnected");
} else {
System.out.println(minDist[n]);
}
}
}
2.卡码网:95. 城市间货物运输 II
题目链接:https://kamacoder.com/problempage.php?pid=1153
文章链接:https://www.programmercarl.com/kamacoder/0095.城市间货物运输II.html
思路:
- 使用 Bellman_ford 算法判断负权回路
在 bellman_ford 算法中,松弛 n-1 次所有的边 就可以求得 起点到任何节点的最短路径,松弛 n 次以上,minDist数组(记录起到到其他节点的最短距离)中的结果也不会有改变 。
而本题有负权回路的情况下,一直都会有更短的最短路,所以 松弛 第n次,minDist数组 也会发生改变。
那么解决本题的 核心思路,就是在 kama94.城市间货物运输I 的基础上,再多松弛一次,看minDist数组 是否发生变化。若变化,则表示出现负权回路;否则,没有。
注意:若题目中没有提前声明图中是否没有负权回路,则对于 在有负权值的图中求最短路,都需要先看看这个图里有没有负权回路。若有,在这样的图中求最短路的话, 就会在这个环里无限循环 (也是负数+负数 只会越来越小),无法求出最短路径。
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(); // 道路数量
List<List<Integer>> roadList = new ArrayList<>();
for (int i=0;i<m;i++) {
int s = sc.nextInt();
int t = sc.nextInt();
int v = sc.nextInt();
List<Integer> list = new ArrayList<>();
list.add(s);
list.add(t);
list.add(v);
roadList.add(list);
}
int[] minDist = new int[n+1]; // 记录起点到各个节点的最短距离
Arrays.fill(minDist,Integer.MAX_VALUE);
minDist[1]=0;
boolean flag = false; // 判断是否出现负权回路
// 松弛n-1次
for(int j=1;j<=n;j++) { // 注意这里松弛了n下
// 对所有道路进行分析
for(int i=0;i<m;i++) {
int from = roadList.get(i).get(0);
int to = roadList.get(i).get(1);
int value = roadList.get(i).get(2);
if (j < n) {
if (minDist[from] != Integer.MAX_VALUE && minDist[from] + value < minDist[to]) {
minDist[to] = minDist[from] + value;
}
} else { // 多松弛一下,检查是否数组有变化
if (minDist[from] != Integer.MAX_VALUE && minDist[from] + value < minDist[to]) {
flag = true; // 出现负权回路
}
}
}
}
if (flag) {
System.out.println("circle");
return;
}
if (minDist[n] == Integer.MAX_VALUE) {
System.out.println("unconnected");
} else {
System.out.println(minDist[n]);
}
}
}
- 使用 队列优化版的bellman_ford(SPFA)判断是否有负权回路。
在 0094.城市间货物运输I-SPFA 中,在极端情况下,即:所有节点都与其他节点相连,每个节点的入度为 n-1 (n为节点数量),所以每个节点最多加入 n-1 次队列。
那么如果节点加入队列的次数 超过了 n-1次 ,那么该图就一定有负权回路。
import java.util.*;
public class Main {
// 基于Bellman_ford-SPFA方法
// Define an inner class Edge
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) {
// Input processing
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));
}
// Declare the minDist array to record the minimum distance form current node to the original node
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
minDist[1] = 0;
// Declare a queue to store the updated nodes instead of traversing all nodes each loop for more efficiency
Queue<Integer> queue = new LinkedList<>();
queue.offer(1);
// Declare an array to record the times each node has been offered in the queue
int[] count = new int[n + 1];// 记录节点加入队列几次
count[1]++;
// Declare a boolean array to record if the current node is in the queue to optimise the processing
boolean[] isInQueue = new boolean[n + 1];
// Declare a boolean value to check if there is a negative weight loop inside the graph
boolean flag = false;
while (!queue.isEmpty()) {
int curNode = queue.poll();
isInQueue[curNode] = false; // Represents the current node is not in the queue after being polled
for (Edge edge : graph.get(curNode)) {
if (minDist[edge.to] > minDist[edge.from] + edge.val) { // Start relaxing the edge
minDist[edge.to] = minDist[edge.from] + edge.val;
if (!isInQueue[edge.to]) { // Don't add the node if it's already in the queue
queue.offer(edge.to);
count[edge.to]++;
isInQueue[edge.to] = true;
}
if (count[edge.to] == n) { // 如果加入队列次数超过 n-1次 就说明该图与负权回路
flag = true;
while (!queue.isEmpty()) queue.poll();
break;
}
}
}
}
if (flag) {
System.out.println("circle");
} else if (minDist[n] == Integer.MAX_VALUE) {
System.out.println("unconnected");
} else {
System.out.println(minDist[n]);
}
}
}
3.卡码网:96. 城市间货物运输 III
题目链接:https://kamacoder.com/problempage.php?pid=1154
文章链接:https://www.programmercarl.com/kamacoder/0096.城市间货物运输III.html
思路:
- 使用 Bellman_ford 算法
题目中描述是 最多经过 k 个城市的条件下,而不是一定经过k个城市,也可以经过的城市数量比k小,但要最短的路径。
最多经过k个城市等价于最多经过k + 1 条边。
由于对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离,那么对所有边松弛 k + 1次,就是求 起点到达 与起点k + 1条边相连的节点的 最短距离。若松弛k+1次后,终点城市权值有变化,则表示最多经过k+1条边能够到达终点城市,此时权值就是从起点到终点的最低运输成本;若终点城市权值仍然是初始值,则表示k+1个边无法到达。
注意:
第n次松弛必须保证只会更新与起点相连n条边的节点的最短距离,不会更新其他情况的节点,且与起点相连n条边的节点的最短距离是由上一次松弛的结果获得的,而不是当前松弛的结果获得。当前松弛结果只会影响下一次松弛结果。
所以在每次计算 minDist 时候,要基于 对所有边上一次松弛的 minDist 数值才行,因此我们要记录上一次松弛的minDist。只有这样前面节点松弛的结果才不会影响剩余节点的权值。
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(); // 道路数量
List<List<Integer>> roadList = new ArrayList<>();
for (int i=0;i<m;i++) {
int s = sc.nextInt();
int t = sc.nextInt();
int v = sc.nextInt();
List<Integer> list = new ArrayList<>();
list.add(s);
list.add(t);
list.add(v);
roadList.add(list);
}
int src = sc.nextInt();
int dst = sc.nextInt();
int k = sc.nextInt();
int[] minDist = new int[n+1]; // 记录起点到各个节点的最短距离
Arrays.fill(minDist,Integer.MAX_VALUE);
minDist[src]=0;
// 松弛k+1次
for(int j=1;j<=k+1;j++) {
int[] minDistCopy = Arrays.copyOf(minDist,minDist.length); // 该数组保证每次松弛节点权值的更新只受上一次松弛结果的影响
// 对所有道路进行分析
for(int i=0;i<m;i++) {
int from = roadList.get(i).get(0);
int to = roadList.get(i).get(1);
int value = roadList.get(i).get(2);
if (minDistCopy[from] != Integer.MAX_VALUE && minDistCopy[from] + value < minDist[to]) {
minDist[to] = minDistCopy[from] + value;
} // 使用上一次松弛的结果使得这次松弛只会更新以上一次松弛更新节点为起点的边的终点节点的权值。
}
}
if (minDist[dst] == Integer.MAX_VALUE) {
System.out.println("unreachable");
} else {
System.out.println(minDist[dst]);
}
}
}
- 使用 队列优化版的bellman_ford(SPFA)
使用SPFA算法解决本题的时候,关键在于 如何控制松弛k次。可以用一个变量 que_size 记录每一轮松弛入队列的所有节点数量。下一轮松弛的时候,就把队列里 que_size 个节点都弹出来,就是上一轮松弛入队列的节点。
import java.util.*;
public class Main {
static class Edge { // 邻接表中的边
int to; // 连接的节点
int val; // 边的权重
Edge(int t, int w) {
to = t;
val = w;
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
List<List<Edge>> grid = new ArrayList<>();
for (int i = 0; i <= n; i++) {
grid.add(new ArrayList<>());
}
// 将所有边保存到邻接表中
for (int i = 0; i < m; i++) {
int p1 = scanner.nextInt();
int p2 = scanner.nextInt();
int val = scanner.nextInt();
grid.get(p1).add(new Edge(p2, val));
}
int start = scanner.nextInt();
int end = scanner.nextInt();
int k = scanner.nextInt();
k++;
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
int[] minDistCopy = new int[n + 1]; // 用来记录每一次遍历的结果
minDist[start] = 0;
Queue<Integer> queue = new LinkedList<>();
queue.add(start); // 队列里放入起点
int queueSize;
while (k-- > 0 && !queue.isEmpty()) {
boolean[] visited = new boolean[n + 1]; // 每一轮松弛中,控制节点不用重复入队列
minDistCopy = minDist.clone(); // 获取上一次计算的结果
queueSize = queue.size(); // 记录上次入队列的节点个数
while (queueSize-- > 0) { // 上一轮松弛入队列的节点,这次对应的边都要做松弛
int node = queue.poll();
for (Edge edge : grid.get(node)) {
int from = node;
int to = edge.to;
int price = edge.val;
if (minDist[to] > minDistCopy[from] + price) {
minDist[to] = minDistCopy[from] + price;
if (visited[to]) continue; // 不用重复放入队列,但需要重复松弛,所以放在这里位置
visited[to] = true;
queue.add(to);
}
}
}
}
if (minDist[end] == Integer.MAX_VALUE) {
System.out.println("unreachable");
} else {
System.out.println(minDist[end]);
}
}
}