刷题笔记之拓扑排序

拓扑排序

实现思路

拓扑排序思路如下:

  1. 找到所有入度为0的节点,加入队列
  2. 删除队列中的一个节点,并将其指向的所有节点的入度值减1
  3. 若有入度值为0的城市,加入队列
  4. 重复上述操作,直至队列为空。此时没有入度值为0的节点,要么遍历完成,要么图中有环。

拓扑排序和图的关系

直观地说就是,让你把⼀幅图「拉平」,⽽且这个「拉平」的图⾥⾯,所有箭头⽅向都是⼀致的,⽐如上图所有箭头都是朝右的。
在这里插入图片描述

在这里插入图片描述
很显然,如果⼀幅有向图中存在环,是⽆法进⾏拓扑排序的,因为肯定做不到所有箭头⽅向⼀致;反过来,如果⼀幅图是「有向⽆环图」,那么⼀定可以进⾏拓扑排序。

旅行问题

题目链接:https://www.nowcoder.com/practice/ecc6e261da68412caad810669b1e891f?tpId=196&&tqId=37210&rp=1&ru=/ta/job-code-total&qru=/ta/job-code-total/question-ranking

实现思路:

  • 首先初始化优先队列、入度数组、标记数组(用来判断某个城市是否来过)、后置城市数组,先将入度为0的城市开销、编号添加到队列,并将对应标记置为true。
  • 每次取优先队列的最小值,如果当前花费还够,则减去对应开销,并将可去城市数cnt加一。然后将对应的后置数组里的所有城市入度减一。遍历所有的城市,如果没有访问过,并且入度为0,则加入到队列。
 // 牛客 NC522 旅行Ⅰ
public int Travel (int N, int V, int[] A, Point[] list) {
    // o1[0]:该城市的编号 o1[1]:去该城市的花费
    PriorityQueue<int[]> queue = new PriorityQueue<>(
            (o1,o2) -> o1[1] == o2[1] ? o1[0] - o2[0] : o1[1] - o2[1]);
    // 标记数组
    boolean[] visited = new boolean[N];
    // 入度数组
    int[] Indeg=new int[N];
    // 后置城市数组
    int[][] limit=new int[N][N];

    // 给后置城市数组和入度数组赋值
    for (Point p : list) {
        limit[p.x-1][p.y-1] = 1;
        Indeg[p.y-1]++;
    }

    // 如果入度为0,则表示可以访问,加入到队列,并且访问标记置为true
    for (int i = 0; i < N; i++) {
        if (Indeg[i] == 0) {
            visited[i] = true;
            queue.offer(new int[]{i,A[i]});
        }
    }
    // 记录最多可去的城市
    int count = 0;
    while (!queue.isEmpty()) {
        int[] city = queue.poll();
        int id = city[0];
        int cost = city[1];
        if (V < cost) {
            break;
        }
        V -= cost;
        count++;
        //如果存在后置城市,将对应后置城市入度减一
        for (int i = 0; i < N; i++) {
            if (limit[id][i] == 1) {
                Indeg[i]--;
            }
        }
        //如果入度为0,并且没有访问过,则加入到队列,并且访问标记置为true
        for (int i = 0; i < N; i++) {
            if (!visited[i] && Indeg[i] == 0) {
                visited[i] = true;
                queue.offer(new int[]{i,A[i]});
            }
        }

    }
    return count;

}

拓扑排序在图中的应用

207 课程表

题目描述

题目链接:https://leetcode-cn.com/problems/course-schedule/

题目解析

题⽬应该不难理解,什么时候⽆法修完所有课程?当存在循环依赖的时候。

具体来说,我们⾸先可以把课程看成「有向图」中的节点,节点编号分别是 0, 1, …, numCourses-
1,把课程之间的依赖关系看做节点之间的有向边。⽐如说必须修完课程 1 才能去修课程 3,那么就有⼀条有向边从节点 1 指向 3。

所以我们可以根据题⽬输⼊的 prerequisites 数组⽣成⼀幅类似这样的图:

在这里插入图片描述
如果发现这幅有向图中存在环,那就说明课程之间存在循环依赖,肯定没办法全部上完;反之,如果没有环,那么肯定能上完全部课程。那么题目就转成判断是否有环问题了。

我们可以用邻接表存储上述结构代码如下:

List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
    List<Integer>[] graph = new LinkedList[numCourses];

    for (int i = 0; i < graph.length; i++) {
        graph[i] = new LinkedList<>();
    }
    for (int[] edge : prerequisites) {
        int from = edge[0];
        int to = edge[1];
        // 修完课程 from 才能修课程 to
        // 在图中添加⼀条从 from 指向 to 的有向边
        graph[from].add(to);
    }
    return graph;
}

然后遍历是否有环,代码如下:

// 记录⼀次 traverse 递归经过的节点
boolean[] onPath;
// 记录遍历过的节点,防⽌⾛回头路
boolean[] visited;
// 记录图中是否有环
boolean hasCycle = false;
public boolean canFinish(int numCourses, int[][] prerequisites) {
    List<Integer>[] graph = buildGraph(numCourses, prerequisites);
    visited = new boolean[numCourses];
    onPath = new boolean[numCourses];

    for (int i = 0; i < numCourses; i++) {
        traverse(graph,i);
    }
    return !hasCycle;
}

// 从节点 s 开始 BFS 遍历,将遍历过的节点标记为 true
void traverse(List<Integer>[] graph, int s) {
    if (onPath[s]) { // 有环
        hasCycle = true;
    }
    if (visited[s] || hasCycle) {
        return;
    }

    onPath[s] = true;
    visited[s] = true;
    for (Integer t : graph[s]) {
        traverse(graph,t);
    }
    onPath[s] = false;

}
总结

这道题的核⼼就是判断⼀幅有向图中是否存在环

不过如果出题⼈继续恶⼼你,让你不仅要判断是否存在环,还要返回这个环具体有哪些节点,怎么办?

你可能说,onPath ⾥⾯为 true 的索引,不就是组成环的节点编号吗?

不是的,假设下图中绿⾊的节点是递归的路径,它们在 onPath 中的值都是 true,但显然成环的节点只是其中的⼀部分:

在这里插入图片描述
那么这就需要用到拓扑排序了

201 课程表 ll

题目描述

题目链接:https://leetcode-cn.com/problems/course-schedule-ii/

题目解析

这道题就是上道题的进阶版,不是仅仅让你判断是否可以完成所有课程,⽽是 进⼀步让你返回⼀个合理的上课顺序 ,保证开始修每个课程时,前置的课程都已经修完。

其实也不难看出来,如果把课程抽象成节点,课程之间的依赖关系抽象成有向边,那么这幅图的拓扑排序结果就是上课顺序。

那么关键问题来了,如何进⾏拓扑排序?其实特别简单, 将后序遍历的结果进⾏反转,就是拓扑排序的结果。

建图函数和 207 中一样,这里直接拿来用,全部代码如下:

// 记录后序遍历结果
List<Integer> postorder = new ArrayList<>();
// 记录⼀次 traverse 递归经过的节点
boolean[] onPath;
// 记录遍历过的节点,防⽌⾛回头路
boolean[] visited;
// 记录图中是否有环
boolean hasCycle = false;

public int[] findOrder(int numCourses, int[][] prerequisites) {
    visited = new boolean[numCourses];
    onPath = new boolean[numCourses];
    List<Integer>[] graph = buildGraph(numCourses, prerequisites);

    for (int i = 0; i < numCourses; i++) {
        traverse(graph,i);
    }
    if (hasCycle) {
        return new int[]{};
    }

    Collections.reverse(postorder);
    int[] result = new int[numCourses];
    for (int i = 0; i < postorder.size(); i++) {
        result[i] = postorder.get(i);
    }
    return result;
    
}
void traverse(List<Integer>[] graph, int s) {
    if (onPath[s]) {
        hasCycle = true;
    }
    if (visited[s] || hasCycle) {
        return;
    }
    // 前序遍历位置
    onPath[s] = true;
    visited[s] = true;
    for (Integer t : graph[s]) {
        traverse(graph,t);
    }
    // 后序遍历位置
    postorder.add(s);
    onPath[s] = false;
    
}

List<Integer>[] buildGraph(int numCourses, int[][] prerequisites) {
    List<Integer>[] graph = new LinkedList[numCourses];

    for (int i = 0; i < graph.length; i++) {
        graph[i] = new LinkedList<>();
    }
    for (int[] edge : prerequisites) {
        int from = edge[1];
        int to = edge[0];
        // 修完课程 from 才能修课程 to
        // 在图中添加⼀条从 from 指向 to 的有向边
        graph[from].add(to);
    }
    return graph;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

熠熠98

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值