第 16 站 · 图论
拓扑排序与依赖关系
Kahn 算法:入度为 0 的先出队 O(V + E)
先做这道
LC 207课程表卡住再看
把课程先修关系建为有向图,通过拓扑排序检测环。
要点
- 入度为 0 的点没有未完成的前置依赖,可以先放;放下之后把它指向的点入度减一。
- 输出的点数少于 n,说明剩下的点都在环上:「能不能修完所有课」就是这样判断的。
- 要求字典序最小的拓扑序时,把队列换成小根堆。
易错点
- 边的方向读反(「a 依赖 b」应该建 b → a),整个顺序就倒过来了。
- 只把一开始入度为 0 的点入队,后面减到 0 的点忘了入队。
- DFS 写拓扑排序时只有「访问过 / 没访问」两种状态是判不出环的,还需要「访问中」这第三种状态。
看动画
一步一步播放这个模式在小例子上的执行过程。可以暂停、单步前进或后退,拖动进度条跳到任意一步。
拓扑排序 · Kahn 算法
按先修关系给 6 门课排一个可行的学习顺序
换个输入
- 输出顺序
- —
箭头 X → Y 表示先修 X 才能修 Y。入度为 0 的 A、B 没有先修要求,先入队。
始终成立队列里只放入度为 0 的节点:它的先修都已输出,现在输出不会违反任何边。
模板
选一种语言展开。四种写法实现的是同一个模板,每一份都编译运行过,并用测试数据核对了输出。看完用它重写热身题 LC 207。
Python
from collections import deque
def topo_order(n: int, edges: list[tuple[int, int]]) -> list[int]:
"""Edge (u, v) means u comes before v. Returns [] if there is a cycle."""
graph = [[] for _ in range(n)]
indeg = [0] * n
for u, v in edges:
graph[u].append(v)
indeg[v] += 1
q = deque(i for i in range(n) if indeg[i] == 0)
order = []
while q:
u = q.popleft()
order.append(u)
for v in graph[u]:
indeg[v] -= 1
if indeg[v] == 0: # every prerequisite of v is placed
q.append(v)
return order if len(order) == n else []C++
// Edge {u, v} means u comes before v. Returns {} if there is a cycle.
vector<int> topoOrder(int n, const vector<pair<int, int>>& edges) {
vector<vector<int>> graph(n);
vector<int> indeg(n, 0);
for (auto [u, v] : edges) {
graph[u].push_back(v);
indeg[v]++;
}
queue<int> q;
for (int i = 0; i < n; i++)
if (indeg[i] == 0) q.push(i);
vector<int> order;
while (!q.empty()) {
int u = q.front();
q.pop();
order.push_back(u);
for (int v : graph[u])
if (--indeg[v] == 0) q.push(v); // every prerequisite of v is placed
}
if ((int)order.size() < n) order.clear();
return order;
}Java
class Solution {
// Edge {u, v} means u comes before v. Returns an empty list if there is a cycle.
List<Integer> topoOrder(int n, int[][] edges) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
int[] indeg = new int[n];
for (int[] e : edges) {
graph.get(e[0]).add(e[1]);
indeg[e[1]]++;
}
Deque<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; i++) if (indeg[i] == 0) q.offer(i);
List<Integer> order = new ArrayList<>();
while (!q.isEmpty()) {
int u = q.poll();
order.add(u);
for (int v : graph.get(u))
if (--indeg[v] == 0) q.offer(v); // every prerequisite of v is placed
}
return order.size() == n ? order : new ArrayList<>();
}
}Go
// topoOrder takes edges {u, v} meaning u comes before v; it returns nil if there is a cycle.
func topoOrder(n int, edges [][2]int) []int {
graph := make([][]int, n)
indeg := make([]int, n)
for _, e := range edges {
graph[e[0]] = append(graph[e[0]], e[1])
indeg[e[1]]++
}
var q []int
for i, d := range indeg {
if d == 0 {
q = append(q, i)
}
}
order := make([]int, 0, n)
for len(q) > 0 {
u := q[0]
q = q[1:]
order = append(order, u)
for _, v := range graph[u] {
indeg[v]--
if indeg[v] == 0 { // every prerequisite of v is placed
q = append(q, v)
}
}
}
if len(order) < n {
return nil
}
return order
}常见变体
这一站的题还会用到下面 2 种写法。展开看什么时候换用它,代码同样四种语言都跑过测试。
DFS 三色标记:灰色是当前路径时间 O(V + E) · 空间 O(V)
用 DFS 判环或找「走不到环」的点:白色没访问,灰色在当前递归路径上,黑色已经确认安全;DFS 时碰到灰色就是回到了路径上,也就是有环。LC 802 找到最终的安全状态、LC 207 课程表的 DFS 写法。
Python
def eventual_safe_nodes(graph: list[list[int]]) -> list[int]:
"""Nodes every path from which ends at a terminal node: no cycle is reachable (LC 802)."""
WHITE, GRAY, BLACK = 0, 1, 2 # unvisited / on the current DFS path / finished and safe
color = [WHITE] * len(graph)
def safe(u: int) -> bool:
if color[u] != WHITE:
return color[u] == BLACK # GRAY: back on the current path, so a cycle
color[u] = GRAY
if not all(safe(v) for v in graph[u]):
return False # stays GRAY: on a cycle or leading to one
color[u] = BLACK
return True
return [u for u in range(len(graph)) if safe(u)]C++
// Nodes every path from which ends at a terminal node: no cycle is reachable (LC 802).
vector<int> eventualSafeNodes(const vector<vector<int>>& graph) {
enum { WHITE, GRAY, BLACK }; // unvisited / on the current DFS path / finished and safe
vector<int> color(graph.size(), WHITE);
function<bool(int)> safe = [&](int u) {
if (color[u] != WHITE) return color[u] == BLACK; // GRAY: back on the current path, so a cycle
color[u] = GRAY;
for (int v : graph[u])
if (!safe(v)) return false; // stays GRAY: on a cycle or leading to one
color[u] = BLACK;
return true;
};
vector<int> res;
for (int u = 0; u < (int)graph.size(); u++)
if (safe(u)) res.push_back(u);
return res;
}Java
class Solution {
private static final int WHITE = 0, GRAY = 1, BLACK = 2; // unvisited / on the current DFS path / finished and safe
private int[][] graph;
private int[] color;
// Nodes every path from which ends at a terminal node: no cycle is reachable (LC 802).
List<Integer> eventualSafeNodes(int[][] graph) {
this.graph = graph;
color = new int[graph.length];
List<Integer> res = new ArrayList<>();
for (int u = 0; u < graph.length; u++)
if (safe(u)) res.add(u);
return res;
}
private boolean safe(int u) {
if (color[u] != WHITE) return color[u] == BLACK; // GRAY: back on the current path, so a cycle
color[u] = GRAY;
for (int v : graph[u])
if (!safe(v)) return false; // stays GRAY: on a cycle or leading to one
color[u] = BLACK;
return true;
}
}Go
// eventualSafeNodes returns the nodes every path from which ends at a terminal node (LC 802).
func eventualSafeNodes(graph [][]int) []int {
const (
white = iota // unvisited
gray // on the current DFS path
black // finished and safe
)
color := make([]int, len(graph))
var safe func(u int) bool
safe = func(u int) bool {
if color[u] != white {
return color[u] == black // gray: back on the current path, so a cycle
}
color[u] = gray
for _, v := range graph[u] {
if !safe(v) {
return false // stays gray: on a cycle or leading to one
}
}
color[u] = black
return true
}
res := []int{}
for u := range graph {
if safe(u) {
res = append(res, u)
}
}
return res
}拓扑序上做 DP:最长路径 / 最少学期时间 O(V + E) · 空间 O(V + E)
依赖关系构成 DAG、要算「最早什么时候能做完」「最长链有多长」:按 Kahn 的出队顺序推进,每处理一条边 u→v 就用 term[u] + 1(或加上 u 的耗时)更新 term[v]。LC 1136 并行课程;LC 2050 并行课程 III 把 +1 换成课程时长。
Python
from collections import deque
def min_semesters(n: int, relations: list[tuple[int, int]]) -> int:
"""Courses 1..n, (a, b): a before b, any number per term. Fewest terms, or -1 on a cycle (LC 1136)."""
graph = [[] for _ in range(n + 1)]
indeg = [0] * (n + 1)
for a, b in relations:
graph[a].append(b)
indeg[b] += 1
term = [1] * (n + 1) # term[v]: the earliest term v can be taken
q = deque(v for v in range(1, n + 1) if indeg[v] == 0)
done = 0
while q:
u = q.popleft()
done += 1
for v in graph[u]:
term[v] = max(term[v], term[u] + 1) # DP in topological order: u is final when it leaves
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return max(term[1:]) if done == n else -1C++
// Courses 1..n, {a, b}: a before b, any number per term. Fewest terms, or -1 on a cycle (LC 1136).
int minSemesters(int n, const vector<pair<int, int>>& relations) {
vector<vector<int>> graph(n + 1);
vector<int> indeg(n + 1, 0), term(n + 1, 1); // term[v]: the earliest term v can be taken
for (auto [a, b] : relations) {
graph[a].push_back(b);
indeg[b]++;
}
queue<int> q;
for (int v = 1; v <= n; v++)
if (indeg[v] == 0) q.push(v);
int done = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
done++;
for (int v : graph[u]) {
term[v] = max(term[v], term[u] + 1); // DP in topological order: u is final when it leaves
if (--indeg[v] == 0) q.push(v);
}
}
return done == n ? *max_element(term.begin() + 1, term.end()) : -1;
}Java
class Solution {
// Courses 1..n, {a, b}: a before b, any number per term. Fewest terms, or -1 on a cycle (LC 1136).
int minSemesters(int n, int[][] relations) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i <= n; i++) graph.add(new ArrayList<>());
int[] indeg = new int[n + 1], term = new int[n + 1]; // term[v]: the earliest term v can be taken
Arrays.fill(term, 1);
for (int[] r : relations) {
graph.get(r[0]).add(r[1]);
indeg[r[1]]++;
}
Deque<Integer> q = new ArrayDeque<>();
for (int v = 1; v <= n; v++)
if (indeg[v] == 0) q.offer(v);
int done = 0, best = 0;
while (!q.isEmpty()) {
int u = q.poll();
done++;
best = Math.max(best, term[u]);
for (int v : graph.get(u)) {
term[v] = Math.max(term[v], term[u] + 1); // DP in topological order: u is final when it leaves
if (--indeg[v] == 0) q.offer(v);
}
}
return done == n ? best : -1;
}
}Go
// minSemesters returns the fewest terms for courses 1..n ({a, b}: a before b), or -1 on a cycle.
func minSemesters(n int, relations [][2]int) int {
graph := make([][]int, n+1)
indeg, term := make([]int, n+1), make([]int, n+1) // term[v]: the earliest term v can be taken
for _, r := range relations {
graph[r[0]] = append(graph[r[0]], r[1])
indeg[r[1]]++
}
var q []int
for v := 1; v <= n; v++ {
term[v] = 1
if indeg[v] == 0 {
q = append(q, v)
}
}
done, best := 0, 0
for len(q) > 0 {
u := q[0]
q = q[1:]
done++
best = max(best, term[u])
for _, v := range graph[u] {
term[v] = max(term[v], term[u]+1) // DP in topological order: u is final when it leaves
indeg[v]--
if indeg[v] == 0 {
q = append(q, v)
}
}
}
if done != n {
return -1
}
return best
}为什么对
- 入度为 0 的点没有未完成的前驱,放在当前位置不会违反任何边的方向;删掉它的出边后,剩下的图仍然是同样的问题。
- 环上每个点的入度永远至少为 1(它在环上的前驱永远不会被取出),所以输出的点少于 n ⇔ 图中有环。
- 任何有向无环图都至少有一个入度为 0 的点(否则沿入边一直往回走,终会重复某个点、形成环),所以只要无环,队列就不会提前变空。
30 秒口述稿
面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。
拓扑排序我用 Kahn 算法:先统计入度,把入度为 0 的点入队;每取出一个点就放进结果,并把它指向的点入度减一,减到 0 就入队。入度为 0 意味着前驱都已完成,所以放在这里不会违反依赖。如果最后输出的点少于 n,说明图中有环(剩下的点要么在环上,要么在环的下游),课程表这类题就是靠这个判断。时间 O(V + E)。For topological order I use Kahn's algorithm: count in-degrees and enqueue every node with none. Each node I dequeue goes into the result, and every node it points to loses one in-degree and is enqueued when it reaches zero. In-degree zero means all its prerequisites are done, so placing it violates nothing. If fewer than n nodes come out, the graph has a cycle (every leftover node is on one or downstream of one), which answers course schedule problems, all in O(V + E).自评一下
对照上面的答案给自己打个分。没掌握的题会按 1、3、7、21 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。
小测
3 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习
作答记录只保存在这台设备的浏览器里;登录后打开OA 选择题练习页,会存进账号。
模板练习
用上面的模板和变体出题:把打乱的行排回原序,把挖掉的边界条件填回去,或者在空白编辑器里整段默写,跑本站的测试并逐行对比。每次「换一题」都会重新出题。
下面几行被打乱了。点一行选中,再点另一行和它交换,排回模板原来的顺序;缩进是提示。
from collections import deque
def topo_order(n: int, edges: list[tuple[int, int]]) -> list[int]:
"""Edge (u, v) means u comes before v. Returns [] if there is a cycle."""
graph = [[] for _ in range(n)] q.append(v)
return order if len(order) == n else []分档练习
本站在 500 路里的 7 道题,按基础、核心、进阶分三档。每题附一句话思路,先自己想,想不出再看。
基础 1
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 207 | 课程表 | 把课程先修关系建为有向图,通过拓扑排序检测环。 |
核心 4
本站 OA 真题 7
- Effective Role PrivilegesSnowflake
- Collect Opportunity Data in a TreeSalesforce
- Collect CoinsCisco
- Uber Circular Shuttle OptimizationUber
- Print ScheduleOptiver
- Compilation Order with Topological SortApplied Intuition
- Spreadsheet Formula EvaluatorHarvey