拓扑排序实战:如何用 BFS 解决配方依赖问题?
在算法面试中,拓扑排序(Topological Sorting) 是一个高频考点,常见于 任务调度、依赖管理、编译顺序、配方制作 等问题。这篇文章将通过 食谱制作问题 ,带你掌握如何利用 BFS 进行拓扑排序。
1. 题目背景:配方制作问题
假设你有一些配方(recipes) ,每个配方都需要一定的原料(ingredients)才能制作。同时,你还拥有一些初始原料(supplies) 。
目标是找出所有可以最终制作出来的配方。
示例:
ini
String[] recipes = {"bread", "sandwich", "burger"};
List<List<String>> ingredients = List.of(
List.of("flour", "water"),
List.of("bread", "ham"),
List.of("sandwich", "lettuce")
);
String[] supplies = {"flour", "water", "ham", "lettuce"};
bread
需要flour
和water
,可以直接制作。sandwich
需要bread
和ham
,bread
可以制作,因此sandwich
也能制作。burger
需要sandwich
和lettuce
,sandwich
可制作,因此burger
也能制作。
最终输出:["bread", "sandwich", "burger"]
2. 拓扑排序解决方案
这个问题的核心在于 配方之间的依赖关系 ,可以用 拓扑排序 来解决。
解题思路:
-
构建有向图(Graph)
- 把**食材(原料)**视为节点。
- 让 配方依赖于原料,形成有向边。
- 记录 每个配方的入度(in-degree) ,即它所需的原料个数。
-
初始化队列(BFS)
- 将所有 初始可用的原料(supplies) 加入队列。
- 遍历队列,找到 入度为 0 的 recipe,表示它的所有原料都已经具备。
-
执行拓扑排序
- 不断取出队列中的元素,减少依赖它的 recipe 的入度。
- 当某个 recipe 的入度变为 0,说明它可以制作,加入队列。
代码实现
ini
import java.util.*;
public class RecipeMaker {
public List<String> findAllRecipes(String[] recipes, List<List<String>> ingredients, String[] supplies) {
Map<String, List<Integer>> graph = buildGraph(recipes, ingredients);
Map<String, Integer> inDegree = new HashMap<>();
Deque<String> queue = new ArrayDeque<>();
List<String> result = new ArrayList<>();
for (String supply : supplies) {
queue.offer(supply);
}
while (!queue.isEmpty()) {
String curIngredient = queue.poll();
if (!graph.containsKey(curIngredient)) continue;
for (int recipeIndex : graph.get(curIngredient)) {
inDegree.put(recipes[recipeIndex], inDegree.get(recipes[recipeIndex]) - 1);
if (inDegree.get(recipes[recipeIndex]) == 0) {
result.add(recipes[recipeIndex]);
queue.offer(recipes[recipeIndex]);
}
}
}
return result;
}
private Map<String, List<Integer>> buildGraph(String[] recipes, List<List<String>> ingredients) {
Map<String, List<Integer>> graph = new HashMap<>();
Map<String, Integer> inDegree = new HashMap<>();
for (int recipeIndex = 0; recipeIndex < recipes.length; recipeIndex++) {
inDegree.put(recipes[recipeIndex], ingredients.get(recipeIndex).size());
for (String ingredient : ingredients.get(recipeIndex)) {
graph.putIfAbsent(ingredient, new ArrayList<>());
graph.get(ingredient).add(recipeIndex);
}
}
return graph;
}
}
3. 关键规律总结
🟢 规律 1:拓扑排序适用于依赖关系
- 若 A 依赖 B,则 B 先制作。
- 适用于 任务调度、编译顺序、配方制作、课程安排等问题。
🟢 规律 2:入度(in-degree)控制 recipe 何时可制作
- 入度 0 代表该 recipe 所有原料可用。
- 逐步减少依赖项,保证 recipe 有序解锁。
🟢 规律 3:BFS 逐步解锁更多可制作的 recipe
- 先制作 直接可用的原料。
- 再解锁 依赖这些原料的 recipe。
4. 易错点分析
❌ 1. 没有正确初始化 inDegree
- 需要为每个 recipe 初始化原料数量。
❌ 2. 漏掉 queue.offer(recipe)
- 当 recipe 入度变 0 时,必须入队列,否则 BFS 不会继续。
❌ 3. 误解 graph
结构
graph
存储的是 原料 -> 依赖它的 recipe,而不是 recipe 本身。
5. 经典测试用例
✅ 基础用例:所有 recipe 可制作
ini
String[] recipes = {"bread", "sandwich"};
List<List<String>> ingredients = List.of(
List.of("flour", "water"),
List.of("bread", "ham")
);
String[] supplies = {"flour", "water", "ham"};
输出: ["bread", "sandwich"]
✅ 无法制作的 recipe
ini
String[] recipes = {"cake"};
List<List<String>> ingredients = List.of(List.of("flour", "sugar", "egg"));
String[] supplies = {"flour", "sugar"};
输出: []
(缺少 egg
,无法制作 cake
)
✅ 环形依赖(不可制作)
ini
String[] recipes = {"cake", "muffin"};
List<List<String>> ingredients = List.of(
List.of("muffin"),
List.of("cake")
);
String[] supplies = {"flour"};
输出: []
(cake
依赖 muffin
,muffin
依赖 cake
,形成环)
6. 结语
拓扑排序是一个非常实用的算法技巧,掌握它可以解决大量 任务调度、依赖关系 相关的问题。希望这篇文章能帮你更深入理解 BFS 在拓扑排序中的应用!🚀