Day 50
拓扑排序
拓扑排序看上去很复杂,其实了解其原理之后,代码不难
题目链接:https://kamacoder.com/problempage.php?pid=1191
文章讲解:https://www.programmercarl.com/kamacoder/0117.软件构建.html
视频讲解:https://www.bilibili.com/video/BV1czGJzkERe
思路分析
应用场景
节点之间存在依赖关系,需要通过拓扑排序梳理依赖关系
模板思路
(1)找到入度为 0 的节点,加入结果集
(2)将该节点从图中移除
本质是要将该节点作为出发点所连接的节点的入度减一就可以了,这样好能根据入度找下一个节点,不用真在图里把这个节点删掉
注意点
结果集的顺序,就是我们想要的拓扑排序顺序 (结果集里顺序可能不唯一)
需要判断是否存在环(即循环依赖),可以通过输出的节点个数判断
题解
java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// N 个文件
int n = scanner.nextInt();
// M 条依赖关系
int m = scanner.nextInt();
// 记录节点的依赖关系
List<List<Integer>> umap = new ArrayList<>();
// 记录节点的入度
int[] inDegree = new int[n];
for (int i = 0; i < n; i++) {
umap.add(new ArrayList<>());
}
for (int i = 0; i < m; i++) {
int s = scanner.nextInt();
int t = scanner.nextInt();
// 记录 s 指向哪些节点
umap.get(s).add(t);
// t 的入度加一
inDegree[t]++;
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) {
// 入度为 0 的节点可以作为开头,先加入队列
queue.add(i);
}
}
List<Integer> result = new ArrayList<>();
// 拓扑排序(BFS版本)
while (!queue.isEmpty()) {
// 取出队头元素
int cur = queue.poll();
// 记录结果
result.add(cur);
for (int file : umap.get(cur)){
// cur 指向的节点的入度减一
inDegree[file]--;
if (inDegree[file] == 0){
queue.add(file);
}
}
}
if (result.size() == n){
for (int i = 0; i < result.size(); i++) {
System.out.print(result.get(i));
if (i < result.size() - 1){
System.out.print(" ");
}
}
}else {
// 出现环(即循环依赖)
System.out.println(-1);
}
}
}dijkstra(朴素版)
后面几天都是最短路系列了,对于最短路系列,我的建议是,如果第一次接触最短路算法的话,能看懂原理,能照着代码随想录把代码抄下来就可以了,二刷的时候再尝试自己去写出来。三刷的时候,差不多才能把最短路吃透。对于一刷的录友们,不要强行去逼迫自己去学透,很难刚接触到最短路算法就学透
题目链接:https://kamacoder.com/problempage.php?pid=1047
文章讲解:https://www.programmercarl.com/kamacoder/0047.参会dijkstra朴素.html
视频讲解:https://www.bilibili.com/video/BV1kXuEzAEaz
思路分析
(1)思路模板(和 Prim 算法很像)
第一步,选源点到哪个节点近且该节点未被访问过
第二步,该最近节点被标记访问过
第三步,更新非访问节点到源点的距离(即更新 minDist 数组)
(2)注意点
节点编号从 1 开始,数组大小初始为 n + 1 个大小,注意边界判断
dijkstra 算法可以同时求起点到所有节点的最短路径
权值不能为负数
题解
java
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
int[][] grid = new int[n + 1][n + 1];
// 需要求最小值,初始化为最大值
for (int i = 0; i <= n; i++) {
Arrays.fill(grid[i], Integer.MAX_VALUE);
}
for (int i = 0; i < m; i++) {
int p1 = scanner.nextInt();
int p2 = scanner.nextInt();
int val = scanner.nextInt();
grid[p1][p2] = val;
}
int start = 1;
int end = n;
// 存储从源点到每个节点的最短距离
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
// 记录顶点是否被访问过
boolean[] visited = new boolean[n + 1];
// 起点到自身的距离为 0
minDist[start] = 0;
// Dijkstra
for (int i = 1; i <= n; i++) {
int minVal = Integer.MAX_VALUE;
// 用于记录节点。初始时指向源点
int cur = 1;
// 步骤一:找到离源点最近的节点
for (int j = 1; j <= n; j++) {
if (!visited[j] && minDist[j] < minVal){
minVal = minDist[j];
cur = j;
}
}
// 步骤二:标记该节点被访问过
visited[cur] = true;
// 步骤三:更新非访问节点到源点的距离(即更新 minDist 数组)
for (int j = 1; j <= n ; j++) {
if (!visited[j] && grid[cur][j] != Integer.MAX_VALUE && minDist[cur] + grid[cur][j] < minDist[j]){
minDist[j] = minDist[cur] + grid[cur][j];
}
}
}
// 无法从源点到达终点
if (minDist[end] == Integer.MAX_VALUE){
System.out.println(-1);
}else {
System.out.println(minDist[end]);
}
}
}打印路径
思路和 prim 算法类似,在更新最短路径的时候记录路径,最后输出即可
注意点:不能写成 parent[cur] = j,在 for 循环中,有多个 j 满足要求,那么 parent[cur] 就会被反复覆盖,因为 cur 是一个固定值
java
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
int[][] grid = new int[n + 1][n + 1];
// 需要求最小值,初始化为最大值
for (int i = 0; i <= n; i++) {
Arrays.fill(grid[i], Integer.MAX_VALUE);
}
for (int i = 0; i < m; i++) {
int p1 = scanner.nextInt();
int p2 = scanner.nextInt();
int val = scanner.nextInt();
grid[p1][p2] = val;
}
int start = 1;
int end = n;
// 存储从源点到每个节点的最短距离
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
// 记录顶点是否被访问过
boolean[] visited = new boolean[n + 1];
// 起点到自身的距离为 0
minDist[start] = 0;
// 记录最短路径
int[] path = new int[n + 1];
// Dijkstra
for (int i = 1; i <= n; i++) {
int minVal = Integer.MAX_VALUE;
// 用于记录节点。初始时指向源点
int cur = 1;
// 步骤一:找到离源点最近的节点
for (int j = 1; j <= n; j++) {
if (!visited[j] && minDist[j] < minVal) {
minVal = minDist[j];
cur = j;
}
}
// 步骤二:标记该节点被访问过
visited[cur] = true;
// 步骤三:更新非访问节点到源点的距离(即更新 minDist 数组)
for (int j = 1; j <= n; j++) {
if (!visited[j] && grid[cur][j] != Integer.MAX_VALUE && minDist[cur] + grid[cur][j] < minDist[j]) {
minDist[j] = minDist[cur] + grid[cur][j];
// 这里通过 cur 节点找到了更短的路径
// 即 path 数组记录的 j 节点的上一个节点是 cur
// 举例(1)原先是 起点 --> j 节点(2)现在是 起点 --> cur 节点 --> j 节点
path[j] = cur;
}
}
}
// 无法从源点到达终点
if (minDist[end] == Integer.MAX_VALUE) {
System.out.println(-1);
} else {
System.out.println(minDist[end]);
}
// 输出最短情况
for (int i = 1; i <= n; i++) {
// (1)原先是 起点 --> i 节点(2)现在是 起点 --> cur 节点 --> i 节点
System.out.println(path[i] + " -> " + i);
}
}
}封装 Dijkstra
java
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// n 个节点
int n = scanner.nextInt();
// n 条边
int m = scanner.nextInt();
// 节点编号从 1 开始
int[][] graph = new int[n + 1][n + 1];
// 初始化为最大值
for (int i = 0; i <= n; i++) {
Arrays.fill(graph[i], Integer.MAX_VALUE);
}
// m 条边
for (int i = 0; i < m; i++) {
int s = scanner.nextInt();
int t = scanner.nextInt();
int val = scanner.nextInt();
graph[s][t] = val;
}
// 定义起点和终点
int start = 1;
int end = n;
// 传入图、起点终点、节点个数
int res = dijkstra(graph, start, end, n);
System.out.println(res);
}
public static int dijkstra(int[][] graph, int start, int end, int n) {
// 表示从起点到所有点的最短距离,初始为最大值
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
boolean[] visited = new boolean[n + 1];
// 起点到自身的距离为 0
minDist[start] = 0;
// Dijkstra
// n 个节点,执行 n 次
for (int i = 1; i <= n; i++) {
// 用于更新最小值
int minVal = Integer.MAX_VALUE;
// 用于更新节点
int cur = 1;
// 第一步:找到距离源点最近的节点
for (int j = 1; j <= n; j++) {
if (!visited[j] && minDist[j] < minVal) {
minVal = minDist[j];
cur = j;
}
}
// 第二步:设置为访问过
visited[cur] = true;
// 第三步:更新最短路径(通过当前节点是否会使得路径更短)
for (int j = 1; j <= n; j++) {
if (!visited[j] && graph[cur][j] != Integer.MAX_VALUE
&& minDist[cur] + graph[cur][j] < minDist[j]){
minDist[j] = minDist[cur] + graph[cur][j];
}
}
}
if (minDist[end] == Integer.MAX_VALUE){
return -1;
}
return minDist[end];
}
}