文章目录
题目
标题和出处
标题:从给定原材料中找到所有可以做出的菜
难度
4 级
题目描述
要求
有 n \texttt{n} n 道不同菜的信息。给定一个字符串数组 recipes \texttt{recipes} recipes 和一个二维字符串数组 ingredients \texttt{ingredients} ingredients。第 i \texttt{i} i 道菜的名字为 recipesi \texttt{recipesi} recipesi,如果拥有它所有 的原材料 ingredientsi \texttt{ingredientsi} ingredientsi,那么就可以做出 这道菜。一道菜的原材料可能是另一道 菜,也就是说 ingredientsi \texttt{ingredientsi} ingredientsi 可能包含 recipes \texttt{recipes} recipes 中另一个字符串。
另外给定一个字符串数组 supplies \texttt{supplies} supplies,包含初始时拥有的所有原材料,每一种原材料都有无限多。
返回可以做出的所有菜。可以按任意顺序返回答案。
注意两道菜在它们的原材料中可能互相包含。
示例
示例 1:
输入: recipes = "bread", ingredients = \["yeast","flour"], supplies = "yeast","flour","corn" \texttt{recipes = "bread", ingredients = \["yeast","flour"], supplies = "yeast","flour","corn"} recipes = "bread", ingredients = \["yeast","flour"], supplies = "yeast","flour","corn"
输出: "bread" \texttt{"bread"} "bread"
解释:
我们可以做出 "bread" \texttt{"bread"} "bread",因为我们有原材料 "yeast" \texttt{"yeast"} "yeast" 和 "flour" \texttt{"flour"} "flour"。
示例 2:
输入: recipes = "bread","sandwich", ingredients = \["yeast","flour","bread","meat"], supplies = "yeast","flour","meat" \texttt{recipes = "bread","sandwich", ingredients = \["yeast","flour","bread","meat"], supplies = "yeast","flour","meat"} recipes = "bread","sandwich", ingredients = \["yeast","flour","bread","meat"], supplies = "yeast","flour","meat"
输出: "bread","sandwich" \texttt{"bread","sandwich"} "bread","sandwich"
解释:
我们可以做出 "bread" \texttt{"bread"} "bread",因为我们有原材料 "yeast" \texttt{"yeast"} "yeast" 和 "flour" \texttt{"flour"} "flour"。
我们可以做出 "sandwich" \texttt{"sandwich"} "sandwich",因为我们有原材料 "meat" \texttt{"meat"} "meat" 且可以做出原材料 "bread" \texttt{"bread"} "bread"。
示例 3:
输入: recipes = "bread","sandwich","burger", ingredients = \["yeast","flour","bread","meat","sandwich","meat","bread"], supplies = "yeast","flour","meat" \texttt{recipes = "bread","sandwich","burger", ingredients = \["yeast","flour","bread","meat","sandwich","meat","bread"], supplies = "yeast","flour","meat"} recipes = "bread","sandwich","burger", ingredients = \["yeast","flour","bread","meat","sandwich","meat","bread"], supplies = "yeast","flour","meat"
输出: "bread","sandwich","burger" \texttt{"bread","sandwich","burger"} "bread","sandwich","burger"
解释:
我们可以做出 "bread" \texttt{"bread"} "bread",因为我们有原材料 "yeast" \texttt{"yeast"} "yeast" 和 "flour" \texttt{"flour"} "flour"。
我们可以做出 "sandwich" \texttt{"sandwich"} "sandwich",因为我们有原材料 "meat" \texttt{"meat"} "meat" 且可以做出原材料 "bread" \texttt{"bread"} "bread"。
我们可以做出 "burger" \texttt{"burger"} "burger",因为我们有原材料 "meat" \texttt{"meat"} "meat" 且可以做出原材料 "bread" \texttt{"bread"} "bread" 和 "sandwich" \texttt{"sandwich"} "sandwich"。
数据范围
- n = recipes.length = ingredients.length \texttt{n} = \texttt{recipes.length} = \texttt{ingredients.length} n=recipes.length=ingredients.length
- 1 ≤ n ≤ 100 \texttt{1} \le \texttt{n} \le \texttt{100} 1≤n≤100
- 1 ≤ ingredientsi.length, supplies.length ≤ 100 \texttt{1} \le \texttt{ingredientsi.length, supplies.length} \le \texttt{100} 1≤ingredientsi.length, supplies.length≤100
- 1 ≤ recipesi.length, ingredientsij.length, suppliesk.length ≤ 10 \texttt{1} \le \texttt{recipesi.length, ingredientsij.length, suppliesk.length} \le \texttt{10} 1≤recipesi.length, ingredientsij.length, suppliesk.length≤10
- recipesi \texttt{recipesi} recipesi、 ingredientsij \texttt{ingredientsij} ingredientsij 和 suppliesk \texttt{suppliesk} suppliesk 只包含小写英语字母
- 所有 recipes \texttt{recipes} recipes 和 supplies \texttt{supplies} supplies 中的值各不相同
- ingredientsi \texttt{ingredientsi} ingredientsi 中的字符串各不相同
解法
思路和算法
可以将所有的原材料和原材料之间的依赖关系看成有向图,每一道菜也是一种原材料。如果一道菜 recipe \textit{recipe} recipe 的原材料有 ingredient \textit{ingredient} ingredient,则存在一条从 ingredient \textit{ingredient} ingredient 指向 recipe \textit{recipe} recipe 的有向边。
只有当一道菜的所有原材料都具备时才能做出这道菜,因此可以使用拓扑排序判断从初始原材料开始是否可以做出每道菜。
首先建立有向图,记录每种原材料对应的所有后继原材料,即每种原材料是哪些菜的原材料,并记录每种原材料的入度,即每种原材料依赖于多少种原材料,然后从初始原材料开始,使用广度优先搜索实现拓扑排序。
拓扑排序时,对于每个原材料,执行如下操作。
-
得到该原材料的所有后继原材料。
-
对于每种后继原材料,将后继原材料的入度减 1 1 1。如果在更新入度之后,后继原材料的入度变为 0 0 0,则后继原材料对应的一道菜是可以做出的菜,将这道菜添加道答案中,并继续对后继原材料执行搜索。
遍历结束之后,即可得到可以做出的所有菜。
代码
java
class Solution {
public List<String> findAllRecipes(String[] recipes, List<List<String>> ingredients, String[] supplies) {
List<String> allRecipes = new ArrayList<String>();
Map<String, List<String>> adjacentMap = new HashMap<String, List<String>>();
Map<String, Integer> indegrees = new HashMap<String, Integer>();
int n = recipes.length;
for (int i = 0; i < n; i++) {
String recipe = recipes[i];
List<String> currIngredients = ingredients.get(i);
for (String ingredient : currIngredients) {
adjacentMap.putIfAbsent(ingredient, new ArrayList<String>());
adjacentMap.get(ingredient).add(recipe);
}
indegrees.put(recipe, currIngredients.size());
}
Queue<String> queue = new ArrayDeque<String>();
for (String ingredient : supplies) {
queue.offer(ingredient);
}
while (!queue.isEmpty()) {
String ingredient = queue.poll();
List<String> adjacent = adjacentMap.getOrDefault(ingredient, new ArrayList<String>());
for (String next : adjacent) {
indegrees.put(next, indegrees.get(next) - 1);
if (indegrees.get(next) == 0) {
allRecipes.add(next);
queue.offer(next);
}
}
}
return allRecipes;
}
}
复杂度分析
-
时间复杂度: O ( n + m + d ) O(n + m + d) O(n+m+d),其中 n n n 是数组 recipes \textit{recipes} recipes 的长度, m m m 是数组 supplies \textit{supplies} supplies 的长度, d d d 是数组 ingredients \textit{ingredients} ingredients 中的元素总数。有向图中有 n + m n + m n+m 个顶点和 d d d 条边,建立有向图和拓扑排序的时间复杂度由顶点数和边数决定。这里将字符串的长度视为常数。
-
空间复杂度: O ( n + m + d ) O(n + m + d) O(n+m+d),其中 n n n 是数组 recipes \textit{recipes} recipes 的长度, m m m 是数组 supplies \textit{supplies} supplies 的长度, d d d 是数组 ingredients \textit{ingredients} ingredients 中的元素总数。有向图中有 n + m n + m n+m 个顶点和 d d d 条边,存储有向图的信息需要 O ( n + m + d ) O(n + m + d) O(n+m+d) 的空间,拓扑排序时的队列需要 O ( n + m ) O(n + m) O(n+m) 的空间,因此空间复杂度是 O ( n + m + d ) O(n + m + d) O(n+m+d)。这里将字符串的长度视为常数。