1125-最小的必要团队

Raphael Liu Lv10

作为项目经理,你规划了一份需求的技能清单 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 <= 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 中的技能
  • 题目数据保证「必要团队」一定存在

方法一:动态规划

思路与算法

题目输入数组 req_skills 的长度最大为 16,req_skills 中的每一项,被选择或者不被选择,总共的组合情况为 2^{16 种。因此可以通过「状态压缩」来表示一个技能集合。

我们将每一个技能 req_skills}[i] 映射到一个二进制数的第 i 位。例如:

  • req_skills}[0] 用 2^{0} = (1 << 0) = 1 来表示。
  • req_skills}[1] 用 2^{1} = (1 << 1) = 2 来表示。

以此类推,如此一来我们就可以用一个数字来表示一个技能集合。同时两个集合的并集计算,就可以转化为两个整数的或运算。

我们采用自下而上的「动态规划」的思路来解题,用 dp}[i] 来表示状态,状态含义是满足技能集合为 i 的最小人数的数组。初始化状态是 dp}[0],为空数组,因为如果不需要任何技能,不用任何人就可以完成。

我们首先依次遍历 peoples,求出当前这个人所有的技能集合 cur_skill。然后遍历 dp 表中的结果 dp}[\textit{prev}],其中原来的技能集合用 prev 来表示。设加入当前这个人后新的技能集合是 comb,由原来的技能集合和当前技能集合求并集后,可以得到:comb} = \textit{prev} | \textit{cur_skill。状态转移的规则是,如果 dp}[\textit{comb}] 不存在,或 dp}[\textit{prev}] 的长度加上 1 小于 dp}[\textit{comb}].\textit{size}(),那么我们就需要更新 dp}[\textit{comb}] 为 dp}[\textit{prev}],再将当前人加入到 dp}[\textit{comb}]。这里我们更新 dp}[\textit{comb}] 时候,可以采用直接覆盖的方式,因为更新后的结果因为已经包含了当前员工的技能,所以不会再次满足转移规则,而发生重复转移。

最后,所有技能的集合用 (1 << n) - 1 来表示,其中 n 是 req_skills 的长度,我们只需要返回最终答案 dp}[(1 << n) - 1]。

代码

[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public int[] smallestSufficientTeam(String[] req_skills, List<List<String>> people) {
int n = req_skills.length, m = people.size();
HashMap<String, Integer> skill_index = new HashMap<>();
for (int i = 0; i < n; ++i) {
skill_index.put(req_skills[i], i);
}
List<Integer>[] dp = new List[1 << n];
dp[0] = new ArrayList<>();
for (int i = 0; i < m; ++i) {
int cur_skill = 0;
for (String s : people.get(i)) {
cur_skill |= 1 << skill_index.get(s);
}
for (int prev = 0; prev < dp.length; ++prev) {
if (dp[prev] == null) {
continue;
}
int comb = prev | cur_skill;
if (dp[comb] == null || dp[prev].size() + 1 < dp[comb].size()) {
dp[comb] = new ArrayList<>(dp[prev]);
dp[comb].add(i);
}
}
}
return dp[(1 << n) - 1].stream().mapToInt(i -> i).toArray();
}
}
[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) {
int n = req_skills.size(), m = people.size();
unordered_map<string, int> skill_index;
for (int i = 0; i < n; ++i) {
skill_index[req_skills[i]] = i;
}
vector<vector<int>> dp(1 << n);
for (int i = 0; i < m; ++i) {
int cur_skill = 0;
for (string& s : people[i]) {
cur_skill |= 1 << skill_index[s];
}
for (int prev = 0; prev < dp.size(); ++prev) {
if (prev > 0 && dp[prev].empty()) {
continue;
}
int comb = prev | cur_skill;
if (comb == prev) {
continue;
}
if (dp[comb].empty() || dp[prev].size() + 1 < dp[comb].size()) {
dp[comb] = dp[prev];
dp[comb].push_back(i);
}
}
}
return dp[(1 << n) - 1];
}
};
[sol1-C#]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Solution {
public int[] SmallestSufficientTeam(string[] req_skills, IList<IList<string>> people) {
int n = req_skills.Length, m = people.Count;
IDictionary<string, int> skill_index = new Dictionary<string, int>();
for (int i = 0; i < n; ++i) {
skill_index.Add(req_skills[i], i);
}
IList<int>[] dp = new IList<int>[1 << n];
dp[0] = new List<int>();
for (int i = 0; i < m; ++i) {
int cur_skill = 0;
foreach (string s in people[i]) {
cur_skill |= 1 << skill_index[s];
}
for (int prev = 0; prev < dp.Length; ++prev) {
if (dp[prev] == null) {
continue;
}
int comb = prev | cur_skill;
if (dp[comb] == null || dp[prev].Count + 1 < dp[comb].Count) {
dp[comb] = new List<int>(dp[prev]);
dp[comb].Add(i);
}
}
}
return dp[(1 << n) - 1].ToArray();
}
}
[sol1-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
n, m = len(req_skills), len(people)
skill_index = {v: i for i, v in enumerate(req_skills)}
dp = [None] * (1 << n)
dp[0] = []
for i, p in enumerate(people):
cur_skill = 0
for s in p:
cur_skill |= 1 << skill_index[s]
for prev in range(1 << n):
if dp[prev] == None:
continue
comb = prev | cur_skill
if dp[comb] == None or len(dp[comb]) > len(dp[prev]) + 1:
dp[comb] = dp[prev] + [i]
return dp[(1 << n) - 1]
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
typedef struct {
char *key;
int val;
UT_hash_handle hh;
} HashItem;

HashItem *hashFindItem(HashItem **obj, char *key) {
HashItem *pEntry = NULL;
HASH_FIND_STR(*obj, key, pEntry);
return pEntry;
}

bool hashAddItem(HashItem **obj, char *key, int val) {
if (hashFindItem(obj, key)) {
return false;
}
HashItem *pEntry = (HashItem *)malloc(sizeof(HashItem));
pEntry->key = key;
pEntry->val = val;
HASH_ADD_STR(*obj, key, pEntry);
return true;
}

bool hashSetItem(HashItem **obj, char *key, int val) {
HashItem *pEntry = hashFindItem(obj, key);
if (!pEntry) {
hashAddItem(obj, key, val);
} else {
pEntry->val = val;
}
return true;
}

int hashGetItem(HashItem **obj, char *key, int defaultVal) {
HashItem *pEntry = hashFindItem(obj, key);
if (!pEntry) {
return defaultVal;
}
return pEntry->val;
}

void hashFree(HashItem **obj) {
HashItem *curr = NULL, *tmp = NULL;
HASH_ITER(hh, *obj, curr, tmp) {
HASH_DEL(*obj, curr);
free(curr);
}
}

int* smallestSufficientTeam(char ** req_skills, int req_skillsSize, char *** people, int peopleSize, int* peopleColSize, int* returnSize) {
int n = req_skillsSize, m = peopleSize;
HashItem *skill_index = NULL;
for (int i = 0; i < n; ++i) {
hashAddItem(&skill_index, req_skills[i], i);
}

int *dp[1 << n], dpColSize[1 << n];
memset(dpColSize, 0, sizeof(dpColSize));
for (int i = 0; i < (1 << n); i++) {
dp[i] = NULL;
}
dp[0] = (int *)calloc(m, sizeof(int));
for (int i = 0; i < m; ++i) {
int cur_skill = 0;
for (int j = 0; j < peopleColSize[i]; j++) {
cur_skill |= 1 << hashGetItem(&skill_index, people[i][j], 0);
}
for (int prev = 0; prev < (1 << n); ++prev) {
if (dp[prev] == NULL) {
continue;
}
int comb = prev | cur_skill;
if (dp[comb] == NULL || dpColSize[prev] + 1 < dpColSize[comb]) {
dp[comb] = (int *)calloc(m, sizeof(int));
memcpy(dp[comb], dp[prev], sizeof(int) * dpColSize[prev]);
dpColSize[comb] = dpColSize[prev];
dp[comb][dpColSize[comb]++] = i;
}
}
}
for (int i = 0; i < (1 << n) - 1; i++) {
if (dp[i]) {
free(dp[i]);
}
}
*returnSize = dpColSize[(1 << n) - 1];
return dp[(1 << n) - 1];
}
[sol1-Go]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func smallestSufficientTeam(req_skills []string, people [][]string) []int {
n, m := len(req_skills), len(people)
skill_index := make(map[string]int)
for i, skill := range req_skills {
skill_index[skill] = i
}
dp := make([][]int, 1 << n)
dp[0] = []int {}
for i := 0; i < m; i++ {
cur_skill := 0
for _, s := range people[i] {
cur_skill |= 1 << skill_index[s]
}
for prev := 0; prev < len(dp); prev++ {
if dp[prev] == nil {
continue
}
comb := prev | cur_skill
if dp[comb] == nil || len(dp[prev]) + 1 < len(dp[comb]) {
dp[comb] = make([]int, len(dp[prev]))
copy(dp[comb], dp[prev])
dp[comb] = append(dp[comb], i)
}
}
}
return dp[(1 << n) - 1]
}
[sol1-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var smallestSufficientTeam = function(req_skills, people) {
const n = req_skills.length;
const m = people.length;
let skillIndex = new Map();
for (let i = 0; i < n; i++) {
skillIndex.set(req_skills[i], i);
}
let dp = new Array(1 << n);
dp[0] = [];
for (let i = 0; i < m; i++) {
let cur_skill = 0;
for (let s of people[i]) {
cur_skill |= 1 << skillIndex.get(s);
}
for (let prev = 0; prev < dp.length; prev++) {
if (dp[prev] === undefined) {
continue;
}
let comb = prev | cur_skill;
if (dp[comb] === undefined || dp[prev].length + 1 < dp[comb].length) {
dp[comb] = [...dp[prev], i];
}
}
}
return dp[(1 << n) - 1];
};

复杂度分析

  • 时间复杂度:O(m^{2} \times 2^{n}),其中 n 是 req_skills 的长度,m 是 peoples 的长度。
  • 空间复杂度:O(m \times 2^{n}),其中 n 是 req_skills 的长度,m 是 peoples 的长度。

方法二:动态规划 + 优化

思路与算法
在方法一中,我们用 dp}[i] 来表示状态,状态含义是满足技能集合为 i 的最小人数的数组,每一个状态都用数组记录了具体的人员编号。这个过程浪费很多空间去储存结果,也消耗了很多时间去生成数组。
实际上我们只去要记录下每个状态的产生来源,就可以按序还原每个状态的具体人员编号的数组。

我们用:

  • dp}[i] 来表示,满足技能集合为 i 的最小人数。类似方法一中,我们初始化 dp}[0] = 0,其它 dp}[i] 初始为最大值 m。
  • prev_skill}[i] 来表示先前的技能集合,技能集合 i 是从 prev_skill}[i] 转移来的。
  • prev_people}[i] 来表示一个最新加入的员工,技能集合 i 是通过加入员工 prev_people}[i] 而转移来的。

通过这样方式,我们就记录了每一个状态的转移来源。

最后,所有技能的集合用 (1 << n) - 1 来表示,其中 n 是 req_skills 的长度。当我们要复原一个技能集合 i 的时候,我们可以找到最后一个员工 prev_people}[i], 把它加入结果中,然后赋值 i 为 prev_skill}[i]。不断重复这个过程,直到 i = 0,表示我们已找到需要技能集合的最少员工。

代码

[sol2-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public int[] smallestSufficientTeam(String[] req_skills, List<List<String>> people) {
int n = req_skills.length, m = people.size();
HashMap<String, Integer> skill_index = new HashMap<>();
for (int i = 0; i < n; ++i) {
skill_index.put(req_skills[i], i);
}
int[] dp = new int[1 << n];
Arrays.fill(dp, m);
dp[0] = 0;
int[] prev_skill = new int[1 << n];
int[] prev_people = new int[1 << n];
for (int i = 0; i < m; i++) {
List<String> p = people.get(i);
int cur_skill = 0;
for (String s : p) {
cur_skill |= 1 << skill_index.get(s);
}
for (int prev = 0; prev < (1 << n); prev++) {
int comb = prev | cur_skill;
if (dp[comb] > dp[prev] + 1) {
dp[comb] = dp[prev] + 1;
prev_skill[comb] = prev;
prev_people[comb] = i;
}
}
}
List<Integer> res = new ArrayList<>();
int i = (1 << n) - 1;
while (i > 0) {
res.add(prev_people[i]);
i = prev_skill[i];
}
return res.stream().mapToInt(j -> j).toArray();
}
}
[sol2-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public:
vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) {
int n = req_skills.size(), m = people.size();
unordered_map<string, int> skill_index;
for (int i = 0; i < n; ++i) {
skill_index[req_skills[i]] = i;
}
vector<int> dp(1 << n, m);
dp[0] = 0;
vector<int> prev_skill(1 << n, 0);
vector<int> prev_people(1 << n, 0);
for (int i = 0; i < m; ++i) {
int cur_skill = 0;
for (string& skill : people[i]) {
cur_skill |= 1 << skill_index[skill];
}
for (int prev = 0; prev < (1 << n); prev++) {
int comb = prev | cur_skill;
if (dp[comb] > dp[prev] + 1) {
dp[comb] = dp[prev] + 1;
prev_skill[comb] = prev;
prev_people[comb] = i;
}
}
}
vector<int> res;
int i = (1 << n) - 1;
while (i > 0) {
res.push_back(prev_people[i]);
i = prev_skill[i];
}
return res;
}
};

[sol2-C#]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Solution {
public int[] SmallestSufficientTeam(string[] req_skills, IList<IList<string>> people) {
int n = req_skills.Length, m = people.Count;
IDictionary<string, int> skill_index = new Dictionary<string, int>();
for (int i = 0; i < n; ++i) {
skill_index.Add(req_skills[i], i);
}
int[] dp = new int[1 << n];
for (int i = 0; i < dp.Length; i++) {
dp[i] = m;
}
dp[0] = 0;
int[] prev_skill = new int[1 << n];
int[] prev_people = new int[1 << n];
for (int i = 0; i < m; ++i) {
int cur_skill = 0;
foreach (string s in people[i]) {
cur_skill |= 1 << skill_index[s];
}
for (int prev = 0; prev < dp.Length; prev++) {
int comb = prev | cur_skill;
if (dp[comb] > dp[prev] + 1) {
dp[comb] = dp[prev] + 1;
prev_skill[comb] = prev;
prev_people[comb] = i;
}
}
}
List<int> res = new List<int>();
int skills = (1 << n) - 1;
while (skills > 0) {
res.Add(prev_people[skills]);
skills = prev_skill[skills];
}
return res.ToArray();
}
}
[sol2-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
n, m = len(req_skills), len(people)
skill_index = {v: i for i, v in enumerate(req_skills)}
dp = [m] * (1 << n)
dp[0] = 0
prev_skill = [0] * (1 << n)
prev_people = [0] * (1 << n)
for i, p in enumerate(people):
cur_skill = 0
for s in p:
cur_skill |= 1 << skill_index[s]
for prev in range(1 << n):
comb = prev | cur_skill
if dp[comb] > dp[prev] + 1:
dp[comb] = dp[prev] + 1
prev_skill[comb] = prev
prev_people[comb] = i
res = []
i = (1 << n) - 1
while i > 0:
res.append(prev_people[i])
i = prev_skill[i]
return res
[sol2-Go]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func smallestSufficientTeam(req_skills []string, people [][]string) []int {
n, m := len(req_skills), len(people)
skill_index := make(map[string]int)
for i, skill := range req_skills {
skill_index[skill] = i
}
dp := make([]int, 1 << n)
for i := range dp {
dp[i] = m
}
dp[0] = 0
prev_skill := make([]int, 1 << n)
prev_people := make([]int, 1 << n)
for i := 0; i < m; i++ {
cur_skill := 0
for _, s := range people[i] {
cur_skill |= 1 << skill_index[s]
}
for prev := 0; prev < (1 << n); prev++ {
comb := prev | cur_skill
if dp[comb] > dp[prev]+1 {
dp[comb] = dp[prev] + 1
prev_skill[comb] = prev
prev_people[comb] = i
}
}
}
res := []int{}
i := (1 << n) - 1
for i > 0 {
res = append(res, prev_people[i])
i = prev_skill[i]
}
return res
}
[sol2-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var smallestSufficientTeam = function(req_skills, people) {
const n = req_skills.length;
const m = people.length;
let skillIndex = new Map();
for (let i = 0; i < n; i++) {
skillIndex.set(req_skills[i], i);
}
let dp = new Array(1 << n).fill(m);
dp[0] = 0;
let prev_skill = new Array(1 << n);
let prev_people = new Array(1 << n);
for (let i = 0; i < m; i++) {
let cur_skill = 0;
for (let s of people[i]) {
cur_skill |= 1 << skillIndex.get(s);
}
for (let prev = 0; prev < (1 << n); prev++) {
let comb = prev | cur_skill;
if (dp[comb] > dp[prev] + 1) {
dp[comb] = dp[prev] + 1;
prev_skill[comb] = prev;
prev_people[comb] = i;
}
}
}
let res = [];
let skills = (1 << n) - 1;
while (skills > 0) {
res.push(prev_people[skills]);
skills = prev_skill[skills];
}
return res;
};
[sol2-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
typedef struct {
char *key;
int val;
UT_hash_handle hh;
} HashItem;

HashItem *hashFindItem(HashItem **obj, char *key) {
HashItem *pEntry = NULL;
HASH_FIND_STR(*obj, key, pEntry);
return pEntry;
}

bool hashAddItem(HashItem **obj, char *key, int val) {
if (hashFindItem(obj, key)) {
return false;
}
HashItem *pEntry = (HashItem *)malloc(sizeof(HashItem));
pEntry->key = key;
pEntry->val = val;
HASH_ADD_STR(*obj, key, pEntry);
return true;
}

bool hashSetItem(HashItem **obj, char *key, int val) {
HashItem *pEntry = hashFindItem(obj, key);
if (!pEntry) {
hashAddItem(obj, key, val);
} else {
pEntry->val = val;
}
return true;
}

int hashGetItem(HashItem **obj, char *key, int defaultVal) {
HashItem *pEntry = hashFindItem(obj, key);
if (!pEntry) {
return defaultVal;
}
return pEntry->val;
}

void hashFree(HashItem **obj) {
HashItem *curr = NULL, *tmp = NULL;
HASH_ITER(hh, *obj, curr, tmp) {
HASH_DEL(*obj, curr);
free(curr);
}
}

int* smallestSufficientTeam(char ** req_skills, int req_skillsSize, char *** people, int peopleSize, int* peopleColSize, int* returnSize) {
int n = req_skillsSize, m = peopleSize;
HashItem *skill_index = NULL;
for (int i = 0; i < n; ++i) {
hashAddItem(&skill_index, req_skills[i], i);
}

int dp[1 << n];
int prev_skill[1 << n], prev_people[1 << n];
memset(prev_skill, 0, sizeof(prev_skill));
memset(prev_people, 0, sizeof(prev_people));
dp[0] = 0;
for (int i = 1; i < (1 << n); i++) {
dp[i] = m;
}
for (int i = 0; i < m; ++i) {
int cur_skill = 0;
for (int j = 0; j < peopleColSize[i]; j++) {
cur_skill |= 1 << hashGetItem(&skill_index, people[i][j], 0);
}
for (int prev = 0; prev < (1 << n); prev++) {
int comb = prev | cur_skill;
if (dp[comb] > dp[prev] + 1) {
dp[comb] = dp[prev] + 1;
prev_skill[comb] = prev;
prev_people[comb] = i;
}
}
}

hashFree(&skill_index);
int *res = (int *)calloc(m, sizeof(int));
int i = (1 << n) - 1;
int pos = 0;
while (i > 0) {
res[pos++] = prev_people[i];
i = prev_skill[i];
}
*returnSize = pos;
return res;
}

复杂度分析

  • 时间复杂度:O(m \times 2^{n}),其中 n 是 req_skills 的长度,m 是 peoples 的长度。
  • 空间复杂度:O(2^{n}),其中 n 是 req_skills 的长度。
 Comments
On this page
1125-最小的必要团队