【LeetCode 每日一题】1733. 需要教语言的最少人数
Problem: 1733. 需要教语言的最少人数
文章目录
- 整体思路
- 完整代码
- 时空复杂度
- 时间复杂度:O(M*L + F*L + N*(T+M))
- 空间复杂度:O(M*N + T)
整体思路
这段代码旨在解决一个关于语言教学的优化问题。问题背景是:有一群用户,每个人会说一种或多种语言。还有一些好友关系。如果一对好友不能直接交流(即他们没有任何共同语言),那么他们就构成了“无法交流”的好友对。我们需要选择一种语言,教给尽可能少的人,使得所有这些“无法交流”的好友对都能通过这门新教的语言进行交流。问题要求返回需要教学的最少人数。
该算法的思路清晰,分为三个主要步骤:预处理 -> 筛选 -> 暴力枚举。
-
第一步:预处理
- 为了能够快速查询某个人是否会说某种语言,算法首先创建了一个
m x (n+1)
的布尔二维数组learned
。 learned[i][j]
为true
表示用户i
会说语言j
。- 通过遍历输入的
languages
数组,将这个learned
查找表填充好。这避免了在后续步骤中反复线性搜索每个用户的语言列表。
- 为了能够快速查询某个人是否会说某种语言,算法首先创建了一个
-
第二步:筛选出无法交流的好友对
- 算法的核心是只关注那些当前无法交流的好友对,因为只有他们才需要我们去解决问题。
- 代码遍历
friendships
数组。对于每一对好友(u, v)
:- 它会检查他们是否有共同语言。通过遍历用户
u
的所有语言x
,并在learned
查找表中查询v
是否也会说x
(learned[v][x]
)。 - 如果找到了任何一种共同语言,说明这对好友可以交流,直接使用
continue next;
(带标签的continue
)跳到外层循环,处理下一对好友。 - 如果遍历完
u
的所有语言都没有找到共同点,说明这对好友无法交流。将这对好友[u, v]
加入到一个todoList
中。
- 它会检查他们是否有共同语言。通过遍历用户
- 循环结束后,
todoList
中就包含了所有需要我们去“修复”关系的好友对。
-
第三步:枚举所有语言作为通用语
- 现在,我们需要从
1
到n
这n
种语言中,选择一种作为“通用教学语言”。 - 算法采用暴力枚举的方式,遍历每一种可能的语言
k
(从1到n)。 - 对于每一种被选作通用语的语言
k
,它计算需要教多少人才能让todoList
中的所有好友都能交流。- 创建一个
HashSet<Integer> set
,用于存储需要被教语言k
的不重复的用户ID。 - 遍历
todoList
中的每一对无法交流的好友(u, v)
。 - 为了让
u
和v
能通过语言k
交流,他们都必须会说语言k
。 - 检查
u
是否会说语言k
(!learned[u][k]
),如果不会,就把u
加入set
。 - 检查
v
是否会说语言k
(!learned[v][k]
),如果不会,就把v
加入set
。
- 创建一个
- 当遍历完
todoList
后,set.size()
就是选择语言k
作为通用语时,需要教学的总人数。 ans = Math.min(ans, set.size())
:用这个值去更新全局的最小教学人数ans
。
- 现在,我们需要从
-
返回结果
- 在枚举完所有可能的通用语言后,
ans
中存储的就是最终的最小值,将其返回。
- 在枚举完所有可能的通用语言后,
完整代码
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;class Solution {/*** 计算最少需要教多少人,才能让所有好友都能交流。* @param n 语言的总数* @param languages 每个用户会的语言列表* @param friendships 好友关系列表* @return 最少需要教学的人数*/public int minimumTeachings(int n, int[][] languages, int[][] friendships) {int m = languages.length; // 用户总数// 步骤 1: 预处理,创建一个快速查询表// learned[i][j] = true 表示用户 i 会说语言 jboolean[][] learned = new boolean[m][n + 1];for (int i = 0; i < m; i++) {for (int x : languages[i]) {learned[i][x] = true;}}// 步骤 2: 筛选出所有无法交流的好友对List<int[]> todoList = new ArrayList<>();next: // 标签,用于跳出内层循环到外层循环的下一次迭代for (int[] f : friendships) {int u = f[0] - 1; // 用户索引从0开始int v = f[1] - 1;// 检查 u 和 v 是否有共同语言for (int x : languages[u]) {if (learned[v][x]) {// 有共同语言,这对好友可以交流,跳过continue next;}}// 遍历完 u 的所有语言都没有共同点,加入待办列表todoList.add(f);}// 如果所有好友都能交流,无需教学,返回0if (todoList.isEmpty()) {return 0;}int ans = m; // 答案的上限是所有人都学一种新语言// 步骤 3: 枚举每一种语言 k 作为通用教学语言for (int k = 1; k <= n; k++) {// 使用 Set 来存储需要被教语言 k 的不重复的用户Set<Integer> set = new HashSet<>();// 遍历所有无法交流的好友对for (int[] f : todoList) {int u = f[0] - 1;int v = f[1] - 1;// 如果 u 不会说语言 k,则需要教他if (!learned[u][k]) {set.add(u);}// 如果 v 不会说语言 k,则需要教他if (!learned[v][k]) {set.add(v);}}// 更新全局最小教学人数ans = Math.min(ans, set.size());}return ans;}
}
时空复杂度
时间复杂度:O(ML + FL + N*(T+M))
-
预处理
learned
数组 (步骤1):- 遍历
m
个用户,每个用户的语言列表平均长度为L
。 - 时间复杂度为 O(M * L),其中
L
是languages[i]
的最大长度。
- 遍历
-
筛选
todoList
(步骤2):- 遍历
F
个好友关系。 - 对于每对好友,内层循环遍历其中一个用户的语言列表,长度为
L
。 - 时间复杂度为 O(F * L),其中
F
是friendships
的长度。
- 遍历
-
枚举语言 (步骤3):
- 外层循环遍历
N
种语言。 - 内层循环遍历
todoList
,其大小设为T
(T <= F
)。 - 在循环内部,
set.add()
操作的平均时间复杂度是 O(1)。 - 因此,这部分的时间复杂度为 O(N * T)。
- 外层循环遍历
综合分析:
总的时间复杂度是以上三部分之和:O(M*L + F*L + N*T)
。这是一个比较复杂的表达式,取决于各个输入参数的大小。在题目给定的约束下,这个复杂度是可接受的。
空间复杂度:O(M*N + T)
-
learned
数组:- 创建了一个
m x (n+1)
的布尔数组。 - 空间复杂度为 O(M * N)。
- 创建了一个
-
todoList
列表:- 在最坏情况下,所有好友对都无法交流,列表大小为
F
(好友关系数)。 - 空间复杂度为 O(F)。设需要处理的好友对为
T
,则为 O(T)。
- 在最坏情况下,所有好友对都无法交流,列表大小为
-
set
集合:- 在枚举循环中,
set
的大小在最坏情况下可能包含todoList
中所有涉及的用户。 - 最多有
2*T
个用户被加入,所以空间复杂度为 O(T)。 - 这个空间是可复用的,每次外层循环都会创建一个新的
set
。
- 在枚举循环中,
综合分析:
算法所需的额外空间主要由 learned
数组和 todoList
决定。因此,总的空间复杂度为 O(M*N + T)。