【LeetCode】1125. 最小的必要团队(hard)——状压dp+回溯

【题目链接】

https://leetcode.cn/problems/smallest-sufficient-team/

【题目大意】

给定一个所需的技能列表和一群身怀技能的人。寻找一个最小数量的团队,使得团队中的技能覆盖给定的技能列表。保证能全覆盖,且每个人的技能都是所需的技能。所以其实就是找一个最少数量的子集,使其并集等于给定的集合。

【输入示例】

输入:req_skills = [“java”,“nodejs”,“reactjs”], people = [[“java”],[“nodejs”],[“nodejs”,“reactjs”]]
输出:[0,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 <= req_skills[i].length <= 16
req_skills[i] 由小写英文字母组成
req_skills 中的所有字符串 互不相同
1 <= people.length <= 60
0 <= people[i].length <= 16
1 <= people[i][j].length <= 16
people[i][j] 由小写英文字母组成
people[i] 中的所有字符串 互不相同
people[i] 中的每个技能是 req_skills 中的技能
题目数据保证「必要团队」一定存在


状压dp

  看到 r e q _ s k i l l s . l e n g t h < = 16 req\_skills.length <= 16 req_skills.length<=16 ,首先就应该想到状态压缩了。求一些子集的并集来得到一个大集合,对应着或运算。假设 r e q _ s k i l l s req\_skills req_skills p e o p l e people people 的长度分别为 m m m n n n ,我们可以用一个数组 d p [ 1 < < m ] dp[1<<m] dp[1<<m] 来表示每一个子集(也即状态), d p [ i ] dp[i] dp[i] 是一个集合,表示了构成当前状态的最小的团队人员编号。由于每个人的技能数最多也是16,因此我们可以将每个人的技能集合也编码成一个十进制数,表示为 s k i l l _ s e t skill\_set skill_set
  首先需要遍历所有状态(用 i i i表示)是肯定的。对于每一个状态,我们遍历人员列表(用 j j j表示),对当前状态,加入当前员工之后,这个员工的技能会使技能集合进入一个新的状态 n e w _ s t a t s = i ∣ s k i l l _ s e t [ j ] new\_stats=i|skill\_set[j] new_stats=iskill_set[j],如果 d p [ i ] . s i z e + 1 < d p [ n e w _ s t a t s ] . s i z e dp[i].size+1<dp[new\_stats].size dp[i].size+1<dp[new_stats].size ,即当前集合的加入使得新状态有了一个新的更小的解,我们就更新这个最小解,将当前状态的的人员列表加上当前人员,挂到新状态中。当 i = ( 1 < < m ) − 1 i=(1<<m)-1 i=(1<<m)1 时,即所有二进制位都为 1 了,此时 d p [ ( 1 < < m ) − 1 ] dp[(1<<m)-1] dp[(1<<m)1] 所表示的集合即为答案所需的最小团队。

class Solution {
    
    
public:
    vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) {
    
    
        int m=req_skills.size(),n=people.size();
        map<string,int> idx;//每个技能的编码位置
        for(int i=0;i<m;i++) idx[req_skills[i]]=i;
        vector<int> skill_set(n,0);//每个人的技能集合编码
        for(int i=0;i<n;i++){
    
    
            for(auto s:people[i]){
    
    
                skill_set[i]|=1<<idx[s];
            }
        }

        vector<vector<int>> dp(1<<m);//总的技能集合状态
        for(int i=0;i<(1<<m);i++){
    
    
            if(i&&dp[i].empty()) continue;//不是初始状体且集合为空,表示状态不可达,直接跳过
            for(int j=0;j<n;j++){
    
    
                int new_stats=i|skill_set[j];
                if(!new_stats) continue;//空集需要跳过,因为空集满足更新条件,但显然不需要
                if(dp[new_stats].empty()||dp[i].size()+1<dp[new_stats].size()){
    
    //新状态没去过或者是有更优解
                    dp[new_stats]=dp[i];
                    dp[new_stats].push_back(j);
                }
            }
        }
        return dp[(1<<m)-1];
    }
};



利用回溯优化空间

  对于每一个状态,我们都用一个员工编号的集合来表示,这导致空间开销很大。而且明显很多数据是重复的,对于最优解的状态转移过程,前一个状态的员工编号集合,一定是后一个状态的员工编号的子集,且差异仅限于当前加入的员工的编号。那我们能不能优化一下这个重复的空间呢?当然可以。我们可以用两个额外的数组来实现减少这部分的重复开销。 p r e _ s t a t s [ i ] pre\_stats[i] pre_stats[i] 用来表示当前状态的上一个状态; p r e _ p e o p l e [ i ] pre\_people[i] pre_people[i] 用来表示导致转移到 i i i 状态的新加入的员工编号。我们在跑完dp后,只需要从 p r e _ s t a t s [ ( 1 < < m ) − 1 ] pre\_stats[(1<<m)-1] pre_stats[(1<<m)1] 开始回溯,并每次读取导致转换到这一状态的 p r e _ p e o p l e pre\_people pre_people 加入一个新集合,最后得到的这个集合即是结果集合。

class Solution {
    
    
public:
    vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) {
    
    
        int m=req_skills.size(),n=people.size();
        map<string,int> idx;//每个技能的编码位置
        for(int i=0;i<m;i++) idx[req_skills[i]]=i;
        vector<int> skill_set(n,0);//每个人的技能集合编码
        for(int i=0;i<n;i++){
    
    
            for(auto s:people[i]){
    
    
                skill_set[i]|=1<<idx[s];
            }
        }

        vector<int> dp(1<<m,INT_MAX);//总的技能集合状态
        vector<int> pre_people(1<<m);//上一个贡献技能的人
        vector<int> pre_stats(1<<m);//上一个状态
        
        dp[0]=0;
        for(int i=0;i<(1<<m);i++){
    
    
            if(dp[i]==INT_MAX) continue;//状态不可达,直接跳过
            for(int j=0;j<n;j++){
    
    
                int new_stats=i|skill_set[j];
                if(dp[i]+1<dp[new_stats]){
    
    
                    dp[new_stats]=dp[i]+1;
                    pre_stats[new_stats]=i;
                    pre_people[new_stats]=j;
                }
            }
        }
        vector<int> res;
        for(int i=(1<<m)-1;i;i=pre_stats[i]){
    
    
            res.push_back(pre_people[i]);
        }
        return res;
    }
};

猜你喜欢

转载自blog.csdn.net/qq_44623371/article/details/130028818
今日推荐