Facebook Pixel

1125. Smallest Sufficient Team

Problem Description

You are given a list of required skills req_skills and a list of people. Each person people[i] has a list of skills they possess.

Your task is to form a sufficient team - a set of people such that every skill in req_skills is covered by at least one person in the team. The team should be represented by the indices of the people selected.

For example:

  • If req_skills = ["java", "nodejs", "reactjs"]
  • And people = [["java"], ["nodejs"], ["nodejs", "reactjs"]]
  • Then team = [0, 2] would be a sufficient team because person 0 has "java" and person 2 has both "nodejs" and "reactjs", covering all required skills

The goal is to find the smallest possible team that covers all required skills. You need to return the indices of the people in this minimal team. The answer can be returned in any order.

Key points:

  • Every skill in req_skills must be covered by at least one team member
  • One person can contribute multiple skills to the team
  • You want to minimize the total number of people in the team
  • The solution is guaranteed to exist
  • Return the indices (positions) of the selected people, not their skill lists
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key observation is that we need to track which skills are covered as we build our team. Since there are at most 16 skills, we can represent any combination of skills as a binary number where each bit indicates whether a specific skill is covered or not. For example, if we have 3 skills, 101 in binary means the 1st and 3rd skills are covered.

This naturally leads to a dynamic programming approach where our state is the set of skills currently covered. We can think of this as: "What's the minimum number of people needed to cover a specific set of skills?"

Starting from an empty skill set (0 in binary), we can progressively build up to covering all skills by considering adding each person one at a time. When we add a person, they contribute their skills to our current skill set using the bitwise OR operation.

The challenge is that we also need to track which people form the optimal team, not just the count. So alongside tracking the minimum number of people for each skill set, we need to remember:

  • Who was the last person added to achieve this skill set
  • What was the previous skill set before adding this person

This allows us to backtrack from the final state (all skills covered) to reconstruct the actual team members.

The reason this works efficiently is that:

  1. The number of possible skill sets is bounded by 2^m where m ≤ 16, making it computationally feasible
  2. Each person's skills can be pre-computed as a bitmask for quick combination with existing skill sets
  3. We only update a state when we find a better (smaller) team to achieve it, ensuring optimality

By the time we've processed all possible combinations, f[2^m - 1] (all skills covered) will contain the minimum team size, and we can trace back through our recorded transitions to find exactly which people form this optimal team.

Learn more about Dynamic Programming and Bitmask patterns.

Solution Approach

First, we create a mapping dictionary d where each skill in req_skills gets mapped to its index position. This allows us to quickly convert skills to bit positions.

Next, we preprocess each person's skills into a bitmask. For each person i, we create p[i] which represents all their skills as a single integer. If person i has skill s, we set the bit at position d[s] using p[i] |= 1 << d[s].

We then initialize three arrays for our dynamic programming:

  • f[i]: Minimum number of people needed to cover skill set i. Initialize all to infinity except f[0] = 0 (no people needed for no skills)
  • g[i]: The index of the last person added when achieving skill set i with minimum people
  • h[i]: The previous skill set state before adding the last person to achieve state i

The core DP transition works as follows:

  1. Iterate through all possible skill sets from 0 to (1 << m) - 1
  2. For each valid state i (where f[i] != inf), try adding each person j
  3. The new state after adding person j is i | p[j] (combining current skills with person j's skills)
  4. If f[i] + 1 < f[i | p[j]], we found a better way to achieve state i | p[j]:
    • Update f[i | p[j]] = f[i] + 1 (one more person than state i)
    • Record g[i | p[j]] = j (person j was added last)
    • Record h[i | p[j]] = i (came from state i)

After filling the DP table, we reconstruct the answer by backtracking:

  1. Start from the final state i = (1 << m) - 1 (all skills covered)
  2. Add g[i] (the last person for this state) to our answer
  3. Move to the previous state: i = h[i]
  4. Repeat until i = 0

This backtracking gives us the indices of all people in the minimum sufficient team.

The time complexity is O(2^m * n) where m is the number of required skills and n is the number of people. The space complexity is O(2^m) for the DP arrays.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Input:

  • req_skills = ["java", "nodejs", "reactjs"]
  • people = [["java"], ["nodejs"], ["nodejs", "reactjs"]]

Step 1: Create skill mapping

d = {"java": 0, "nodejs": 1, "reactjs": 2}

Step 2: Convert people's skills to bitmasks

  • Person 0 has ["java"]: p[0] = 001 (bit 0 set)
  • Person 1 has ["nodejs"]: p[1] = 010 (bit 1 set)
  • Person 2 has ["nodejs", "reactjs"]: p[2] = 110 (bits 1 and 2 set)

Step 3: Initialize DP arrays We have 2^3 = 8 possible skill states (000 to 111 in binary).

f = [0, inf, inf, inf, inf, inf, inf, inf]  // minimum people needed
g = [-1, -1, -1, -1, -1, -1, -1, -1]        // last person added
h = [-1, -1, -1, -1, -1, -1, -1, -1]        // previous state

Step 4: DP transitions

Starting from state 000 (no skills):

  • Add person 0: 000 | 001 = 001
    • f[001] = 1, g[001] = 0, h[001] = 000
  • Add person 1: 000 | 010 = 010
    • f[010] = 1, g[010] = 1, h[010] = 000
  • Add person 2: 000 | 110 = 110
    • f[110] = 1, g[110] = 2, h[110] = 000

From state 001 (java covered):

  • Add person 1: 001 | 010 = 011
    • f[011] = 2, g[011] = 1, h[011] = 001
  • Add person 2: 001 | 110 = 111
    • f[111] = 2, g[111] = 2, h[111] = 001

From state 010 (nodejs covered):

  • Add person 0: 010 | 001 = 011
    • f[011] is already 2, no update
  • Add person 2: 010 | 110 = 110
    • f[110] is already 1, no update

From state 110 (nodejs, reactjs covered):

  • Add person 0: 110 | 001 = 111
    • f[111] is already 2, no update

After processing all transitions:

State  Binary  f    g    h
0      000     0    -1   -1
1      001     1    0    0
2      010     1    1    0
3      011     2    1    1
4      100     inf  -1   -1
5      101     inf  -1   -1
6      110     1    2    0
7      111     2    2    1

Step 5: Backtrack from state 111 (all skills covered)

  • Start at state 111: Add person g[111] = 2 to team
  • Move to state h[111] = 001
  • At state 001: Add person g[001] = 0 to team
  • Move to state h[001] = 000
  • State 000 is our base case, stop

Result: Team = [2, 0] (or [0, 2] when reversed)

This team covers all skills: Person 0 has "java", Person 2 has "nodejs" and "reactjs".

Solution Implementation

1class Solution:
2    def smallestSufficientTeam(
3        self, req_skills: List[str], people: List[List[str]]
4    ) -> List[int]:
5        # Create a mapping from skill name to its bit position
6        skill_to_bit = {skill: bit_pos for bit_pos, skill in enumerate(req_skills)}
7      
8        num_skills = len(req_skills)
9        num_people = len(people)
10      
11        # Convert each person's skills to a bitmask representation
12        # person_skillmasks[i] represents all skills of person i as a bitmask
13        person_skillmasks = [0] * num_people
14        for person_idx, person_skills in enumerate(people):
15            for skill in person_skills:
16                person_skillmasks[person_idx] |= 1 << skill_to_bit[skill]
17      
18        # DP arrays for tracking the smallest team
19        # min_team_size[mask] = minimum number of people needed to cover skills represented by mask
20        min_team_size = [float('inf')] * (1 << num_skills)
21      
22        # last_person[mask] = the last person added to achieve this skill mask
23        last_person = [0] * (1 << num_skills)
24      
25        # prev_mask[mask] = the previous skill mask before adding last_person[mask]
26        prev_mask = [0] * (1 << num_skills)
27      
28        # Base case: 0 people needed to cover 0 skills
29        min_team_size[0] = 0
30      
31        # Try all possible skill combinations
32        for current_mask in range(1 << num_skills):
33            # Skip if this mask is not reachable
34            if min_team_size[current_mask] == float('inf'):
35                continue
36          
37            # Try adding each person to the current team
38            for person_idx in range(num_people):
39                # Calculate new skill mask after adding this person
40                new_mask = current_mask | person_skillmasks[person_idx]
41              
42                # If adding this person results in a smaller team for new_mask
43                if min_team_size[current_mask] + 1 < min_team_size[new_mask]:
44                    min_team_size[new_mask] = min_team_size[current_mask] + 1
45                    last_person[new_mask] = person_idx
46                    prev_mask[new_mask] = current_mask
47      
48        # Reconstruct the team by backtracking from the full skill mask
49        result_team = []
50        current_mask = (1 << num_skills) - 1  # All skills covered
51      
52        while current_mask:
53            result_team.append(last_person[current_mask])
54            current_mask = prev_mask[current_mask]
55      
56        return result_team
57
1class Solution {
2    public int[] smallestSufficientTeam(String[] req_skills, List<List<String>> people) {
3        // Create a mapping from skill name to its index position
4        Map<String, Integer> skillToIndex = new HashMap<>();
5        int numSkills = req_skills.length;
6        int numPeople = people.size();
7      
8        // Map each required skill to an index (0 to m-1)
9        for (int i = 0; i < numSkills; ++i) {
10            skillToIndex.put(req_skills[i], i);
11        }
12      
13        // Create bitmask representation for each person's skills
14        // personSkillMask[i] represents the skills of person i as a bitmask
15        int[] personSkillMask = new int[numPeople];
16        for (int i = 0; i < numPeople; ++i) {
17            for (String skill : people.get(i)) {
18                // Set the bit corresponding to this skill
19                personSkillMask[i] |= 1 << skillToIndex.get(skill);
20            }
21        }
22      
23        // Dynamic programming arrays
24        // minTeamSize[mask] = minimum team size to achieve skill mask
25        int[] minTeamSize = new int[1 << numSkills];
26        // lastPersonAdded[mask] = the last person added to achieve this mask
27        int[] lastPersonAdded = new int[1 << numSkills];
28        // previousMask[mask] = the previous skill mask before adding lastPersonAdded
29        int[] previousMask = new int[1 << numSkills];
30      
31        // Initialize with infinity (large value)
32        final int INFINITY = 1 << 30;
33        Arrays.fill(minTeamSize, INFINITY);
34        // Base case: 0 people needed for 0 skills
35        minTeamSize[0] = 0;
36      
37        // Dynamic programming: iterate through all possible skill combinations
38        for (int currentMask = 0; currentMask < (1 << numSkills); ++currentMask) {
39            // Skip if this mask is not reachable
40            if (minTeamSize[currentMask] == INFINITY) {
41                continue;
42            }
43          
44            // Try adding each person to the current team
45            for (int personIdx = 0; personIdx < numPeople; ++personIdx) {
46                // Calculate new skill mask after adding this person
47                int newMask = currentMask | personSkillMask[personIdx];
48              
49                // If adding this person results in a smaller team
50                if (minTeamSize[currentMask] + 1 < minTeamSize[newMask]) {
51                    minTeamSize[newMask] = minTeamSize[currentMask] + 1;
52                    lastPersonAdded[newMask] = personIdx;
53                    previousMask[newMask] = currentMask;
54                }
55            }
56        }
57      
58        // Reconstruct the solution by backtracking from the full skill mask
59        List<Integer> result = new ArrayList<>();
60        int fullSkillMask = (1 << numSkills) - 1;
61      
62        // Trace back from the full skill mask to find all people in the team
63        for (int mask = fullSkillMask; mask != 0; mask = previousMask[mask]) {
64            result.add(lastPersonAdded[mask]);
65        }
66      
67        // Convert list to array and return
68        return result.stream().mapToInt(Integer::intValue).toArray();
69    }
70}
71
1class Solution {
2public:
3    vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) {
4        // Map skill name to its index position
5        unordered_map<string, int> skillToIndex;
6        int numSkills = req_skills.size();
7        int numPeople = people.size();
8      
9        // Build skill to index mapping
10        for (int i = 0; i < numSkills; ++i) {
11            skillToIndex[req_skills[i]] = i;
12        }
13      
14        // Convert each person's skills to a bitmask
15        // personSkillMask[i] represents all skills of person i as a bitmask
16        int personSkillMask[numPeople];
17        memset(personSkillMask, 0, sizeof(personSkillMask));
18      
19        for (int i = 0; i < numPeople; ++i) {
20            for (const auto& skill : people[i]) {
21                personSkillMask[i] |= (1 << skillToIndex[skill]);
22            }
23        }
24      
25        // DP arrays for tracking minimum team size and reconstruction
26        // minTeamSize[mask] = minimum number of people needed to cover skills represented by mask
27        int minTeamSize[1 << numSkills];
28        // lastPerson[mask] = the last person added to achieve this skill mask
29        int lastPerson[1 << numSkills];
30        // prevMask[mask] = the previous skill mask before adding lastPerson
31        int prevMask[1 << numSkills];
32      
33        // Initialize with large values (0x3f3f3f3f when memset with 63)
34        memset(minTeamSize, 63, sizeof(minTeamSize));
35        minTeamSize[0] = 0;  // Base case: 0 people needed for 0 skills
36      
37        // Dynamic programming to find minimum team for each skill combination
38        for (int currentMask = 0; currentMask < (1 << numSkills); ++currentMask) {
39            // Skip if current state is unreachable
40            if (minTeamSize[currentMask] == 0x3f3f3f3f) {
41                continue;
42            }
43          
44            // Try adding each person to the current team
45            for (int person = 0; person < numPeople; ++person) {
46                int newMask = currentMask | personSkillMask[person];
47              
48                // If adding this person results in a smaller team for newMask
49                if (minTeamSize[currentMask] + 1 < minTeamSize[newMask]) {
50                    minTeamSize[newMask] = minTeamSize[currentMask] + 1;
51                    lastPerson[newMask] = person;
52                    prevMask[newMask] = currentMask;
53                }
54            }
55        }
56      
57        // Reconstruct the solution by backtracking from the full skill mask
58        vector<int> result;
59        int fullSkillMask = (1 << numSkills) - 1;
60      
61        // Trace back from full skill coverage to empty set
62        for (int mask = fullSkillMask; mask != 0; mask = prevMask[mask]) {
63            result.push_back(lastPerson[mask]);
64        }
65      
66        return result;
67    }
68};
69
1/**
2 * Finds the smallest sufficient team of people that collectively possess all required skills
3 * Uses dynamic programming with bitmask to track skill coverage
4 * 
5 * @param req_skills - Array of required skills
6 * @param people - Array where people[i] contains the skills of person i
7 * @returns Array of indices representing the smallest team
8 */
9function smallestSufficientTeam(req_skills: string[], people: string[][]): number[] {
10    // Map each skill to its bit position (index)
11    const skillToIndex: Map<string, number> = new Map();
12    const numSkills = req_skills.length;
13    const numPeople = people.length;
14  
15    // Build skill to index mapping
16    for (let i = 0; i < numSkills; ++i) {
17        skillToIndex.set(req_skills[i], i);
18    }
19  
20    // Convert each person's skills to a bitmask
21    // personSkillMask[i] represents all skills of person i as a bitmask
22    const personSkillMask: number[] = new Array(numPeople).fill(0);
23    for (let i = 0; i < numPeople; ++i) {
24        for (const skill of people[i]) {
25            personSkillMask[i] |= 1 << skillToIndex.get(skill)!;
26        }
27    }
28  
29    const INFINITY = 1 << 30;
30    const totalStates = 1 << numSkills; // Total number of skill combinations (2^numSkills)
31  
32    // dp[mask] = minimum number of people needed to cover skills represented by mask
33    const dp: number[] = new Array(totalStates).fill(INFINITY);
34  
35    // lastPersonAdded[mask] = index of the last person added to achieve this mask
36    const lastPersonAdded: number[] = new Array(totalStates).fill(0);
37  
38    // previousMask[mask] = the previous mask state before adding the last person
39    const previousMask: number[] = new Array(totalStates).fill(0);
40  
41    // Base case: 0 people needed to cover 0 skills
42    dp[0] = 0;
43  
44    // Dynamic programming: try adding each person to each possible skill combination
45    for (let currentMask = 0; currentMask < totalStates; ++currentMask) {
46        // Skip if current state is not reachable
47        if (dp[currentMask] === INFINITY) {
48            continue;
49        }
50      
51        // Try adding each person to the current skill set
52        for (let personIndex = 0; personIndex < numPeople; ++personIndex) {
53            const newMask = currentMask | personSkillMask[personIndex];
54          
55            // If adding this person results in fewer people for the new skill set
56            if (dp[currentMask] + 1 < dp[newMask]) {
57                dp[newMask] = dp[currentMask] + 1;
58                lastPersonAdded[newMask] = personIndex;
59                previousMask[newMask] = currentMask;
60            }
61        }
62    }
63  
64    // Reconstruct the team by backtracking from the full skill set
65    const team: number[] = [];
66    const fullSkillMask = (1 << numSkills) - 1; // All skills covered
67  
68    // Backtrack from full coverage to empty set
69    for (let mask = fullSkillMask; mask !== 0; mask = previousMask[mask]) {
70        team.push(lastPersonAdded[mask]);
71    }
72  
73    return team;
74}
75

Time and Space Complexity

Time Complexity: O(2^m × n)

The algorithm uses dynamic programming with bitmask states. The outer loop iterates through all possible skill combinations, which is 2^m states (where m is the number of required skills). For each valid state i, the inner loop iterates through all n people to try adding each person to the current team. Therefore, the total time complexity is O(2^m × n).

Space Complexity: O(2^m)

The algorithm maintains three arrays (f, g, and h), each of size 2^m to store:

  • f[i]: minimum number of people needed to achieve skill set i
  • g[i]: the last person added to achieve skill set i
  • h[i]: the previous state before adding the last person

Additionally, the array p of size n stores the skill bitmask for each person, but this is O(n) which is dominated by O(2^m) in most practical cases. The overall space complexity is O(2^m).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Redundant State Transitions and Performance Issues

A critical pitfall in this solution is that it processes every possible person for every reachable state, even when adding a person doesn't contribute any new skills. This creates unnecessary iterations and can significantly slow down the algorithm.

The Problem:

  • If a person's skills are already fully covered by the current mask (i.e., person_skillmasks[person_idx] & current_mask == person_skillmasks[person_idx]), then new_mask will equal current_mask
  • The algorithm still performs the comparison and update checks, wasting computational resources
  • For large inputs with many people having overlapping skills, this becomes a performance bottleneck

Solution: Add a check to skip persons who don't contribute new skills:

for person_idx in range(num_people):
    # Skip if this person doesn't add any new skills
    if person_skillmasks[person_idx] & ~current_mask == 0:
        continue
  
    # Calculate new skill mask after adding this person
    new_mask = current_mask | person_skillmasks[person_idx]
  
    # Rest of the logic remains the same...

2. Memory Optimization Opportunity Missed

Another pitfall is using three separate arrays (min_team_size, last_person, prev_mask) when a single dictionary could be more memory-efficient for sparse cases.

The Problem:

  • When the number of required skills is large (e.g., 15-16), the arrays allocate 2^m elements even if many states are never reached
  • This wastes memory for impossible or unreachable skill combinations

Solution: Use a dictionary-based approach for sparse state representation:

# Instead of arrays, use dictionaries
dp = {0: 0}  # skill_mask -> min_team_size
parent = {}   # skill_mask -> (prev_mask, person_added)

for current_mask in list(dp.keys()):
    for person_idx in range(num_people):
        new_mask = current_mask | person_skillmasks[person_idx]
      
        if new_mask not in dp or dp[current_mask] + 1 < dp[new_mask]:
            dp[new_mask] = dp[current_mask] + 1
            parent[new_mask] = (current_mask, person_idx)

3. Edge Case: Empty Required Skills

The code doesn't explicitly handle the case where req_skills is empty, which could lead to unexpected behavior.

The Problem:

  • If req_skills = [], then num_skills = 0 and (1 << num_skills) - 1 = 0
  • The reconstruction loop while current_mask: would immediately exit, returning an empty team
  • While technically correct, it might be worth adding explicit handling for clarity

Solution: Add an early return for edge cases:

def smallestSufficientTeam(self, req_skills, people):
    if not req_skills:
        return []
  
    # Rest of the implementation...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More