Java拓扑排序:2115 从给定原材料中找到所有可以做出的菜
你有 n
道不同菜的信息。给你一个字符串数组 recipes
和一个二维字符串数组 ingredients
。第 i
道菜的名字为 recipes[i]
,如果你有它 所有 的原材料 ingredients[i]
,那么你可以 做出 这道菜。一份食谱也可以是 其它 食谱的原料,也就是说 ingredients[i]
可能包含 recipes
中另一个字符串。
同时给你一个字符串数组 supplies
,它包含你初始时拥有的所有原材料,每一种原材料你都有无限多。
请你返回你可以做出的所有菜。你可以以 任意顺序 返回它们。
注意两道菜在它们的原材料中可能互相包含。
示例 1:
输入:recipes = ["bread"], ingredients = [["yeast","flour"]], supplies = ["yeast","flour","corn"] 输出:["bread"] 解释: 我们可以做出 "bread" ,因为我们有原材料 "yeast" 和 "flour" 。
示例 2:
输入:recipes = ["bread","sandwich"], ingredients = [["yeast","flour"],["bread","meat"]], supplies = ["yeast","flour","meat"] 输出:["bread","sandwich"] 解释: 我们可以做出 "bread" ,因为我们有原材料 "yeast" 和 "flour" 。 我们可以做出 "sandwich" ,因为我们有原材料 "meat" 且可以做出原材料 "bread" 。
示例 3:
输入:recipes = ["bread","sandwich","burger"], ingredients = [["yeast","flour"],["bread","meat"],["sandwich","meat","bread"]], supplies = ["yeast","flour","meat"] 输出:["bread","sandwich","burger"] 解释: 我们可以做出 "bread" ,因为我们有原材料 "yeast" 和 "flour" 。 我们可以做出 "sandwich" ,因为我们有原材料 "meat" 且可以做出原材料 "bread" 。 我们可以做出 "burger" ,因为我们有原材料 "meat" 且可以做出原材料 "bread" 和 "sandwich" 。
示例 4:
输入:recipes = ["bread"], ingredients = [["yeast","flour"]], supplies = ["yeast"] 输出:[] 解释: 我们没法做出任何菜,因为我们只有原材料 "yeast" 。
思路:
将食谱和原料视为图中的节点,原料到食谱的依赖关系视为边,通过广度优先搜索(BFS)遍历图,找出所有可以制作的食谱。
1. Map<String, List<String>> graph
- 作用:构建图的邻接表,表示 “原料 → 依赖该原料的食谱” 的映射关系。
- 数据流向:
- 键(Key):原料名称(如
"flour"
)。 - 值(Value):依赖该原料的食谱列表(如
["bread", "cake"]
)。
- 键(Key):原料名称(如
- 构建逻辑:遍历所有食谱的原料,将每个原料对应的食谱添加到列表中。
2. Map<String, Integer> degree
- 作用:记录每个食谱的入度(即所需原料的数量),用于拓扑排序。
- 数据流向:
- 键(Key):食谱名称(如
"bread"
)。 - 值(Value):该食谱所需的原料数量(如
2
表示需要 2 种原料)。
- 键(Key):食谱名称(如
- 构建逻辑:遍历所有食谱,统计每种食谱的原料数量。
3. List<String> res
- 作用:存储最终可以制作的食谱(即所有入度减为 0 的食谱)。
- 数据流向:在 BFS 过程中,每当一个食谱的所有原料都满足时(入度为 0),将其加入该列表。
4. List<List<String>> ingredients
- 作用:输入参数,存储每个食谱对应的原料列表。
- 数据流向:
- 外层
List
:每个元素对应一个食谱的索引。 - 内层
List
:每个元素是该食谱所需的原料名称(如["flour", "water"]
)。
- 外层
- 使用方式:在构建图和入度表时遍历该列表。
5. List<String> graph.get(ingredient)
- 作用:辅助列表,是
graph
中每个原料对应的食谱列表。 - 数据流向:在 BFS 处理每个原料时,遍历该列表,将对应食谱的入度减 1。
总结
数据结构 | 名称 | 作用 |
---|---|---|
Map | graph | 记录 “原料 → 依赖该原料的食谱” 的映射,用于构建图。 |
Map | degree | 记录每个食谱的入度(所需原料数量),用于拓扑排序。 |
List | res | 存储最终可制作的食谱。 |
List | ingredients | 输入参数,存储每个食谱的原料列表。 |
List | graph.values() | 辅助列表,存储每个原料对应的食谱列表(用于 BFS 遍历)。 |
public class Solution {// 主方法:寻找所有可以制作的食谱public List<String> findAllRecipes(String[] recipes, List<List<String>> ingredients, String[] supplies) {// 构建图结构:记录每个原料可以用来制作哪些食谱Map<String, List<String>> graph = new HashMap<>();// 记录每个食谱所需的原料数量(入度)Map<String, Integer> degree = new HashMap<>();// 遍历所有食谱,构建图和入度表for (int i = 0; i < recipes.length; i++) {String recipe = recipes[i];List<String> ingredientList = ingredients.get(i);// 对于每个食谱的每种原料for (String ingredient : ingredientList) {// 如果该原料还没有在图中,为其创建一个列表graph.putIfAbsent(ingredient, new ArrayList<>());// 将当前食谱添加到该原料可以制作的食谱列表中graph.get(ingredient).add(recipe);}// 记录该食谱所需的原料数量(即入度)degree.put(recipe, ingredientList.size());}// 存储最终可以制作的食谱List<String> res = new ArrayList<>();// 使用队列进行广度优先搜索,初始时将所有可用的补给加入队列Queue<String> queue = new LinkedList<>(Arrays.asList(supplies));// BFS遍历:处理所有可用的补给和可制作的食谱while (!queue.isEmpty()) {String supply = queue.poll();// 如果当前补给可以用来制作某些食谱if (graph.containsKey(supply)) {// 遍历该补给可以制作的所有食谱for (String recipe : graph.get(supply)) {// 减少该食谱所需的原料数量(入度减1)degree.put(recipe, degree.get(recipe) - 1);// 如果该食谱所需的所有原料都已满足(入度为0)if (degree.get(recipe) == 0) {// 将该食谱加入队列,以便后续检查它是否可以作为其他食谱的原料queue.offer(recipe);// 将该食谱加入结果列表res.add(recipe);}}}}return res;}
}
另一种更简单直观的方法:
class Solution {public List<String> findAllRecipes(String[] recipes, List<List<String>> ingredients, String[] supplies) {// 存储最终可以制作出来的食谱列表List<String> res = new ArrayList<>();// 使用队列来处理当前可用的原料(包括初始补给和新制作出的食谱)Queue<String> q = new LinkedList<>();// 将所有初始补给加入队列for (int i = 0; i < supplies.length; i++) {q.offer(supplies[i]);}// 当队列不为空时,持续处理可用的原料while (!q.isEmpty()) {// 取出当前可用的原料String supplie = q.poll();// 遍历所有食谱的原料列表for (int i = 0; i < ingredients.size(); i++) {// 获取当前食谱的原料列表List<String> list = ingredients.get(i);// 如果原料列表为空,跳过当前循环(实际题目中应该不会出现这种情况)if (list.size() == 0) {continue;}// 从原料列表中移除当前可用的原料// 注意:这里直接修改了原始输入的ingredients列表list.remove(supplie);// 如果移除后原料列表为空,说明该食谱所需的所有原料都已满足if (list.size() == 0) {// 将该食谱添加到结果列表中res.add(recipes[i]);// 同时将该食谱作为新的"原料"加入队列,以便后续检查依赖它的食谱q.offer(recipes[i]);}}}// 返回最终可以制作的食谱列表return res;}
}