【算法基础:搜索与图论】3.3 拓扑排序

news2024/11/25 19:53:13

文章目录

  • 拓扑排序介绍
    • 如何构造拓扑排序(⭐重要!)
  • 例题:848. 有向图的拓扑序列
    • BFS 写法构造拓扑排序
  • 相关题目练习
    • 207. 课程表(判断是否存在拓扑序列)
      • bfs 写法
      • dfs 写法
    • 210. 课程表 II(找到一个拓扑序列)
    • 1136. 并行课程(找拓扑序列过程中记录最少学期数)
    • 2050. 并行课程 III(边带值的拓扑序列,好题!🐂)
    • 444. 序列重建(将问题转换成拓扑排序)
    • 269. 火星词典(需要考虑情况比较多的题目,需要细心)
  • 相关资料

拓扑排序介绍

https://oi-wiki.org/graph/topo/
本文主要学习拓扑排序相关知识。

拓扑排序的英文名是 Topological sorting。

拓扑排序要解决的问题是给一个 有向无环图所有节点排序

我们可以拿大学每学期排课的例子来描述这个过程,比如学习大学课程中有:程序设计,算法语言,高等数学,离散数学,编译技术,普通物理,数据结构,数据库系统等。按照例子中的排课,当我们想要学习 数据结构 的时候,就必须先学会 离散数学 和 编译技术。当然还有一个更加前的课程 算法语言。这些课程就相当于几个顶点 u, 顶点之间的有向边 (u,v) 就相当于学习课程的顺序。教务处安排这些课程,使得在逻辑关系符合的情况下排出课表,就是拓扑排序的过程。
在这里插入图片描述
但是如果某一天排课的老师打瞌睡了,说想要学习 数据结构,还得先学 操作系统,而 操作系统 的前置课程又是 数据结构,那么到底应该先学哪一个(不考虑同时学习的情况)?在这里,数据结构 和 操作系统 间就出现了一个环,显然你现在没办法弄清楚你需要学什么了,于是你也没办法进行拓扑排序了。因为如果有向图中存在环路,那么我们就没办法进行 拓扑排序。

因此我们可以说 在一个 DAG(有向无环图) 中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点 u 到 v 的有向边 (u,v), 都可以有 u 在 v 的前面。

还有给定一个 DAG,如果从 i 到 j 有边,则认为 j 依赖于 i。如果 i 到 j 有路径(i 可达 j),则称 j 间接依赖于 i。

拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。


由于有环的图没有拓扑序列,因此拓扑排序还可以判断图中是否有环。

如何构造拓扑排序(⭐重要!)

  1. 从图中选择一个入度为零的点。
  2. 输出该顶点,从图中删除此顶点及其所有的出边。

重复上面两步,直到所有顶点都输出,拓扑排序完成,或者图中不存在入度为零的点,此时说明图是有环图,拓扑排序无法完成,陷入死锁。

例题:848. 有向图的拓扑序列

在这里插入图片描述

BFS 写法构造拓扑排序

思路按照:

  1. 从图中选择一个入度为零的点。
  2. 输出该顶点,从图中删除此顶点及其所有的出边。

重复上面两步,直到所有顶点都输出,拓扑排序完成,或者图中不存在入度为零的点,此时说明图是有环图,拓扑排序无法完成,陷入死锁。

import java.util.*;

public class Main {
    static List<Integer>[] g;       // 用来建图
    static int[] in;                // 存储各个节点的入度
    static int n, m;

    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();
        g = new ArrayList[n + 1];
        in = new int[n + 1];
        Arrays.setAll(g, e -> new ArrayList<Integer>());
        for (int i = 0; i < m; ++i) {
            int a = scanner.nextInt(), b = scanner.nextInt();
            g[a].add(b);
            in[b]++;    // b 的入度 + 1
        }
        bfs();
    }

    static void bfs() {
        Queue<Integer> q = new LinkedList<>();
        boolean[] st = new boolean[n + 1];
        // 将所有初始入度 = 0 的节点放入队列
        for (int i = 1; i <= n; ++i) {
            if (in[i] == 0) {
                q.offer(i);
                st[i] = true;
            }
        }

        List<Integer> res = new ArrayList<>();
        while (!q.isEmpty()) {
            int x = q.poll();
            res.add(x);
            for (int y: g[x]) {                 // 将 x 的子节点 y 的入度 - 1
                in[y]--;
                if (!st[y] && in[y] == 0) {
                    st[y] = true;
                    q.offer(y);
                }
            }
        }
        if (res.size() == n) {                  // 如果都放进序列了,说明存在拓扑序列
            for (int v: res) System.out.print(v + " ");
        } else System.out.println("-1");;
    }
}

这道题是有向无环图,因此是可以不使用 st 数组存储节点是否已经被访问过。修改后的代码如下:

import java.util.*;

public class Main {
    static List<Integer>[] g;       // 用来建图
    static int[] in;                // 存储各个节点的入度
    static int n, m;

    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt();
        g = new ArrayList[n + 1];
        in = new int[n + 1];
        Arrays.setAll(g, e -> new ArrayList<Integer>());
        for (int i = 0; i < m; ++i) {
            int a = scanner.nextInt(), b = scanner.nextInt();
            g[a].add(b);
            in[b]++;    // b 的入度 + 1
        }
        bfs();
    }

    static void bfs() {
        Queue<Integer> q = new LinkedList<>();
        // 将所有初始入度 = 0 的节点放入队列
        for (int i = 1; i <= n; ++i) {
            if (in[i] == 0) q.offer(i);
        }

        List<Integer> res = new ArrayList<>();
        while (!q.isEmpty()) {
            int x = q.poll();
            res.add(x);
            for (int y: g[x]) {                 // 将 x 的子节点 y 的入度 - 1
                if (--in[y] == 0) q.offer(y);
            }
        }
        if (res.size() == n) {                  // 如果都放进序列了,说明存在拓扑序列
            for (int v: res) System.out.print(v + " ");
        } else System.out.println("-1");;
    }
}

相关题目练习

207. 课程表(判断是否存在拓扑序列)

https://leetcode.cn/problems/course-schedule/

在这里插入图片描述

bfs 写法

class Solution {
    List<Integer>[] g;
    int[] in;

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        g = new ArrayList[numCourses];
        Arrays.setAll(g, e -> new ArrayList<Integer>());
        in = new int[numCourses];
        for (int[] prerequisity: prerequisites) {
            g[prerequisity[0]].add(prerequisity[1]);
            in[prerequisity[1]]++;
        }
        
        int sum = 0;
        Queue<Integer> q = new LinkedList<Integer>();
        for (int i = 0; i < numCourses; ++i) {
            if (in[i] == 0) {
                q.offer(i);
                sum++;
            }
        }
        while (!q.isEmpty()) {
            int x = q.poll();
            for (int y: g[x]) {
                if (--in[y] == 0) {
                    sum++;
                    q.offer(y);
                }
            } 
        }

        return sum == numCourses;
    }
}

dfs 写法

相比于 bfs 写法, dfs 写法多开了一个 st 数组用来存储各个节点是否已经被访问过了。

class Solution {
    List<Integer>[] g;
    boolean[] st;
    int[] in;
    int sum = 0;

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        g = new ArrayList[numCourses];
        Arrays.setAll(g, e -> new ArrayList<Integer>());
        in = new int[numCourses];
        st = new boolean[numCourses];
        for (int[] prerequisity: prerequisites) {
            g[prerequisity[0]].add(prerequisity[1]);
            in[prerequisity[1]]++;
        }
        
        for (int i = 0; i < numCourses; ++i) {
            if (in[i] == 0) {
                dfs(i);
            }
        }

        return sum == numCourses;
    }

    public void dfs(int x) {
        if (st[x]) return;
        ++sum;
        st[x] = true;
        for (int y: g[x]) {
            if (--in[y] == 0) {
                dfs(y);
            }
        }
    }
}

210. 课程表 II(找到一个拓扑序列)

https://leetcode.cn/problems/course-schedule-ii/description/
在这里插入图片描述

对上一题的代码进行简单修改即可,把找到的拓扑序列存下来。

class Solution {
    List<Integer>[] g;
    List<Integer> ans = new ArrayList();
    int[] in;

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        g = new ArrayList[numCourses];
        Arrays.setAll(g, e -> new ArrayList<Integer>());
        in = new int[numCourses];
        for (int[] prerequisity: prerequisites) {
            g[prerequisity[1]].add(prerequisity[0]);
            in[prerequisity[0]]++;
        }
        
        Queue<Integer> q = new LinkedList<Integer>();
        for (int i = 0; i < numCourses; ++i) {
            if (in[i] == 0) {
                q.offer(i);
                ans.add(i);
            }
        }
        while (!q.isEmpty()) {
            int x = q.poll();
            for (int y: g[x]) {
                if (--in[y] == 0) {
                    ans.add(y);
                    q.offer(y);
                }
            } 
        }

        if (ans.size() == numCourses) return ans.stream().mapToInt(Integer::intValue).toArray();
        else return new int[]{};
    }
}

上一题是用 sum 记录可以学的课程数目,这一题是把可学的课程都放进 ans 列表中。

另外还有一道题:630. 课程表 III,但是和 拓扑排序 没什么关系。

1136. 并行课程(找拓扑序列过程中记录最少学期数)

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

在这里插入图片描述

在 bfs 的过程中记录走了几步即可。

class Solution {
    List<Integer>[] g;
    int[] in;

    public int minimumSemesters(int n, int[][] relations) {
        in = new int[n + 1];
        g = new ArrayList[n + 1];
        Arrays.setAll(g, e -> new ArrayList());
        for (int[] r: relations) {
            g[r[0]].add(r[1]);
            in[r[1]]++;
        }

        int ans = 0, sum = 0;           // ans记录答案,sum记录学了几个课程
        Queue<Integer> q = new LinkedList();
        for (int i = 1; i <= n; ++i) {
            if (in[i] == 0) {
                q.offer(i);
                sum++;
            }
        }
        while (!q.isEmpty()) {
            int sz = q.size();
            for (int i = 0; i < sz; ++i) {
                int x = q.poll();
                for (int y: g[x]) {
                    if (--in[y] == 0) {
                        ++sum;
                        q.offer(y);
                    }
                }
            }
            ++ans;
        }

        return sum == n? ans: -1;
    }
}

还有一道题目1494. 并行课程 II 可见:从集合论到位运算——常见位运算技巧及相关习题 & 状态压缩DP

2050. 并行课程 III(边带值的拓扑序列,好题!🐂)

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

在这里插入图片描述

是 https://leetcode.cn/problems/parallel-courses/ 的进阶版,学完每门课程的需要花费的时间是不一样的。

借助优先队列,每次取出队列中最先完成的课程,检查完成这个课程后是否有新的课程可以开始学习即可。

最终的答案就是完成最后一门课程时的时间。

class Solution {
    List<Integer>[] g;
    int[] in;

    public int minimumTime(int n, int[][] relations, int[] time) {
        in = new int[n + 1];
        g = new ArrayList[n + 1];
        Arrays.setAll(g, e -> new ArrayList());
        for (int[] r: relations) {
            g[r[0]].add(r[1]);
            in[r[1]]++;
        }

        int ans = 0;           // ans记录答案
        // 按照完成时间升序排序的优先队列
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> a[1] - b[1]);
        for (int i = 1; i <= n; ++i) {
            if (in[i] == 0) {
                pq.offer(new int[]{i, time[i - 1]});
            }
        }
        while (!pq.isEmpty()) {
            int[] cur = pq.poll();
            ans = cur[1];
            for (int y: g[cur[0]]) {
                if (--in[y] == 0) {
                    pq.offer(new int[]{y, ans + time[y - 1]});
                }
            }
        }
        return ans;
    }
}

444. 序列重建(将问题转换成拓扑排序)

https://leetcode.cn/problems/sequence-reconstruction/
在这里插入图片描述

题目数据保证了所有 sequences[i] 都是 nums 的子序列,因此 nums 一定是一个超序列,如果序列有唯一一个超序列,那么这一唯一的超序列一定是 nums

在构造拓扑序列的过程中,如果每次只有一个节点的入度是 0 ,那么构造的拓扑序列就是唯一的,则答案返回 true,否则中间过程一旦检查到同时有两个节点的入度变成了 0,那么就返回 false。

class Solution {
    public boolean sequenceReconstruction(int[] nums, List<List<Integer>> sequences) {
        int n = nums.length;
        List<Integer>[] g = new ArrayList[n + 1];
        int[] in = new int[n + 1];
        Arrays.setAll(g, e -> new ArrayList<Integer>()); 
        for (List<Integer> s: sequences) {
            for (int i = 0; i < s.size() - 1; i++) {
                g[s.get(i)].add(s.get(i + 1));
                in[s.get(i + 1)]++;
            }
        }

        Queue<Integer> q = new LinkedList<Integer>();
        boolean f = false;
        int sum = 0;
        for (int i = 1; i <= n; ++i) {
            if (in[i] == 0) {
                ++sum;
                if (f) return false;
                f = true;
                q.offer(i);
            }
        }

        while (!q.isEmpty()) {
            if (q.size() > 1) return false;
            int x = q.poll();
            for (int y: g[x]) {
                if (--in[y] == 0) {
                    q.offer(y);
                    sum++;
                }
            }
        }

        return sum == n;
    }
}

269. 火星词典(需要考虑情况比较多的题目,需要细心)

https://leetcode.cn/problems/alien-dictionary/

在这里插入图片描述

提示:

1 <= words.length <= 100
1 <= words[i].length <= 100
words[i] 仅由小写英文字母组成

这题比较麻烦,有多个情况需要考虑。

class Solution {
    public String alienOrder(String[] words) {
        Set<Character>[] g = new HashSet[128];
        Arrays.setAll(g, e -> new HashSet<Character>());
        int n = words.length;
        int[] in = new int[128];
        Set<Character> letters = new HashSet();
        // 记录所有出现的字符
        for (String word: words) {
            for (char ch: word.toCharArray()) letters.add(ch);
        }
        // 构造字符之间的顺序关系
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                String a = words[i], b = words[j];
                for (int k = 0; k < a.length(); ++k) {
                    if (k >= b.length()) return "";         // 前面的字符都一样但长的排在前面了不合理
                    char x = a.charAt(k), y = b.charAt(k);
                    if (x != y) {     
                        if (g[y].contains(x)) return "";    // 和之前的顺序冲突了
                        if (!g[x].contains(y)) {
                            g[x].add(y);
                            in[y]++;
                        }
                        break;
                    }
                }
            }
        }

        // bfs 寻找拓扑序列
        StringBuilder ans = new StringBuilder();
        Queue<Character> q = new LinkedList<Character>();
        for (char letter: letters) {
            if (in[letter] == 0) {
                q.offer(letter);
                ans.append(letter);
            }
        }

        while (!q.isEmpty()) {
            char x = q.poll();
            for (char y: g[x]) {
                if (--in[y] == 0) {
                    q.offer(y);
                    ans.append(y);
                }
            }
        }

        return ans.toString();
    }
}

相关资料

https://oi-wiki.org/graph/topo/

更多关于拓扑排序的题目可见:https://leetcode.cn/tag/topological-sort/problemset/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/778680.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Packet Tracer – 实施静态 NAT 和动态 NAT

Packet Tracer – 实施静态 NAT 和动态 NAT 拓扑图 目标 第 1 部分&#xff1a;利用 PAT 配置动态 NAT 第 2 部分&#xff1a;配置静态 NAT 第 3 部分&#xff1a;验证 NAT 实施 第 1 部分&#xff1a; 利用 PAT 配置动态 NAT 步骤 1&#xff1a; 配置允许用于 NAT …

transformer 笔记

目录 目前在NLP领域当中&#xff0c;主要存在三种特征处理器——CNN、RNN 以及 Transformer&#xff0c;当前Transformer的流行程度已经大过CNN和RNN&#xff0c;它抛弃了传统CNN和RNN神经网络&#xff0c;整个网络结构完全由Attention机制以及前馈神经网络组成。 Transformer…

K8s Service网络详解(二)

Kube Proxy Kubernetes 在设计之初就充分考虑了针对容器的服务发现与负载均衡机制。 Service 资源&#xff0c;可以通过 kube-proxy 配合 cloud provider 来适应不同的应用场景。 Service相关的事情都由Node节点上的 kube-proxy处理。在Service创建时Kubernetes会分配IP给Ser…

Flask 定制日志并输出到文件

Flask 定制日志并输出到文件 定制日志器flask缺省日志器配置自定义日志器 定制日志器 flask缺省日志器配置 flask自带的日志系统&#xff0c;缺省配置dictConfig()&#xff0c;但必须在Flask()应用之前使用 # flask缺省配置 from logging.config import dictConfig dictConfig…

Spring MVC-基础概念(定义+创建和连接+@RequestMappring的描述)

目录 1.什么是Spring MVC&#xff1f; 2. MVC 和 Spring MVC 的关系 3.Spring MVC 项目创建 4. RequestMappring实现用户和程序的映射 4.1 RequestMappring 注解解释 4.2 方法1: RequestMapping(“/xxx”) 4.4 RequestMapping(method xxxx, value “xxx”) 是POST/GET…

欧姆龙CX系列PLC串口转以太网欧姆龙cp1hplc以太网连接电脑

你是否还在为工厂设备信息采集困难而烦恼&#xff1f;捷米特JM-ETH-CX转以太网通讯处理器为你解决这个问题&#xff01; 捷米特JM-ETH-CX转以太网通讯处理器专门为满足工厂设备信息化需求而设计&#xff0c;可以用于欧姆龙多个系列PLC的太网数据采集&#xff0c;非常方便构建生…

请用Typescript写出20个数组方法的声明

前言 前段时间看直播看到狼叔直播驳斥”前端已死论“&#xff0c;前端死没死不知道&#xff0c;反正前端是拿不到以前那么多工资了&#xff1b;好&#xff0c;进入正题&#xff0c;狼叔在直播间提到要求前端写出20个数组上的方法&#xff0c;这确实不太简单&#xff0c;但是只…

【CSharp】关于xxx.csproj文件的理解

【CSharp】关于xxx.csproj文件的理解 1、背景2 关于.csproj 文件 1、背景 CShape又简写C#。 在示例代码里&#xff0c;遇到.csproj 文件。 项目结构如下&#xff1a; 本博客属于小白入门级。 2 关于.csproj 文件 上面的iRayBase.csproj 文件后缀是 .csproj 。 csproj的全称…

框架漏洞-CVE复现-Apache Shiro+Apache Solr

什么是框架&#xff1f; 就是别人写好包装起来的一套工具&#xff0c;把你原先必须要写的&#xff0c;必须要做的一些复杂的东西都写好了放在那里&#xff0c;你只要调用他的方法&#xff0c;就可以实现一些本来要费好大劲的功能。 如果网站的功能是采用框架开发的&#xff0c;…

typescript自动编译文件实时更新

npm install -g typescripttsc --init 生成tsconfig.json配置文件 tsc -w 在监听模式下运行&#xff0c;当文件发生改变的时候自动编译

【数学建模快速入门】

MD5码 生成了MD5码之后就不可以再去碰文件了&#xff08;打开都不行&#xff09;百度搜索 1、查询词的外边加上双引号“” 2、在查询词的前面加上&#xff1a;intitle: 3、查询词后面加上空格再输入filetype&#xff1a;文件格式&#xff08;doc/pdf/xls&#xff09; 4、在3的…

React+Redux 数据存储持久化

ReactRedux 数据存储持久化 1、安装相关依赖 yarn add reduxjs/toolkit redux redux-persist 2、userSlice&#xff1a;用户状态数据切片封装 import { createSlice, PayloadAction } from reduxjs/toolkitinterface IUserInfo {userName: stringavatar?: stringbrief?: st…

第111天:免杀对抗-JavaASM汇编CS调用内联CMSF源码特征修改Jar打包

知识点 #知识点&#xff1a; 1、ASM-CS-单汇编&内联C 2、JAVA-MSF-源码修改&打包#章节点&#xff1a; 编译代码面-ShellCode-混淆 编译代码面-编辑执行器-编写 编译代码面-分离加载器-编写 程序文件面-特征码定位-修改 程序文件面-加壳花指令-资源 代码加载面-Dll反射…

基于linux下的高并发服务器开发(第三章)- 3.6 线程取消

#include <pthread.h> int pthread_cancel(pthread_t thread);- 功能&#xff1a;取消线程&#xff08;让线程终止&#xff09;取消某个线程&#xff0c;可以终止某个线程的运行&#xff0c;但是并不是立马终止&#xff0c;而是当子线程执行到一个取消点&#xff0c;线程…

GOT Online|解密游戏性能优化秘籍

随着UWA GOT Online功能的不断迭代&#xff0c;GOT Online为解决各种游戏性能问题&#xff08;如内存占用、CPU耗时、GPU耗时和卡顿&#xff09;提供了丰富的高效、准确且便捷的数据获取方式和分析建议。本文总结了GOT Online&#xff08;SDK 2.4.7版本&#xff09;中的关键优化…

fps php,帧率60帧是什么意思

帧率60的意思是每秒屏幕刷新60次&#xff0c;帧率是用于测量显示帧数的量度。所谓的测量单位为每秒显示帧数即Frames per Second&#xff0c;简称FPS或“赫兹”&#xff0c;此词多用于影视制作和电子游戏。 本文操作环境&#xff1a;Windows7系统&#xff0c;Dell G3电脑。 帧…

计算机网络模型

计算机网络模型 网络模型网络模型中各层对应的协议封装与分用TCP/IP协议簇的组成 网络模型 OSI 七层模型 应用层、表示层、会话层、传输层、网络层、数据链路层、物理层 TCP/IP四层模型 应用层、传输层、网络层、网络接口层 TCP/IP五层模型 应用层、传输层、网络层、数据链路…

【SpringCloud Alibaba】(二)微服务环境搭建

1. 项目流程搭建 整个项目主要分为 用户微服务、商品微服务和订单微服务&#xff0c;整个过程模拟的是用户下单扣减库存的操作。这里&#xff0c;为了简化整个流程&#xff0c;将商品的库存信息保存到了商品数据表&#xff0c;同时&#xff0c;使用商品微服务来扣减库存。小伙…

【简单认识MySQL主从复制与读写分离】

文章目录 一、MySQL主从复制1、配置主从复制的原因&#xff1a;2、主从复制原理1、 MySQL的复制类型2、 MySQL主从复制的工作过程;1、 MySQL主从复制延迟2、优化方案&#xff1a;3、 MySQL 有几种同步方式&#xff1a; 三种4、异步复制&#xff08;Async Replication&#xff0…

【初始C语言】多种输入格式的优劣

多种输入格式的优劣【初始C语言】 一.多种输入格式的不同&#xff08;只针对输入字符&#xff0c;字符串&#xff09;1.scanf&#xff08;"%s",字符数组名&#xff09;2.scanf("%[^\n]s",字符数组名)3.gets(字符数组名)4.fgets&#xff08;字符数组名,规定…