需要教语言的最少人数 图解题解
这道题到底在问什么
- 输入
- n=2, langs=[[1],[2],[1,2]], fr=[[1,2],[1,3],[2,3]]
- 输出
- 1
- 输入
- n=1, langs=[[1],[1]], fr=[[1,2]]
- 输出
- 0
- 输入
- n=3, langs=[[1],[2],[3]], fr=[[1,2],[2,3]]
- 输出
- 2
先想最直接的笨办法
第二步开始统计。我们准备三个计数器,分别记语言1、语言2、语言3 在问题集 S 里已经有几个人会,现在都是 0。为什么只数 S 里的人?因为不在 S 的人本来就没沟通问题,教他们纯属浪费。挨个看 S 里的用户,把他会的语言计数加一。(动画第 17 步)
最优解:为什么这么做
一句话答案:LeetCode 1733 需要教语言的最少人数:先把说不上话的好友两端收进集合 S,再枚举每门语言、选 S 里已会人数最多的那门统一教,答案 = |S| 减去它,时间 O(m·k²)。
只教一门语言,最少教几个人能让好友都聊上话
有 n 门语言,languages[i] 是第 i 位用户会的语言集合,friendships 列出若干对好友。一对好友能沟通,指两人至少有一门共同语言。你只能挑定一门语言,教给任意几个人,让每一对好友都能沟通,问最少教几个。好友关系没有传递性,x、y 是好友、y、z 是好友,不代表 x、z 是好友。题面第一个例子 n=2、languages=[[1],[2],[1,2]],答案是 1。
五对好友里,真正卡住的是哪几对
好友只要已有共同语言,就不用碰。真正的麻烦只出在交集为空、也就是两人没有一门语言重合的那种好友对上。把这类说不上话的好友,两端一起收进集合 S。当前就能沟通的人不在 S 里,教他们任何语言都是白教,最少人数只会出在 S 这批人身上。
为什么全教同一门语言、还专挑已会人数最多的
题目卡死只能选一门语言 L,对 S 里的人只能统一教这一门。把 L 教给 S 中还不会 L 的人后,每条问题好友边——即前面收进集合 S 的那些说不上话的好友——的两端就都会 L,这对好友被修好。要教的人数,等于 S 的总人数减去 S 里本来就会 L 的人数——|S| 固定,要让差最小,就得让已会 L 的人数尽量大。于是枚举每门语言,数它在 S 里已有几人会,选已会最多那门,减完剩下就是最少要教的人。
三步写成代码,返回值到底减掉了谁
第一步用 check 判断两个用户有没有共同语言,参考代码里是最直白的双重循环,拿 u 的每门语言去和 v 的每门语言逐个比,相等返回真;遍历每对好友,check 为假就把 u、v 放进 S。第二步用计数器 cnt 走一遍 S 里的人,每人会的语言各记一票。第三步返回 len(S) 减去 cnt 的最高票,即 S 人数减去已会最多那门的人数。S 为空时没有票,返回 0。
n=2 的三人例子,S 和答案怎么落出来
拿题面第一个例子走一遍:用户1 会 {1},用户2 会 {2},用户3 会 {1,2},好友有 (1,2)、(1,3)、(2,3)。(1,2):{1} 和 {2} 没有重合,两人进 S,S={用户1, 用户2}。(1,3):{1} 和 {1,2} 共有语言1,能沟通,跳过。(2,3):{2} 和 {1,2} 共有语言2,跳过。三对看完,S={用户1, 用户2}。
再数 S 里的语言:用户1 会语言1,用户2 会语言2,两门各 1 人会,最高票 1。答案 = |S| 减最高票 = 2 − 1 = 1,教这 1 个人即可:给他补上另一人已会的那门,两人就有了共同语言。
这几处最容易写偏,复杂度也一并算清
想给不同的人教不同语言凑得更省,最招人上当,可题目只准选一门 L,唯有统一教 L,两端才同时会 L。把不在 S 的人也拉进来数,实则稀释每门语言的票数,最优语言和最少人数一起算歪。已会 L 的人天生就能用 L 沟通、不用教,若也算进要教的名单,就成了教满整个 S,正解是减去这批人。复杂度上,设好友对数为 m、单人语言数上限为 k:建 S 时逐对双重循环判交集,是 O(m·k²);之后只在 S 内统计一遍,是 O(|S|·k)。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3三步走:交集为空的好友两端进问题集 S;只在 S 内数每门语言的人数;答案 = |S| 减去已会人数最多那门语言的人数。
- 4先看清画面。上面这排是 5 位用户,右边表里写着每人会的语言:用户1只会语言1,用户2只会语言2,用户3会语言1和2,用户4会语言1,用户5会语言3。两个好友要能聊天,必须有共同语言。我们的任务是挑一门语言去教,让所有好友都能沟通,而且教的人越少越好。
- 5第一步,把说不上话的好友挑出来。一共有五对好友:(1,2)、(1,3)、(2,3)、(2,4)、(1,5)。我们一对一对地看两人的语言集合有没有交集。有交集的就跳过,没交集的说明他们现在聊不了,就把这两个人一起放进「问题集 S」。现在 S 还是空的,从第一对开始。
- 6看好友对 (1,2)。用户1 会 {1},用户2 会 {2}。把两个集合放一起,看看它们有没有共同的语言。此刻问题集 S 还是空的。
- 7交集是 ∅,空的。用户1 和用户2 没有任何共同语言,现在根本聊不了,所以两个人一起加入问题集 S,在数组上标成红色。加完之后 S = {用户1, 用户2}。
- 8看好友对 (1,3)。用户1 会 {1},用户3 会 {1,2}。把两个集合放一起,看看它们有没有共同的语言。此刻问题集 S 里已经有 {用户1, 用户2}(红色标出)。
- 9交集是 {1},不空。用户1 和用户3 靠语言1 就能沟通(绿色标出),这对好友没问题,直接跳过,S 保持 {用户1, 用户2} 不变。
- 10看好友对 (2,3)。用户2 会 {2},用户3 会 {1,2}。把两个集合放一起,看看它们有没有共同的语言。此刻问题集 S 里已经有 {用户1, 用户2}(红色标出)。
- 11交集是 {2},不空。用户2 和用户3 靠语言2 就能沟通(绿色标出),这对好友没问题,直接跳过,S 保持 {用户1, 用户2} 不变。
- 12看好友对 (2,4)。用户2 会 {2},用户4 会 {1}。把两个集合放一起,看看它们有没有共同的语言。此刻问题集 S 里已经有 {用户1, 用户2}(红色标出)。
- 13交集是 ∅,空的。用户2 和用户4 没有任何共同语言,现在根本聊不了,所以两个人一起加入问题集 S,在数组上标成红色。加完之后 S = {用户1, 用户2, 用户4}。
- 14看好友对 (1,5)。用户1 会 {1},用户5 会 {3}。把两个集合放一起,看看它们有没有共同的语言。此刻问题集 S 里已经有 {用户1, 用户2, 用户4}(红色标出)。
- 15交集是 ∅,空的。用户1 和用户5 没有任何共同语言,现在根本聊不了,所以两个人一起加入问题集 S,在数组上标成红色。加完之后 S = {用户1, 用户2, 用户4, 用户5}。
- 16五对好友全看完了。红色的用户1、用户2、用户4、用户5 都出现在某对说不上话的好友里,进了问题集 S,一共 4 人。用户3 一直没进 S,因为它和好友1、好友2 都能靠语言沟通,压根不用教。接下来只盯着 S 这 4 个人,数一数每门语言已经有多少人会。
- 17第二步开始统计。我们准备三个计数器,分别记语言1、语言2、语言3 在问题集 S 里已经有几个人会,现在都是 0。为什么只数 S 里的人?因为不在 S 的人本来就没沟通问题,教他们纯属浪费。挨个看 S 里的用户,把他会的语言计数加一。
- 18轮到用户1(紫色),它会 {1}。把语言1 的计数从 0 加到 1。这门语言暂时领先,当前最多有 1 人会。
- 19轮到用户2(紫色),它会 {2}。把语言2 的计数从 0 加到 1。语言2 追平语言1,当前最多仍是 1 人会。
- 20轮到用户4(紫色),它会 {1}。把语言1 的计数从 1 加到 2。这门语言暂时领先,当前最多有 2 人会。
- 21轮到用户5(紫色),它会 {3}。把语言3 的计数从 0 加到 1。当前领先的语言已会 2 人。
- 22统计完了:语言1 在 S 里有 2 人会(用户1、用户4,标绿),语言2 有 1 人会,语言3 有 1 人会。语言1 已会的人最多,所以就选它作为要教的那一门。已经会语言1 的用户1、用户4 不用教;还不会语言1 的用户2、用户5 标成红色,正是要教的对象。
- 23算总账。S 里 4 个人,其中 2 个已经会语言1,那么只要把语言1 教给剩下还不会的用户2 和用户5 就够了。答案 = S 的人数 4 减去已会语言1 的 2 人,等于 2。教完之后,S 里 4 个人全都会语言1,那三对原本说不上话的好友(两端都在 S 里)就都能沟通了。
- 24回顾整条链:先逐对好友挑出说不上话的两端,凑成问题集 S = {用户1,2,4,5};再只在 S 里数语言人数,语言1 已会的人最多有 2 个;最后把语言1 教给还不会的 2 个人。全绿表示 S 里 4 人现在都会语言1,所有好友都能沟通。最少要教 2 名用户。窍门就一句:交集为空的好友进 S,教 S 里已会人数最多的那门语言。
⚠️ 容易写错的地方
✗ 错:想给不同的人教不同语言,以为这样更省
✓ 对:全程只能选一门语言 L,教给问题集里所有还不会 L 的人
本题硬性规定只能选定一门语言 L,所以必须统一选 L。统一教 L 后,S 里还不会 L 的人被补齐,问题好友的两个端点都会 L,因此这些边都被修复
✗ 错:把所有人都拿去数语言,或者把不在好友里的人也算进来
✓ 对:只把出现在「说不上话」好友对里的人放进 S,只在 S 内统计
当前所有好友都能沟通的人,不需要任何改动,教他们是浪费。把他们算进统计会稀释语言计数,选出的最优语言和最少人数都会算错。范围必须严格锁在 S
✗ 错:误以为 S 里已经会最多语言那门的人也要教
✓ 对:答案 = |S| 减去 S 内已会该语言的人数,已会的人省下不教
选中语言 L 后,S 里已经会 L 的人天生就能和别人用 L 沟通,一个都不用教。真正要教的只是 S 里还不会 L 的那部分,所以是「减去已会人数」而不是直接教全部 S
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def minimumTeachings(
self, n: int, languages: List[List[int]], friendships: List[List[int]]
) -> int:
def check(u: int, v: int) -> bool:
for x in languages[u - 1]:
for y in languages[v - 1]:
if x == y:
return True
return False
s = set()
for u, v in friendships:
if not check(u, v):
s.add(u)
s.add(v)
cnt = Counter()
for u in s:
for l in languages[u - 1]:
cnt[l] += 1
return len(s) - max(cnt.values(), default=0)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
int minimumTeachings(int n, vector<vector<int>>& languages, vector<vector<int>>& friendships) {
unordered_set<int> s;
auto check = [&](int u, int v) {
for (int x : languages[u - 1]) {
for (int y : languages[v - 1]) {
if (x == y) {
return true;
}
}
}
return false;
};
for (auto& e : friendships) {
int u = e[0], v = e[1];
if (!check(u, v)) {
s.insert(u);
s.insert(v);
}
}
if (s.empty()) {
return 0;
}
vector<int> cnt(n + 1);
for (int u : s) {
for (int& l : languages[u - 1]) {
++cnt[l];
}
}
return s.size() - *max_element(cnt.begin(), cnt.end());
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int minimumTeachings(int n, int[][] languages, int[][] friendships) {
Set<Integer> s = new HashSet<>();
for (int[] e : friendships) {
int u = e[0], v = e[1];
if (!check(u, v, languages)) {
s.add(u);
s.add(v);
}
}
if (s.isEmpty()) {
return 0;
}
int[] cnt = new int[n + 1];
for (int u : s) {
for (int l : languages[u - 1]) {
++cnt[l];
}
}
int mx = 0;
for (int v : cnt) {
mx = Math.max(mx, v);
}
return s.size() - mx;
}
private boolean check(int u, int v, int[][] languages) {
for (int x : languages[u - 1]) {
for (int y : languages[v - 1]) {
if (x == y) {
return true;
}
}
}
return false;
}
}复杂度
时间
O(m·k² + |S|·k)
设好友数为 m、单个用户会的语言数上限为 k。第一步对每条好友关系判断有没有共同语言,参考代码用双重循环两两比对,是语言数的平方级 O(k²),所以建问题集是 O(m·k²);第二步只遍历 S 里的人累加语言计数,是 O(|S|·k)。若把每个人的语言先放进哈希集合判交集,第一步可降到 O(m·k)
空间
O(用户数 + 语言数)
按峰值算。问题集 S 最多装下所有出现在好友里的用户,是 O(用户数);语言计数无论用定长数组还是哈希表,都是 O(语言数 n)。两者相加,没有更大的额外结构
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 需要教语言的最少人数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么只教一门语言就够,教多门会不会更省?+
本题硬性规定只能选定一门语言来教,这是死约束。所以做法是枚举这一门语言,在每门语言下算 |S| 减去 S 里已经会它的人数,取最小值。要留神的是,这个贪心只在只能选一门的前提下成立;如果题意改成允许给不同的人教不同语言,那就是另一道题了,本题的做法不能直接套,答案甚至可能更省。只教一门只是这条硬约束下的最优解,脱开约束就未必成立。
为什么不用管那些不在问题好友里的人?+
因为他们当前的每个好友都已经能沟通,身上没有一条需要修的连接。教他们任何语言都不会让要教的总数变少,纯属白教。所以只把出现在说不上话好友对里的人收进 S,统计和结算都锁死在 S 内。把无关的人算进来,只会稀释每门语言的票数,把最优语言和最少人数一起算歪。
判断两人有没有共同语言,除了双重循环还有更快的写法吗?+
有。参考代码用的是最直观的双重循环,拿一个人的每门语言去和另一个人的每门语言逐个比,是语言数的平方级。更快的办法是先把一个人会的语言塞进哈希集合,再看另一个人有没有语言落在这个集合里,单次查询接近常数,整体从平方级降到线性。每人会的语言不多时两种写法差别不大;语言一多,哈希集合的优势就显出来了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 需要教语言的最少人数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。