【算法】拓扑排序

22 篇文章 2 订阅

  今天学习拓扑排序。如果一个有向图的任意顶点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图(Directed Acyclic Graph,DAG)。拓扑排序就是将有向无环图的所有顶点排序,使得图中任意两个点 u、v,只要存在边 u → v,那么拓扑排序中 u 一定在 v 前面。例如,高等数学、线性代数都可以直接开始学习,复变函数要学习完前两门课才能学习,所以,拓扑排序可以是:高等数学→线性代数→复变函数,或者线性代数→高等数学→复变函数。下面讲解具体算法的思路。

1.拓扑排序算法

基本思想如下:

  1. 定义一个队列 q,把所有入度为 0 的节点入队;
  2. 取队首节点,输出。删除它的出边,令出边到达的顶点入度减 1,如果该顶点入度变成 0,则将其加入队列;
  3. 反复进行第 2 步,直到队空。如果所有点都被访问,则 G 是有向无环图,否则有环。

代码如下:

List<List<Integer>> edges = new ArrayList<>();      // 邻接表
int[] inDegree = new int[n];      // 记录每个点的入度

public int[] topologicSort(int n, List<List<Integer>> edges, int[] inDegree) {
    int[] ret = new int[n];
    int num = 0;    // 记录加入拓扑排序的定点数
    Queue<Integer> q = new LinkedList<>();
    for (int i = 0; i < n; i++) {       // 入度为0的节点入队
        if (inDegree[i] == 0) q.offer(i);
    }
    while (!q.isEmpty()) {
        int u = q.poll();
        ret[num] = u;
        for (int v : edges.get(u)) {    // 取出后继节点
            inDegree[v]--;
            if (inDegree[v] == 0) {
                q.offer(v);
            }
        }
        edges.get(u).clear();   // 删除顶点u的所有出边
        num++;      // 加入拓扑排序的定点数加1
    }
    if (num != n) return new int[0];	// 排序失败
    else return ret;	// 排序成功
}
2.练习:力扣207. 课程表[1]

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。

例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

该题只需判断能否完成拓扑排序,即有没有环,解法如下:

class Solution {
    private List<List<Integer>> edges;
    private int[] inDegree;
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        edges = new ArrayList<>();          // 邻接表
        for (int i = 0; i < numCourses; i++) {
            edges.add(new ArrayList<>());
        }
        inDegree = new int[numCourses];     // 记录入度

        // 建图
        for (int[] prerequisite : prerequisites) {
            inDegree[prerequisite[0]]++;
            edges.get(prerequisite[1]).add(prerequisite[0]);
        }

        return topologicSort(numCourses);
    }

    private boolean topologicSort(int n) {
        int cnt = 0;    // 记录参加过拓扑排序的节点数量
        Queue<Integer> q = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            if (inDegree[i] == 0) {
                q.offer(i);
            }
        }
        while (!q.isEmpty()) {
            int cur = q.poll();
            cnt++;
            for (int next : edges.get(cur)) {
                inDegree[next]--;
                if (inDegree[next] == 0) {
                    q.offer(next);
                }
            }
        }
        return cnt == n;
    }
}
3.练习:210. 课程表 II[2]

现在你总共有 numCourses 门课需要选,记为 0numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai必须 先选修 bi

  • 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1]

返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组

示例 1:

输入:numCourses = 2, prerequisites = [[1,0]]
输出:[0,1]
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。

示例 2:

输入:numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
输出:[0,2,1,3]
解释:总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。

示例 3:

输入:numCourses = 1, prerequisites = []
输出:[0]

典型的拓扑排序题,在上一题基础上记录排序的结果即可:

class Solution {
    public int[] findOrder(int numsCourses, int[][] prerequisites) {
        List<List<Integer>> edges = new ArrayList<>();      // 邻接表
        for (int i = 0; i < numsCourses; i++) {
            edges.add(new ArrayList<>());
        }
        int[] inDegree = new int[numsCourses];      // 记录每个点的入度
        for (int[] prerequisite : prerequisites) {
            edges.get(prerequisite[1]).add(prerequisite[0]);
            inDegree[prerequisite[0]]++;        // 注意是入度
        }

        return topologicSort(numsCourses, edges, inDegree);
    }

    public int[] topologicSort(int n, List<List<Integer>> edges, int[] inDegree) {
        int[] ret = new int[n];
        int num = 0;    // 记录加入拓扑排序的定点数
        Queue<Integer> q = new LinkedList<>();
        for (int i = 0; i < n; i++) {       // 入度为0的节点入队
            if (inDegree[i] == 0) q.offer(i);
        }
        while (!q.isEmpty()) {
            int u = q.poll();
            ret[num] = u;
            for (int v : edges.get(u)) {    // 取出后继节点
                inDegree[v]--;
                if (inDegree[v] == 0) {
                    q.offer(v);
                }
            }
            edges.get(u).clear();   // 删除顶点u的所有出边
            num++;      // 加入拓扑排序的定点数加1
        }
        if (num != n) return new int[0];
        else return ret;
    }
}
Reference

[1]力扣207. 课程表:https://leetcode-cn.com/problems/course-schedule/

[2]力扣210. 课程表 IIhttps://leetcode-cn.com/problems/course-schedule-ii/

欢迎关注。每天分享大数据开发面经和技术。
在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值