leetcode
leetcode 1851 ~ 1900
从给定原材料中找到所有可以做出的菜

从给定原材料中找到所有可以做出的菜

难度:

标签:

题目描述

代码结果

运行时间: 92 ms, 内存: 22.8 MB


/*
 * 题目思路:
 * 1. 使用Java Stream API实现拓扑排序的逻辑。
 * 2. 使用集合保存初始的supplies。
 * 3. 通过stream和filter筛选可以制作的菜。
 */

import java.util.*;
import java.util.stream.Collectors;

public class RecipeFinderStream {
    public List<String> findAllRecipes(String[] recipes, List<List<String>> ingredients, String[] supplies) {
        Set<String> supplySet = new HashSet<>(Arrays.asList(supplies));
        Map<String, Long> indegree = new HashMap<>();
        Map<String, List<String>> graph = new HashMap<>();

        Arrays.stream(recipes).forEach(recipe -> indegree.put(recipe, 0L));

        for (int i = 0; i < recipes.length; i++) {
            final int index = i;
            ingredients.get(i).stream()
                .filter(ingredient -> !supplySet.contains(ingredient))
                .forEach(ingredient -> {
                    graph.computeIfAbsent(ingredient, x -> new ArrayList<>()).add(recipes[index]);
                    indegree.put(recipes[index], indegree.get(recipes[index]) + 1);
                });
        }

        return indegree.keySet().stream()
            .filter(recipe -> indegree.get(recipe) == 0)
            .flatMap(recipe -> {
                Queue<String> queue = new LinkedList<>();
                List<String> result = new ArrayList<>();
                queue.offer(recipe);
                while (!queue.isEmpty()) {
                    String current = queue.poll();
                    result.add(current);
                    if (graph.containsKey(current)) {
                        graph.get(current).stream()
                            .filter(next -> indegree.put(next, indegree.get(next) - 1) == 1)
                            .forEach(queue::offer);
                    }
                }
                return result.stream();
            })
            .collect(Collectors.toList());
    }
}

解释

方法:

本题解使用深度优先搜索(DFS)和备忘录技术(lru_cache)来解决制作菜肴的问题。思路包括: 1. 使用图的表示方法,将每个菜肴作为节点,其原材料作为边连接。2. 对于每个菜肴,如果所有原材料都可以通过现有供应或者其他可制作的菜肴获得,则该菜肴可以被制作。 3. 使用DFS和回溯法检查每个菜肴是否可以制作,同时使用三种状态标记节点避免重复计算和检测环。 4. 通过递归深度优先搜索,如果一个菜肴的所有原料都可以获得,则这个菜肴标记为可制作,并添加到供应列表中,以供其他菜肴的原料检查使用。

时间复杂度:

O(m+n)

空间复杂度:

O(m+n)

代码细节讲解

🦆
在构建图的表示中,你是如何处理菜肴之间相互作为原材料的情况?例如,如果菜肴A需要菜肴B作为原材料,而菜肴B反过来需要菜肴A,这种情况下算法如何避免进入无限循环?
在题解的DFS实现中,为每个节点(即菜肴)维护三种状态:未访问(默认状态,无状态标记),正在访问(状态标记为1),和访问完毕(状态标记为2)。当DFS探索一个菜肴时,首先将其状态标记为“正在访问”。如果在这个状态下,我们再次遇到该菜肴(递归过程中),这表明存在一个循环依赖,即一个环。这时,算法会认定当前菜肴不能被制作(返回False)。只有当所有所需原材料的DFS探索都返回True,菜肴的状态才会被标记为“访问完毕”,并将菜肴添加到供应列表中。这种状态标记机制有效避免了算法进入无限循环。
🦆
题解中提到使用了三种状态标记节点(正在访问,访问完毕,未访问),能否详细解释这三种状态在DFS过程中的具体作用和转换条件?
在DFS算法中,三种状态具有以下作用及转换条件: 1. **未访问**:这是节点的初始状态,表示该节点尚未被DFS访问。 2. **正在访问**:当DFS开始探索一个节点时,该节点被标记为正在访问。这有助于检测环路:如果在探索过程中再次访问到标记为正在访问的节点,表示存在环,因而当前节点无法完成制作。 3. **访问完毕**:如果节点的所有依赖节点都能成功制作(即所有依赖的DFS调用返回True),节点状态更新为访问完毕。这表示该节点可以成功制作,并且此状态也帮助避免未来重复的DFS探索该节点,优化效率。 状态的转换发生在DFS函数的控制流中:开始探索时标记为正在访问,探索结束后根据结果标记为访问完毕或回退到未访问状态(如果发现环或失败)。
🦆
题解使用了lru_cache来优化DFS的重复计算,具体来说,lru_cache是如何影响DFS的执行效率的?在什么情况下,这种缓存机制会显著提高性能?
lru_cache是一个缓存装饰器,它存储了最近调用的函数结果,并在后续调用中直接返回缓存的结果,而不是重新计算。在DFS中使用lru_cache可以显著提高效率,特别是在处理有重叠子问题的情况。在本题中,如果一个菜肴的制作依赖于其他菜肴,且这些菜肴的DFS结果已经被计算并缓存,那么lru_cache能够避免重复的DFS计算,直接使用缓存结果。这在有大量重复计算时(如多个菜肴共享相同的依赖)特别有用,可以显著减少总体的计算时间和资源消耗。

相关问题