当前位置: 首页 > news >正文

LeetCode算法日记 - Day 48: 课程表II、火星词典

目录

1. 课程表II

1.1 题目解析

1.2 解法

1.3 代码实现

2. 火星词典

2.1 题目解析

2.2 解法

2.3 代码实现


1. 课程表II

https://leetcode.cn/problems/course-schedule-ii/submissions/664409369/

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 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]

提示:

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= numCourses * (numCourses - 1)
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • ai != bi
  • 所有[ai, bi] 互不相同

1.1 题目解析

题目本质
这是一个“课程依赖 → 有向图的拓扑排序”问题。每个课程是节点,依赖 bi→ai 是有向边;要找一条满足所有依赖方向的线性序列(如果图有环则不存在)。

常规解法(直观想法)
“每次挑选所有前置都学完的课”,把它们加入结果,再继续挑选下一批。
如果用朴素做法,每轮都在全图里扫“谁的前置都完成”,时间会很大。

问题分析(为何直观不行)

  • 朴素地“每轮全表扫描”找可学课程,最坏要扫很多轮,复杂度可达 O(n2+m)O(n^2+m)O(n2+m)(n=课程数,m=依赖数)。

  • 同时还不好判断是否存在环(即无法完成)。

思路转折
为高效实现“可学课程”的筛选,必须预处理入度并用队列维护“当前入度为 0 的节点集合”。
这就是 Kahn 算法(BFS 拓扑排序)

  • 入度为 0 ⇒ 没有未满足的前置 ⇒ 可以立刻学习;

  • 学完就“删除出边”(后继入度减 1),新变成 0 的进队;

  • 最后如果处理数量 < n,则有环,返回空。

1.2 解法

算法思想

  • 用入度 in[x] 表示课程 x 还有多少门前置未完成;

  • 初始把所有 in[x]==0 的课程入队;

  • 每次出队一个课程 cur,将它放进结果,并把它指向的课程的入度都减一;减到 0 的再入队;

  • 处理完的数量 index 等于 n 则有解,否则有环返回空数组。

正确性要点:只有当一个节点所有前驱都已出队(入结果)后,它的入度才会降为 0 并被入队,因此结果序列必然满足所有边的方向要求(拓扑序定义)。

i)建图 + 入度:对每条依赖 [a,b] 建边 b → a,并 in[a]++。

ii)初始化队列:把所有 in[i]==0 的节点入队。

iii)BFS 出队

  • cur = q.poll() 写入 result[index++] = cur;

  • 遍历 cur 的后继 x,做 in[x]--;若变 0 则 q.offer(x)。

iv)验环:若 index != n,说明仍有结点入度 > 0(存在环),返回空数组。

v)返回结果:长度为 n 的 result 即任意一组可行的学习顺序。

易错点

  • 返回类型:不能 Arrays.asList();无解应返回 new int[0]。

  • 写入时机:Kahn 法在出队时写入结果(result[index++]=cur),不是入队时。

  • 判环方式:用“已处理计数 index==n”判断

  • 孤立点/无依赖:prerequisites 为空时,所有课都应在初始队列里,结果是 0..n-1。

1.3 代码实现

import java.util.*;class Solution {public int[] findOrder(int n, int[][] p) {int[] in = new int[n];                 // 入度int[] result = new int[n];             // 结果序列Map<Integer, List<Integer>> edges = new HashMap<>(); // 邻接表:b -> [a1,a2...]// 1) 建图 + 入度统计(b -> a,表示先修 b 再修 a)for (int i = 0; i < p.length; i++) {int a = p[i][0], b = p[i][1];edges.computeIfAbsent(b, k -> new ArrayList<>()).add(a);in[a]++;}// 2) 入度为 0 的课程先入队Deque<Integer> q = new ArrayDeque<>();for (int i = 0; i < n; i++) {if (in[i] == 0) q.offer(i);}// 3) Kahn:不断出队,写入结果,并“删除”出边(后继入度--)int index = 0;while (!q.isEmpty()) {int cur = q.poll();result[index++] = cur; // 出队即进入拓扑序for (int next : edges.getOrDefault(cur, Collections.emptyList())) {if (--in[next] == 0) q.offer(next);}}// 4) 验环:若未处理满 n 个课程,说明存在环(无法完成)if (index != n) return new int[0];// 5) 返回任意一种合法拓扑序return result;}
}

复杂度分析

  • 时间复杂度:O(n + m),因为每个课程最多入队出队一次,每条依赖边最多被遍历一次。

  • 空间复杂度:O(n + m),因为需要存储入度数组、结果数组、队列以及邻接表。

2. 火星词典

https://leetcode.cn/problems/Jf1JuT/

现有一种使用英语字母的外星文语言,这门语言的字母顺序与英语顺序不同。

给定一个字符串列表 words ,作为这门语言的词典,words 中的字符串已经 按这门新语言的字母顺序进行了排序 。

请你根据该词典还原出此语言中已知的字母顺序,并 按字母递增顺序 排列。若不存在合法字母顺序,返回 "" 。若存在多种可能的合法字母顺序,返回其中 任意一种 顺序即可。

字符串 s 字典顺序小于 字符串 t 有两种情况:

  • 在第一个不同字母处,如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前,那么 s 的字典顺序小于 t 。
  • 如果前面 min(s.length, t.length) 字母都相同,那么 s.length < t.length 时,s 的字典顺序也小于 t 。

示例 1:

输入:words = ["wrt","wrf","er","ett","rftt"]
输出:"wertf"

示例 2:

输入:words = ["z","x"]
输出:"zx"

示例 3:

输入:words = ["z","x","z"]
输出:""
解释:不存在合法字母顺序,因此返回 ""。

提示:

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

2.1 题目解析

题目本质
把单词表中相邻词的“首个不同字符的相对先后关系”抽象为有向图的边,问题即“在字符集合上做一次拓扑排序”,得到一种可行的字母顺序;若有环或出现非法前缀顺序,则不存在解。

常规解法
直接比较所有成对单词提取关系,再用 BFS/DFS 随便拼一个顺序。

问题分析
比较所有成对单词会产生冗余边,且不必要;若用 DFS 需要额外显式的状态标记与回溯,代码更易错。更稳妥的做法是只比较相邻单词,保证边的最小充分集,并用 Kahn 拓扑排序按入度推进。时间规模由单词总字符数 Σ 决定,字符种类最多 26。

思路转折
要想高效与严谨

  • 必须仅比较相邻词,抽取“首个不同字符”的一条有向边

  • 必须先把所有出现过的字符入图并初始化入度为 0

  • 必须在相邻比较处做前缀非法性判定(如 abc 在 ab 前面)

  • 之后用 Kahn 拓扑:从入度为 0 的字符开始层层剥离,若结束后仍有入度不为 0,说明有环,无解

2.2 解法

算法思想

  • 用所有出现过的字符作为节点

  • 比较相邻单词 a 和 b,定位首个不同字符 x 与 y,建立边 x → y,并给 y 的入度 +1

  • 若 a 是 b 的前缀但更长,则无解

  • 用队列收集所有入度为 0 的节点,反复出队并“削边”,同时把新的入度为 0 的节点入队

  • 输出序列长度若小于节点数,说明存在环,无解

i)初始化

  • 扫描 words 中每个字符,加入图节点集合,入度表置 0,邻接表置空集合

ii)建边

  • 依次比较相邻的 words[i] 与 words[i+1]

  • 自左到右找首个不同字符 x 与 y,若存在则插入边 x → y(用 Set 去重),并将 y 的入度加一

  • 若不存在不同字符且前者长度大于后者,返回空串

iii)拓扑排序(Kahn)

  • 将所有入度为 0 的字符入队

  • 循环出队 u,加入结果,对每个 u 的出边 v,把 v 入度减一,若变为 0 则入队

iv)校验与返回

  • 若结果长度等于节点数,返回结果,否则返回空串

易错点

  • 只比较相邻词,避免冗余与矛盾边

  • 前缀非法性要在相邻比较阶段立即判定

  • 加边时务必去重,否则会错误地多次增加入度

  • 结果长度需与不同字符总数比对,检测环

2.3 代码实现

import java.util.*;class Solution {// 邻接表:字符 -> 它后面能直接到达的字符集合(有向边 u -> v)Map<Character, Set<Character>> edges = new HashMap<>();// 入度表:字符 -> 入度值Map<Character, Integer> in = new HashMap<>();// 非法标记:遇到 "abc" 在 "ab" 前面这种前缀非法顺序时置为 trueboolean flag = false;public String alienOrder(String[] words) {// 1) 初始化所有出现过的字符:入度置 0,邻接表放空 Set,避免后续判空分支for (String w : words) {for (int i = 0; i < w.length(); i++) {char c = w.charAt(i);in.putIfAbsent(c, 0);edges.putIfAbsent(c, new HashSet<>());}}// 2) 只比较相邻单词提取“首个不同字符”的约束边(更小且充分的边集)for (int i = 0; i + 1 < words.length; i++) {add(words[i], words[i + 1]); // 在 add 里做前缀非法性判定与加边if (flag) return "";         // 发现非法前缀,直接无解}// 3) Kahn 拓扑排序:从入度为 0 的字符开始层层“削边”Queue<Character> q = new LinkedList<>();for (char ch : in.keySet()) {if (in.get(ch) == 0) q.offer(ch);}StringBuffer ret = new StringBuffer();while (!q.isEmpty()) {char cur = q.poll();ret.append(cur);// 当前点可能没有出边;如果没有,edges.get(cur) 是空 Set(已在初始化阶段 putIfAbsent)for (char nxt : edges.get(cur)) {in.put(nxt, in.get(nxt) - 1); // 削去一条入边if (in.get(nxt) == 0) q.offer(nxt);}}// 4) 若存在环,说明最终拓扑序长度 < 节点总数(不同字符总数)return ret.length() == in.size() ? ret.toString() : "";}// 只基于相邻单词 si、sj 抽取一条“首个不同字符”的有向边 u -> v// 规则:// - 从左到右找到首个不同字符 c1 != c2,则 edges[c1].add(c2),并给 c2 入度 +1(注意去重)// - 否则(前缀相同),如果 si 比 sj 更长(如 "abc" 在 "ab" 前),则非法,flag = truepublic void add(String si, String sj) {int n = Math.min(si.length(), sj.length());int i = 0;for (; i < n; i++) {char c1 = si.charAt(i), c2 = sj.charAt(i);if (c1 != c2) {if(!edges.containsKey(c1)){edges.put(c1, new HashSet<>());}// 去重加边:避免重复加同一条边导致入度被多次加 1if (!edges.get(c1).contains(c2)) {edges.get(c1).add(c2);in.put(c2, in.get(c2) + 1);}return; // 抽到一条边即可返回;相邻单词只贡献第一处差异的约束}}// 能走到这里说明前 n 位完全相同;若前者更长则非法(前缀顺序错误)if (si.length() > sj.length()) flag = true;// 否则相等或更短是合法的(不产生新边)}
}

复杂度分析

  • 时间复杂度 O(Σ + V + E),Σ 为所有单词的总字符数,V ≤ 26,E ≤ 26×26

  • 空间复杂度 O(V + E)

http://www.dtcms.com/a/392177.html

相关文章:

  • 【面板数据】地级市中国方言多样性指数数据集
  • C++编程学习(第35天)
  • SS443A 霍尔效应传感器:高性能磁感应解决方案
  • MIT新论文:数据即上限,扩散模型的关键能力来自图像统计规律,而非复杂架构
  • GitHub 热榜项目 - 日榜(2025-09-20)
  • 怎么判断 IP是独享的
  • Linux多进程编程(上)
  • 如何在Spring Boot项目中添加自定义的配置文件?
  • 【MySQL初阶】01-MySQL服务器和客户端下载与安装
  • AI搜索的下一站:多模态、个性化与GEO的道德指南
  • OpenLayers地图交互 -- 章节四:修改交互详解
  • Gradle插件的分析与使用
  • 如何避免everything每次都重建索引
  • 基于SIFT+flann+RANSAC+GTM算法的织物图像拼接matlab仿真,对比KAZE,SIFT和SURF
  • 笔记:现代操作系统:原理与实现(3)
  • 【智能系统项目开发与学习记录】Docker 基础
  • 数据展示方案:Prometheus+Grafana+JMeter 备忘
  • flask获取ip地址各种方法
  • 17.6 LangChain多模态实战:语音图像文本融合架构,PPT生成效率提升300%!
  • MyBatis实战教程:SQL映射与动态查询技巧
  • 在 Windows Docker 中通过 vLLM 镜像启动指定大模型的方法与步骤
  • 分类预测 | Matlab实现SSA-BP麻雀搜索算法优化BP神经网络多特征分类预测
  • GO实战项目:基于 `HTML/CSS/JS + Gin + Gorm + 文心一言API`AI 备忘录应用
  • 数据结构【堆(⼆叉树顺序结构)和⼆叉树的链式结构】
  • 我爱学算法之—— 位运算(下)
  • LeetCode第364题_加权嵌套序列和II
  • 云计算和云手机之间的关系
  • 胡服骑射对中国传统文化的影响
  • leetcode-hot-100 (多维动态规划)
  • Chromium 138 编译指南 Ubuntu 篇:depot_tools安装与配置(三)