代码随想录算法训练营第 56 天 | 拓扑排序精讲、Dijkstra(朴素版)精讲

拓扑排序精讲

题目链接

注意本题节点从 0 到 n - 1

拓扑排序思路(bfs 算法):
循环执行以下三步:

  1. 找入度为 0 的节点添加到队列
  2. 从队列取出首节点添加到结果集
  3. 删节点,看有没有它连接的节点入度变成 0

最后怎么判断有没有成环:

结果集个数小于 n,成环,输出 -1

很巧妙的一点:

inDegree[node]-- 后直接判断 if (inDegree[node] == 0)

不用在重新遍历一遍 inDegree 数组,不然还得区分哪些入度为 0 的节点之前已经放到队列中了。

java 复制代码
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

java 复制代码
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

三部曲:

  1. 选距离源点最近且未访问的点
  2. 标记该点访问过,加入到最短路径
  3. 更新所有相连未访问节点到源点距离 minDist

时间复杂度:O(n^2)

不仅是求源点到终点的最短距离,而且它把源点到所有节点的最短距离都求出来了。

重点:minDist[1] 要初始化为 0。

java 复制代码
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);
        }
    }
}

拓展:输出最短路径的每条边

用一维数组 int[] parent。指向为 parent[i] -> i

java 复制代码
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 权值不可以为负数(因为不会因为负数,更新已经访问过的节点)
相关推荐
qinyia1 小时前
WisdomSSH解决docker run命令中log-opt参数不支持导致的容器创建失败问题
java·docker·eureka
potato_may2 小时前
CC++ 内存管理 —— 程序的“五脏六腑”在哪里?
c语言·开发语言·数据结构·c++·内存·内存管理
饕餮怪程序猿2 小时前
A*算法(C++实现)
开发语言·c++·算法
电饭叔2 小时前
不含Luhn算法《python语言程序设计》2018版--第8章14题利用字符串输入作为一个信用卡号之二(识别卡号有效)
java·python·算法
小付爱coding2 小时前
Claude Code安装教程【windows版本】
java·git·python
**蓝桉**2 小时前
数组的执行原理,java程序的执行原理
java·开发语言
2301_800256112 小时前
8.2 空间查询基本组件 核心知识点总结
数据库·人工智能·算法
不穿格子的程序员2 小时前
从零开始写算法——矩阵类题:矩阵置零 + 螺旋矩阵
线性代数·算法·矩阵
YDS8292 小时前
MyBatis-Plus精讲 —— 从快速入门到项目实战
java·后端·spring·mybatis·mybatis-plus