from typing import List
class Solution:
def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
skill_index = {skill: i for i, skill in enumerate(req_skills)}
n = len(req_skills)
dp = {0: []} # key: skill set bitmask, value: list of people indices
for i, person in enumerate(people):
person_skill = 0
for skill in person:
if skill in skill_index:
person_skill |= 1 << skill_index[skill]
if person_skill == 0:
continue
new_dp = dp.copy()
for skill_set, team in dp.items():
combined = skill_set | person_skill
if combined not in new_dp or len(new_dp[combined]) > len(team) + 1:
new_dp[combined] = team + [i]
dp = new_dp
return dp[(1 << n) - 1]
class Solution {
public:
vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) {
int n = req_skills.size();
unordered_map<string, int> skill_index;
for (int i = 0; i < n; ++i) skill_index[req_skills[i]] = i;
unordered_map<int, vector<int>> dp;
dp[0] = {};
for (int i = 0; i < people.size(); ++i) {
int person_skill = 0;
for (auto& skill : people[i]) {
if (skill_index.count(skill)) person_skill |= 1 << skill_index[skill];
}
if (person_skill == 0) continue;
auto dp_copy = dp;
for (auto& [skill_set, team] : dp) {
int combined = skill_set | person_skill;
if (!dp_copy.count(combined) || dp_copy[combined].size() > team.size() + 1) {
dp_copy[combined] = team;
dp_copy[combined].push_back(i);
}
}
dp = move(dp_copy);
}
return dp[(1 << n) - 1];
}
};
class Solution {
public int[] smallestSufficientTeam(String[] req_skills, List<List<String>> people) {
int n = req_skills.length;
Map<String, Integer> skillIndex = new HashMap<>();
for (int i = 0; i < n; ++i) skillIndex.put(req_skills[i], i);
Map<Integer, List<Integer>> dp = new HashMap<>();
dp.put(0, new ArrayList<>());
for (int i = 0; i < people.size(); ++i) {
int personSkill = 0;
for (String skill : people.get(i)) {
if (skillIndex.containsKey(skill))
personSkill |= 1 << skillIndex.get(skill);
}
if (personSkill == 0) continue;
Map<Integer, List<Integer>> newDp = new HashMap<>(dp);
for (int skillSet : dp.keySet()) {
int combined = skillSet | personSkill;
List<Integer> team = new ArrayList<>(dp.get(skillSet));
team.add(i);
if (!newDp.containsKey(combined) || newDp.get(combined).size() > team.size()) {
newDp.put(combined, team);
}
}
dp = newDp;
}
List<Integer> result = dp.get((1 << n) - 1);
return result.stream().mapToInt(Integer::intValue).toArray();
}
}
var smallestSufficientTeam = function(req_skills, people) {
const n = req_skills.length;
const skillIndex = new Map();
for (let i = 0; i < n; ++i) skillIndex.set(req_skills[i], i);
let dp = new Map();
dp.set(0, []);
for (let i = 0; i < people.length; ++i) {
let personSkill = 0;
for (const skill of people[i]) {
if (skillIndex.has(skill))
personSkill |= 1 << skillIndex.get(skill);
}
if (personSkill === 0) continue;
const dpCopy = new Map(dp);
for (const [skillSet, team] of dp) {
const combined = skillSet | personSkill;
if (!dpCopy.has(combined) || dpCopy.get(combined).length > team.length + 1) {
dpCopy.set(combined, [...team, i]);
}
}
dp = dpCopy;
}
return dp.get((1 << n) - 1);
};
You are given a list of required skills, req_skills
, and a list of people, where each person is described by a list of the skills they have. The goal is to form the smallest sufficient team: a subset of people such that every required skill is covered by at least one person in the team, and no skill is left uncovered.
For example, if req_skills = ["java", "nodejs", "reactjs"]
and people = [["java"], ["nodejs"], ["nodejs", "reactjs"]]
, the answer could be [0,2]
(people 0 and 2 together cover all required skills).
The problem is a classic example of the Set Cover problem, known to be NP-Hard. The brute-force way would be to try every possible subset of people, check if their combined skills cover all required skills, and pick the smallest such subset. However, this is not feasible when the number of people or skills grows, since there are 2n possible subsets (where n is the number of people).
To optimize, we can notice that the number of required skills is usually small (up to 16 in Leetcode), so we can use bitmasking to represent skill sets efficiently. This allows us to use dynamic programming (DP) to build up solutions for smaller skill sets and combine them to solve for larger ones.
Think of each skill set as a "state" (represented by a bitmask), and for each state, we keep track of the smallest team that can achieve it. We iteratively add people and update our DP if including them leads to a smaller team for any skill set.
i
is 1 if the skill at index i
is covered.
101
(binary for 5).
dp
where keys are skill bitmasks and values are the smallest team (list of people indices) that can achieve that skill set.
dp[0] = []
(no skills, empty team). For each person, and for each current skill set in dp
, consider adding the person. If the new combined skill set is not in dp
or can be achieved with a smaller team, update dp
for that skill set.
dp[target]
, where target
is the bitmask with all bits set (all skills covered).
This approach is efficient because it only depends on the number of possible skill sets (2n for n skills), not directly on the number of people.
Let's use req_skills = ["java", "nodejs", "reactjs"]
and people = [["java"], ["nodejs"], ["nodejs", "reactjs"]]
.
java:0
, nodejs:1
, reactjs:2
.
dp = {0: []}
.
dp[1] = [0]
dp[2] = [1]
dp[3] = [0,1]
dp[6] = [2]
dp[7] = [0,2]
111
(7 in decimal). dp[7] = [0,2]
is the smallest sufficient team.
The Smallest Sufficient Team problem is a practical application of the set cover problem, made tractable by using bitmasking and dynamic programming. By representing skill sets as integers and iteratively building up the minimal team for each possible combination, we avoid brute-force enumeration of all people subsets. This elegant approach leverages the small number of skills and ensures an efficient and scalable solution.