拓扑排序精讲
注意本题节点从 0 到 n - 1
拓扑排序思路(bfs 算法):
循环执行以下三步:
- 找入度为 0 的节点添加到队列
- 从队列取出首节点添加到结果集
- 删节点,看有没有它连接的节点入度变成 0
最后怎么判断有没有成环:
结果集个数小于 n,成环,输出 -1
很巧妙的一点:
在 inDegree[node]-- 后直接判断 if (inDegree[node] == 0)
不用在重新遍历一遍 inDegree 数组,不然还得区分哪些入度为 0 的节点之前已经放到队列中了。
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();
int[] inDegree = new int[n]; // 入度
HashMap<Integer, List<Integer>> map = new HashMap<>(); // 类似于邻接表,保存 s 连接的 t
for (int i = 0; i < k; i++) {
int s = sc.nextInt();
int t = sc.nextInt();
inDegree[t]++;
List<Integer> temp = map.getOrDefault(s, new ArrayList<>());
temp.add(t);
map.put(s, temp);
}
Queue<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) { // 初始先把入度为 0 的节点添加到队列中
queue.offer(i);
}
}
List<Integer> result = new ArrayList<>();
while (!queue.isEmpty()) {
int cur = queue.poll();
result.add(cur); // 添加到结果集
List<Integer> nodes = map.get(cur);
if (nodes != null) { // 需要保证 nodes 不为空,不然会空指针异常
for (int i = 0; i < nodes.size(); i++) {
int node = nodes.get(i);
inDegree[node]--;
if (inDegree[node] == 0) { // 巧妙:直接在这里判断入度有没有被减成 0,而不是之后重新遍历 inDegree 数组找入度为 0 的节点
queue.offer(node);
}
}
}
}
if (result.size() == n) {
for (int i = 0; i < n - 1; i++) {
System.out.print(result.get(i) + " ");
}
System.out.println(result.get(n - 1));
} else { // 结果集个数小于 n 说明出现了环
System.out.println(-1);
}
}
}
优化:存储图时,将 HashMap<Integer, List<Integer>> 换成 List<List<Integer>> 邻接表,这样不用判 null
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();
int[] inDegree = new int[n]; // 入度
List<List<Integer>> graph = new ArrayList<>(); // 采用邻接表保存图
for (int i = 0; i < n; i++) {
graph.add(new ArrayList<>());
}
for (int i = 0; i < k; i++) {
int s = sc.nextInt();
int t = sc.nextInt();
inDegree[t]++;
graph.get(s).add(t);
}
Queue<Integer> queue = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) { // 初始先把入度为 0 的节点添加到队列中
queue.offer(i);
}
}
List<Integer> result = new ArrayList<>();
while (!queue.isEmpty()) {
int cur = queue.poll();
result.add(cur); // 添加到结果集
List<Integer> nodes = graph.get(cur);
for (int node : nodes) {
inDegree[node]--;
if (inDegree[node] == 0) { // 巧妙:直接在这里判断入度有没有被减成 0,而不是之后重新遍历 inDegree 数组找入度为 0 的节点
queue.offer(node);
}
}
}
if (result.size() == n) {
for (int i = 0; i < n - 1; i++) {
System.out.print(result.get(i) + " ");
}
System.out.println(result.get(n - 1));
} else { // 结果集个数小于 n 说明出现了环
System.out.println(-1);
}
}
}
Dijkstra(朴素版)精讲
注意本体节点从 1 到 n
三部曲:
- 选距离源点最近且未访问的点
- 标记该点访问过,加入到最短路径
- 更新所有相连且未访问节点到源点距离 minDist
不仅是求源点到终点的最短距离,而且它把源点到所有节点的最短距离都求出来了。
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();
int[][] graph = new int[n + 1][n + 1];
for (int i = 0; i < n + 1; i++) {
Arrays.fill(graph[i], Integer.MAX_VALUE); // 重要:一开始节点之间的距离全为 max
}
for (int i = 0; i < k; i++) {
int s = sc.nextInt();
int t = sc.nextInt();
int val = sc.nextInt();
graph[s][t] = val;
}
int start = 1, end = n;
boolean[] visited = new boolean[n + 1]; // 有没有访问过
int[] minDist = new int[n + 1]; // 到源点的距离最小值
Arrays.fill(minDist, Integer.MAX_VALUE); // 到源点距离全部初始化为 max
minDist[1] = 0; // 自己到自己的距离为 0
for (int i = 1; i < n + 1; i++) { // 循环 n 次
// 第一步:选距离源点最近且未访问的点
int minVal = Integer.MAX_VALUE; // 找数组中节点(未访问过)最小值
int cur = 1;
for (int j = 1; j < n + 1; j++) {
if (!visited[j] && minDist[j] < minVal) {
minVal = minDist[j];
cur = j;
}
}
// 第二步:标记为访问过
visited[cur] = true;
// 第三步:更新所有**相连**的、**未访问**的节点到源点距离
for (int j = 1; j < n + 1; j++) {
if (!visited[j] && graph[cur][j] != Integer.MAX_VALUE && minDist[cur] + graph[cur][j] < minDist[j]) { // 未访问过 + 和 cur 节点是连接的 + 防溢出
minDist[j] = minDist[cur] + graph[cur][j];
}
}
}
if (minDist[end] != Integer.MAX_VALUE) {
System.out.println(minDist[end]);
} else {
System.out.println(-1);
}
}
}minDist[end] != Integer.MAX_VALUE) {
System.out.println(minDist[end]);
} else {
System.out.println(-1);
}
}
}
拓展:输出最短路径的每条边
用一维数组 int[] parent。指向为 parent[i] -> i
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();
int[][] graph = new int[n + 1][n + 1];
for (int i = 0; i < n + 1; i++) {
Arrays.fill(graph[i], Integer.MAX_VALUE); // 重要:一开始节点之间的距离全为 max
}
for (int i = 0; i < k; i++) {
int s = sc.nextInt();
int t = sc.nextInt();
int val = sc.nextInt();
graph[s][t] = val;
}
int start = 1, end = n;
boolean[] visited = new boolean[n + 1]; // 有没有访问过
int[] minDist = new int[n + 1]; // 到源点的距离最小值
Arrays.fill(minDist, Integer.MAX_VALUE); // 到源点距离全部初始化为 max
minDist[1] = 0; // 自己到自己的距离为 0
int[] parent = new int[n + 1]; // 存储实际路径。指向为 parent[i] -> i
for (int i = 1; i < n + 1; i++) { // 循环 n 次
// 第一步:选距离源点最近且未访问的点
int minVal = Integer.MAX_VALUE; // 找数组中节点(未访问过)最小值
int cur = 1;
for (int j = 1; j < n + 1; j++) {
if (!visited[j] && minDist[j] < minVal) {
minVal = minDist[j];
cur = j;
}
}
// 第二步:标记为访问过
visited[cur] = true;
// 第三步:更新所有**相连**的、**未访问**的节点到源点距离
for (int j = 1; j < n + 1; j++) {
if (!visited[j] && graph[cur][j] != Integer.MAX_VALUE && minDist[cur] + graph[cur][j] < minDist[j]) { // 未访问过 + 和 cur 节点是连接的 + 防溢出
minDist[j] = minDist[cur] + graph[cur][j];
parent[j] = cur;
}
}
}
if (minDist[end] != Integer.MAX_VALUE) {
System.out.println(minDist[end]);
} else {
System.out.println(-1);
}
// 输出起点到终点的最短路径
int a = end;
int b = parent[end];
while (b != 0) {
System.out.println(b + "->" + a);
a = b;
b = parent[a];
}
// // 输出每个节点最短路径情况
// for (int i = 1; i <= n; i++) {
// System.out.println(parent[i] + "->" + i);
// }
}
}
总结:
和 Prim 算法区别:
-
- Prim 遍历 n - 1 次(找出 n - 1 条边即可)
- Dijkstra 遍历 n 次(因为可能终点也指向了其他的点?所以需要更新距离。)
-
- Prim 初始化为 Integer.MAX_VALUE 后,不用担心溢出问题
- Dijkstra 需要担心溢出问题,因为会做加法。(其实只要只更新相连的节点距离,就刚好也解决了这个问题)
一个是从最小生成树出发,一个是从源点出发
-
- Prim 权值可以为负数
- Dijkstra 权值不可以为负数(因为不会因为负数,更新已经访问过的节点)
1155

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



