Leetcode 1125:最小的必要团队(超详细的解法!!!)

作为项目经理,你规划了一份需求的技能清单 req_skills,并打算从备选人员名单 people 中选出些人组成一个「必要团队」( 编号为 i 的备选人员 people[i] 含有一份该备选人员掌握的技能列表)。

所谓「必要团队」,就是在这个团队中,对于所需求的技能列表 req_skills 中列出的每项技能,团队中至少有一名成员已经掌握。

我们可以用每个人的编号来表示团队中的成员:例如,团队 team = [0, 1, 3] 表示掌握技能分别为 people[0]people[1],和 people[3] 的备选人员。

请你返回 任一 规模最小的必要团队,团队成员用人员编号表示。你可以按任意顺序返回答案,本题保证答案存在。

示例 1:

输入:req_skills = ["java","nodejs","reactjs"], people = [["java"],["nodejs"],["nodejs","reactjs"]]
输出:[0,2]

示例 2:

输入:req_skills = ["algorithms","math","java","reactjs","csharp","aws"], people = [["algorithms","math","java"],["algorithms","math","reactjs"],["java","csharp","aws"],["reactjs","csharp"],["csharp","math"],["aws","java"]]
输出:[1,2]

提示:

  • 1 <= req_skills.length <= 16
  • 1 <= people.length <= 60
  • 1 <= people[i].length, req_skills[i].length, people[i][j].length <= 16
  • req_skillspeople[i] 中的元素分别各不相同
  • req_skills[i][j], people[i][j][k] 都由小写英文字母组成
  • 本题保证「必要团队」一定存在

解题思路

首先不难想到暴力解法,如果总共需要16种技能,总的人数是60的话,那么我们每种技能就有60个人可以选,总的计算量就是 6 0 16 60^{16} 6016,显然不合适。

我们分析一下这个问题,我们只关心哪些技能已经拥有了?当前需要考虑的成员是谁?那么这么做的时间复杂度就是 2 16 ∗ 60 2^{16}*60 21660(因为总共有16种技能,每个技能选和不选是两种情况,所以总共 2 16 2^{16} 216中可能),时间复杂度是可行的。

那么我们可以定义函数 f ( i ) f(i) f(i)表示我们对于技能列表i最少需要的人数,那么这个i我们总共就有1 << 16状态。假设我们从第k个状态到第i个状态,那么也就是i状态中的一些技能k状态中没有,那么此时k状态所需要的人len(k),对于len(k)+1<len(i)的话,那么我们就要更新i状态对应的人。

class Solution:
    def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
        key = {v: i for i, v in enumerate(req_skills)}
        dp = {0: []}
        for i, p in enumerate(people):
            his_skill = 0
            for skill in p:
                if skill in key:
                    his_skill |= 1 << key[skill]
            for skill_set, need in list(dp.items()):
                with_him = skill_set | his_skill
                if with_him == skill_set: continue
                if with_him not in dp or len(dp[with_him]) > len(need) + 1:
                    dp[with_him] = need + [i]
        return dp[(1 << len(req_skills)) - 1]

由于是NP问题,所以我们直接暴力dfs也是可以的。

from functools import lru_cache
class Solution:
    def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
        key = {skill: i for i, skill in enumerate(req_skills)}
        people_skill = [0 for _ in range(len(people))] 
        skill_people = [[] for _ in range(len(req_skills))] 
        for i, p in enumerate(people):
            for skill in p:
                if skill in key:
                    people_skill[i] |= 1 << key[skill]
                    skill_people[key[skill]].append(i)
        
        def first_set_bit(n: int) -> int:
            return int(math.log2(n & -n)) #low bit
        
        @lru_cache()
        def dfs(skills_wanted: int) -> List[int]:
            if skills_wanted == 0:
                return []
            
            smallest_team_size = float('inf')
            res = []
            
            target_skill = first_set_bit(skills_wanted)
            
            for p in skill_people[target_skill]:
                cand_skills_wanted = skills_wanted
                cand_skills_wanted &= ~people_skill[p]
                cand = [p] + dfs(cand_skills_wanted)
                if len(cand) < smallest_team_size:
                    smallest_team_size = len(cand)
                    res = cand
            
            return res
        
        return dfs((1 << len(req_skills)) - 1)

在上面的代码中添加了非常重要的优化,就是我们将会相关技能的人存到了一个list中(也就是skill_people),那么我们缺什么技能只需遍历skill_people找到会相关技能的人即可,而不用将所有的人都遍历一遍。但是这个算法的时间复杂度和动态规划版本不一样,这个算法的时间复杂度是 O ( ∏ i = 0 16 k i ) O(\prod_{i=0}^{16} k_i) O(i=016ki),其中 k i k_i ki表示每个技能会的人数。然后递归部分存在大量重复,可以通过记忆化搜索去除,我这里偷懒了,使用了python中的lru_cache解决,所以最后的时间复杂度是要远小于 O ( ∏ i = 0 16 k i ) O(\prod_{i=0}^{16} k_i) O(i=016ki)的。

reference:

https://leetcode.com/problems/smallest-sufficient-team/discuss/334572/Python-DP-Solution

我将该问题的其他语言版本添加到了我的GitHub Leetcode

如有问题,希望大家指出!!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值