Skip to content

拓扑排序扩展


⭐ 重要技巧

利用拓扑排序的过程,上游节点逐渐推送消息给下游节点的技巧

⚠️ 注意

这个技巧已经是树型dp的内容了,不过即便不会动态规划,本节课也能听懂动态规划专题(包括树型dp)会在后续【必备】课程里讲述

最大食物链计数

题目链接

https://www.luogu.com.cn/problem/P4017

思路分析

以每个节点看作结尾,记录有多少条路径可以到达该节点,将节点的信息推送给与之相连的下一个节点,依次累加统计,得到最终总的食物链条数,采用链式前向星建图

代码实现

java
import java.io.*;
import java.util.Arrays;

public class Main {

    public static int MAXN = 5001;

    public static int MAXM = 500001;

    public static int MOD = 80112002;

    // 链式前向星建图
    public static int[] head = new int[MAXN];

    public static int[] next = new int[MAXM];

    public static int[] to = new int[MAXM];

    public static int cnt;

    // 拓扑排序需要的队列
    public static int[] queue = new int[MAXN];

    // 拓扑排序需要的入度表
    public static int[] indegree = new int[MAXN];

    // 拓扑排序需要的推送信息
    public static int[] lines = new int[MAXN];

    public static int n, m;

    public static void build(int n) {
        cnt = 1;
        Arrays.fill(indegree, 0, n + 1, 0);
        Arrays.fill(lines, 0, n + 1, 0);
        Arrays.fill(head, 0, n + 1, 0);
    }

    // 采用头插,u -> v
    public static void addEdge(int u, int v) {
        // 新的 edge 的 next 伟原来的 head
        next[cnt] = head[u];
        to[cnt] = v;
        // 更新新的 head
        head[u] = cnt++;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        while (in.nextToken() != StreamTokenizer.TT_EOF) {
            n = (int) in.nval;
            in.nextToken();
            m = (int) in.nval;
            build(n);
            for (int i = 0; i < m; i++) {
                in.nextToken();
                int u = (int) in.nval;
                in.nextToken();
                int v = (int) in.nval;
                // u -> v
                addEdge(u, v);
                indegree[v]++;
            }
            out.println(ways());
        }
        out.flush();
        out.close();
        br.close();
    }

    public static int ways() {
        int l = 0;
        int r = 0;
        // 初始化
        for (int i = 1; i <= n; i++) {
            if (indegree[i] == 0) {
                queue[r++] = i;
                lines[i] = 1;
            }
        }
        int ans = 0;

        // 拓扑排序
        while (l < r) {
            int u = queue[l++];
            // 头边的边号为 0,表示当前节点不再有后续的邻居了
            if (head[u] == 0) {
                // 同于原理
                ans = (ans + lines[u]) % MOD;
            } else {
                for (int ei = head[u]; ei > 0; ei = next[ei]) {
                    // u -> v
                    int v = to[ei];
                    lines[v] = (lines[v] + lines[u]) % MOD;
                    if (--indegree[v] == 0) {
                        queue[r++] = v;
                    }
                }
            }
        }
        return ans;
    }
}

喧闹和富有

题目链接

https://leetcode.cn/problems/loud-and-rich/

思路分析

若 a 比 b 有钱则认为有一条 a 指向 b 的边,题目要求返回 ans 数组,i 位置往 i + 1 位置推送信息,如果发现 i 位置的安静值更小,则更新 i + 1 位置的值 ans [ i + 1 ] 为 ans [ i ]

代码实现

java
class Solution {
    public int[] loudAndRich(int[][] richer, int[] quiet) {
        int n = quiet.length;

        // 邻接表建图
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            graph.add(new ArrayList<>());
        }

        // 构建邻接表
        int[] indegree = new int[n];
        for (int[] r : richer) {
            graph.get(r[0]).add(r[1]);
            indegree[r[1]]++;
        }

        int[] queue = new int[n];
        int l = 0;
        int r = 0;
        for (int i = 0; i < n; i++) {
            if (indegree[i] == 0) {
                queue[r++] = i;
            }
        }

        int[] ans = new int[n];
        // 初始化
        for (int i = 0; i < n; i++) {
            ans[i] = i;
        }

        // 拓扑排序
        while (l < r) {
            int cur = queue[l++];
            for (int next : graph.get(cur)) {
                if (quiet[ans[next]] > quiet[ans[cur]]) {
                    ans[next] = ans[cur];
                }
                if (--indegree[next] == 0) {
                    queue[r++] = next;
                }
            }
        }
        return ans;
    }
}

并行课程

题目链接

https://leetcode.cn/problems/parallel-courses-iii/

思路分析

本题坑点:完成第 i 门课程花费的时间记录在 time [ i - 1 ] 中

首先更新 cost [ i ],即完成之前任务需要的时间加上完成第 i 门课程的时间,完成之前任务需要的时间和累加更新后的时间取最大值,推消息,cost [ next ] 更新为 cost [ cur ] 和 cost [ next ] 取最大值

举例:完成 a 课程需要 3,完成 b 课程需要 4,且都是 c 的前置课程,则对于 c 而言,时间取值应该取最大,即至少等到 b 完成了才可以开始完成 c,且过程中 a 已经完成

代码实现

java
class Solution {
    public int minimumTime(int n, int[][] relations, int[] time) {
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i <= n; i++) {
            graph.add(new ArrayList<>());
        }
        int[] indegree = new int[n + 1];
        for (int[] edge : relations) {
            graph.get(edge[0]).add(edge[1]);
            indegree[edge[1]]++;
        }
        int[] queue = new int[n + 1];
        int l = 0;
        int r = 0;
        for (int i = 1; i <= n; i++) {
            if (indegree[i] == 0) {
                queue[r++] = i;
            }
        }
        int[] cost = new int[n + 1];
        int ans = 0;
        while (l < r) {
            int cur = queue[l++];
            // 注意:完成 i 的时间记录在 time[i - 1] 中
            cost[cur] += time[cur - 1];
            ans = Math.max(ans, cost[cur]);
            for (int next : graph.get(cur)) {
                cost[next] = Math.max(cost[next], cost[cur]);
                if (--indegree[next] == 0) {
                    queue[r++] = next;
                }
            }
        }
        return ans;
    }
}

参加会议的最多员工数

题目链接

https://leetcode.cn/problems/maximum-employees-to-be-invited-to-a-meeting/

思路分析

情况一:定义环中人数为 2 的为小环,分别找到环中延申的最长链,再加上中心点的个数

情况二:定义环中人数 > 2 的为大环,大环中无法再融入其他人,即取环中的个数

在环上的点的入度都不会为 0,利用这一特点统计环上节点个数,通过推消息模型记录链的长度,情况一和情况二取最大值即为答案

代码实现

java
class Solution {
    public int maximumInvitations(int[] favorite) {
        // favorite[a] = b 表示 a 喜欢 b
        int n = favorite.length;
        int[] indegree = new int[n];
        for (int i = 0; i < n; i++) {
            indegree[favorite[i]]++;
        }
        int[] queue = new int[n];
        int l = 0;
        int r = 0;
        for (int i = 0; i < n; i++) {
            if (indegree[i] == 0) {
                queue[r++] = i;
            }
        }

        // deep[i] 表示不包括 i 在内,i 之前的最长链的长度
        int[] deep = new int[n];
        while (l < r) {
            int cur = queue[l++];
            int next = favorite[cur];
            deep[next] = Math.max(deep[next], deep[cur] + 1);
            if (--indegree[next] == 0) {
                queue[r++] = next;
            }
        }

        // 情况一:环中节点数为 2,算最长链的个数加上中心点的个数
        int sumOfSmallRings = 0;
        // 情况二:环中节点数 > 2,取环中节点个数
        int bigRings = 0;

        // 遍历环上节点
        for (int i = 0; i < n; i++) {
            // 在环上的点的入度都不会为 0,利用这一特点统计环上节点个数
            if (indegree[i] > 0) {
                int ringSize = 1;
                indegree[i] = 0;
                for (int j = favorite[i]; j != i; j = favorite[j]) {
                    ringSize++;
                    // 标记为 0 的目的是不希望再次遍历在环中的节点,便于找到不同的环
                    indegree[j] = 0;
                }
                // 情况一:小环
                if (ringSize == 2) {
                    sumOfSmallRings += 2 + deep[i] + deep[favorite[i]];
                } else {
                    // 情况二:大环
                    bigRings = Math.max(bigRings, ringSize);
                }
            }

        }
        // 情况一和情况二取最大值即为答案
        return Math.max(sumOfSmallRings, bigRings);
    }
}