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

拓扑排序精讲

题目链接

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

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

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

三部曲:

  1. 选距离源点最近且未访问的点
  2. 标记该点访问过,加入到最短路径
  3. 更新所有相连未访问节点到源点距离 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 权值不可以为负数(因为不会因为负数,更新已经访问过的节点)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值