leetcode
leetcode 1051 ~ 1100
最小的必要团队

最小的必要团队

难度:

标签:

题目描述

代码结果

运行时间: 59 ms, 内存: 0.0 MB


/*
 * 思路:
 * 1. 使用位操作和Stream流进行技能组合的匹配和最小团队计算。
 * 2. 用Map存储技能与位之间的映射,用stream操作对people列表进行处理。
 * 3. 使用reduce方法实现动态规划数组的更新。
 */

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

public class Solution {
    public int[] smallestSufficientTeam(String[] req_skills, List<List<String>> people) {
        int n = req_skills.length;
        Map<String, Integer> skillMap = IntStream.range(0, n)
            .boxed()
            .collect(Collectors.toMap(i -> req_skills[i], i -> i));
        List<Integer>[] dp = new List[1 << n];
        dp[0] = new ArrayList<>();
        IntStream.range(0, people.size()).forEach(i -> {
            int skillMask = people.get(i).stream()
                .map(skillMap::get)
                .reduce(0, (acc, x) -> acc | (1 << x));
            IntStream.range(0, dp.length).forEach(j -> {
                if (dp[j] == null) return;
                int newMask = j | skillMask;
                if (dp[newMask] == null || dp[newMask].size() > dp[j].size() + 1) {
                    dp[newMask] = new ArrayList<>(dp[j]);
                    dp[newMask].add(i);
                }
            });
        });
        List<Integer> result = dp[(1 << n) - 1];
        return result.stream().mapToInt(i -> i).toArray();
    }
}

解释

方法:

这个问题可以通过动态规划求解,利用位掩码来表示技能集。使用一个整数的不同位来代表不同的技能,如果某位为1,则表示拥有该技能。首先,为每个技能建立一个映射,将技能映射到一个索引(即位位置)。然后为每个人创建一个位掩码,表示他们拥有的技能。对于每个技能,维护一个集合,包含所有拥有该技能的人员索引。定义一个递归函数 dp(i, hit),其中 i 表示当前考虑的技能索引,hit 是一个位掩码,表示已覆盖的技能集。如果当前技能已被覆盖,即(hit >> i) & 1 为真,则直接处理下一个技能。否则,尝试通过添加拥有当前技能的人来更新 hit。最终,递归的目标是找到覆盖所有技能的最小团队。使用 @cache 装饰器来缓存递归结果,避免重复计算。

时间复杂度:

O(n * 2^n * 60)

空间复杂度:

O(2^n * n)

代码细节讲解

🦆
在动态规划中的状态定义`dp(i, hit)`,参数`i`和`hit`具体代表什么?
`dp(i, hit)`是一个动态规划函数,其中`i`表示当前正在考虑的技能索引,`hit`是一个位掩码,表示到目前为止已经被覆盖的技能集。在此问题中,每个技能对应一个位,如果某个技能已被覆盖,则`hit`中相应的位为1,否则为0。因此,`hit`可以精确地表示出哪些技能已经被团队成员的集合覆盖。
🦆
为什么在动态规划的递归函数中,使用位掩码`hit`来表示已覆盖的技能集?
使用位掩码`hit`来表示已覆盖的技能集可以让状态表示更为紧凑和高效。位掩码允许我们用一个整型数值来表示所有技能的覆盖情况,这样可以极大地简化状态的存储和操作。此外,位运算(如位与、位或和位移等)通常比其他形式的数据操作更快,这对于求解涉及多种可能组合的动态规划问题特别有利。
🦆
在递归函数`dp(i, hit)`中,如何决定是否需要继续递归到下一个技能?
在递归函数`dp(i, hit)`中,判断是否需要继续递归到下一个技能主要依据当前技能是否已被覆盖。通过检查`hit`位掩码中当前技能对应的位是否为1(即检查`(hit >> i) & 1`是否为真)来确定。如果当前技能已被覆盖,则函数调用自身对下一个技能进行递归(即`dp(i + 1, hit)`)。如果当前技能未被覆盖,则尝试添加不同的拥有该技能的人,更新`hit`,并递归求解。
🦆
在解决方案中提到了使用`min`函数和`key=len`来选择团队,这种方法的效率如何?是否存在更优的策略?
使用`min`函数和`key=len`来选择最小的团队是一种有效的方法,因为它确保在所有可能的团队配置中选取成员数最少的一个。这种方法直接针对问题的目标进行优化。然而,在效率方面,这种方法可能在某些情况下表现不佳,尤其是当候选团队数量很大时。虽然使用缓存(如`@cache`装饰器)可以显著提高递归调用的效率,但计算每个技能的所有可能团队配置仍可能导致高时间复杂度。更优的策略可能包括更高效的剪枝技术,在递归过程中更早地排除明显不优的选项,或者使用启发式或近似算法来找到足够好的解而非最优解。

相关问题