从给定原材料中找到所有可以做出的菜 图解题解
这道题到底在问什么
- 输入
- recipes=[bread], ingredients=[[yeast,flour]], supplies=[yeast,flour,corn]
- 输出
- [bread](原料齐)
- 输入
- bread 又是 sandwich 的原料
- 输出
- [bread,sandwich](连锁推进)
最优解:一步一步想明白
- 3记住「原料指向菜、need 记缺料数、归零即做出入队」,下面逐帧套它。
- 4先把初始原料 yeast、flour、meat 标为起点(绿),它们一开始就备齐。接下来逐道菜建图:让每样原料连一条边指向「用到它的菜」,并记下每道菜的 need(需要几样原料)。
- 5菜「bread」需要 yeast、flour 共 2 样:这几样各连一条边指向 bread,并设 need[bread] = 2(它的入度)。
- 6菜「sandwich」需要 bread、meat 共 2 样:这几样各连一条边指向 sandwich,并设 need[sandwich] = 2(它的入度)。
- 7菜「burger」需要 sandwich、meat、bread 共 3 样:这几样各连一条边指向 burger,并设 need[burger] = 3(它的入度)。
- 8把三样初始原料 yeast、flour、meat 全部入队。右侧面板记着每道菜当前还缺几样:bread 缺 2、sandwich 缺 2、burger 缺 3。开始按队列逐个处理。
- 9弹出已备齐的「yeast」。看依赖它的菜:bread。逐个把它们的 need 减 1。
- 10「yeast」是「bread」的原料之一:need[bread] 减 1,现在还缺 1 样,先放着,等其余原料备齐。
- 11弹出已备齐的「flour」。看依赖它的菜:bread。逐个把它们的 need 减 1。
- 12「flour」是「bread」的原料之一:need[bread] 减到 0,原料全凑齐!做出「bread」,计入答案并入队(它接下来又能当别人的原料)。
- 13弹出已备齐的「meat」。看依赖它的菜:sandwich、burger。逐个把它们的 need 减 1。
- 14「meat」是「sandwich」的原料之一:need[sandwich] 减 1,现在还缺 1 样,先放着,等其余原料备齐。
- 15「meat」是「burger」的原料之一:need[burger] 减 1,现在还缺 2 样,先放着,等其余原料备齐。
- 16弹出已备齐的「bread」。看依赖它的菜:sandwich、burger。逐个把它们的 need 减 1。
- 17「bread」是「sandwich」的原料之一:need[sandwich] 减到 0,原料全凑齐!做出「sandwich」,计入答案并入队(它接下来又能当别人的原料)。
- 18「bread」是「burger」的原料之一:need[burger] 减 1,现在还缺 1 样,先放着,等其余原料备齐。
- 19弹出已备齐的「sandwich」。看依赖它的菜:burger。逐个把它们的 need 减 1。
- 20「sandwich」是「burger」的原料之一:need[burger] 减到 0,原料全凑齐!做出「burger」,计入答案并入队(它接下来又能当别人的原料)。
- 21弹出已备齐的「burger」。没有哪道菜直接以它为原料(或依赖它的菜已处理),它不再解锁新东西,继续看下一个。
- 22队列清空。一路连锁:yeast+flour 解锁 bread → bread+meat 解锁 sandwich → sandwich+meat+bread 解锁 burger。答案 = [bread, sandwich, burger],三道菜全部可做。
- 23回看核心:拓扑排序按「原料就绪顺序」一层层推进,每道菜恰在它最后一样原料备齐的那一刻 need 归零、被做出。没有初始原料能打破的环(循环依赖)永远 need 不归零,自然落选。
⚠️ 容易写错的地方
✗ 错:让菜指向原料
✓ 对:原料指向「依赖它的菜」
拓扑要从「已就绪」推向「待解锁」:原料先就绪,应由原料去触发依赖它的菜减 need,方向反了就推不动
✗ 错:给 supplies 节点也建 need、或当成要做的菜
✓ 对:need 只对菜建,等于该菜原料总数
初始原料不是要做的菜、不该有 need;但一道菜的 need 要按它原料的「总数」初始化(哪怕某些原料已在 supplies 里也照算),这些已备原料随后会在出队时把 need 一步步减下去
✗ 错:原料重复时 need 减多了
✓ 对:本题保证每道菜原料各不相同
若同一原料在一道菜里出现多次,need 会被多减;题目约束原料互异,按出现次数减恰好正确
完整代码(Python / C++ / Java)
Python
from typing import List
from collections import defaultdict, deque
class Solution:
def findAllRecipes(self, recipes: List[str], ingredients: List[List[str]], supplies: List[str]) -> List[str]:
g = defaultdict(list)
need = {}
for recipe, ing in zip(recipes, ingredients):
need[recipe] = len(ing)
for item in ing:
g[item].append(recipe)
q = deque(supplies)
ans = []
while q:
item = q.popleft()
for recipe in g[item]:
need[recipe] -= 1
if need[recipe] == 0:
ans.append(recipe)
q.append(recipe)
return ansC++
#include <queue>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
class Solution {
public:
vector<string> findAllRecipes(vector<string>& recipes, vector<vector<string>>& ingredients, vector<string>& supplies) {
unordered_map<string, vector<string>> g;
unordered_map<string, int> need;
for (int i = 0; i < (int)recipes.size(); ++i) {
need[recipes[i]] = ingredients[i].size();
for (auto &item : ingredients[i]) g[item].push_back(recipes[i]);
}
queue<string> q;
for (auto &s : supplies) q.push(s);
vector<string> ans;
while (!q.empty()) {
string item = q.front(); q.pop();
for (auto &recipe : g[item]) if (--need[recipe] == 0) {
ans.push_back(recipe);
q.push(recipe);
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public List<String> findAllRecipes(String[] recipes, List<List<String>> ingredients, String[] supplies) {
Map<String, List<String>> g = new HashMap<>();
Map<String, Integer> need = new HashMap<>();
for (int i = 0; i < recipes.length; i++) {
need.put(recipes[i], ingredients.get(i).size());
for (String item : ingredients.get(i)) g.computeIfAbsent(item, k -> new ArrayList<>()).add(recipes[i]);
}
Queue<String> q = new ArrayDeque<>(Arrays.asList(supplies));
List<String> ans = new ArrayList<>();
while (!q.isEmpty()) {
String item = q.poll();
for (String recipe : g.getOrDefault(item, Collections.emptyList())) {
need.put(recipe, need.get(recipe) - 1);
if (need.get(recipe) == 0) { ans.add(recipe); q.offer(recipe); }
}
}
return ans;
}
}复杂度
时间
O(V + E)
V 是原料与菜的总数,E 是所有「菜-原料」依赖数。建图遍历每条依赖一次,BFS 中每条边(原料指向菜)也只被处理一次,合计线性
空间
O(V + E)
邻接表 g 存所有依赖边 O(E),need 表与队列 O(V),合计 O(V+E)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 从给定原材料中找到所有可以做出的菜 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如果有循环依赖(比如 A 要 B、B 要 A),这套算法会怎样?+
环里的菜永远做不出来,会自动落选。因为 BFS 只把 need 归零的菜入队,而环里每道菜都缺一样来自环内、迟迟不就绪的原料,need 卡在大于 0、永远不入队。最终答案里不含环内任何菜,无需额外检测环。
能不能用 DFS 记忆化来判断每道菜能否做出?+
可以。对每道菜递归判断「它的每样原料是否都可得」(原料是初始供给,或是另一道可做出的菜),用记忆化缓存每道菜的结果、避免重复计算,同时记录递归栈防止循环依赖死循环(在环上的菜判为做不出)。思路与拓扑等价,拓扑(BFS)写法更直观、天然规避环,通常首选。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 从给定原材料中找到所有可以做出的菜 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。