用户分组 图解题解
这道题到底在问什么
- 输入
- groupSizes=[3,3,3,3,3,1,3]
- 输出
- [[0,1,2],[5],[3,4,6]]
- 输入
- groupSizes=[2,1,3,3,3,2]
- 输出
- [[1],[2,3,4],[0,5]]
最优解:为什么这么做
一句话答案:LeetCode 1282 用户分组:每人指定所在组的大小,按大小分桶——扫一趟把人丢进对应编号的桶,桶一满就当作一组发出、清空复用,写同一个数的人可互换所以随到随分,时间 O(n)。
每个人指定所在组的大小,怎么分
给一个数组 groupSizes,位置 i 上的数就是第 i 个人要求的组大小——他必须待在一个正好这么多人的组里。要把 0 到 n−1 全部人分完,每人恰好进一组,且每组的实际人数等于组里每个人纸条上写的那个数。题面示例 groupSizes=[3,3,3,3,3,1,3],一种合法答案是 [[0,1,2],[5],[3,4,6]]:三个写 3 的凑一组,写 1 的自己单独成组。答案不唯一,写同一个数的人谁跟谁一组都行,题目也保证至少存在一种分法。
要不要把所有分组方式都试一遍
一上来容易担心分组得试各种搭配、凑不齐还要回头换人,那就落进了回溯的指数级枚举,即走不通就退回来重排。但这里有个省事的点被漏掉了:一个人进多大的组是写死的、和别人无关;而写同一个数的人,彼此完全可以互换,谁配谁都合法。既然如此,凑够人就能立刻锁定一组,用不着比较,也用不着反悔。
按组大小分桶,凑满就发出去
给每种组大小准备一个桶,桶的编号就是它要凑的人数。从头扫一遍,读到一个人就看他纸条写几,把他丢进对应编号的桶。哪个桶里的人数一旦等于它的编号,这一桶就正好是一个满员的组,当场打包进答案,随后把桶倒空。倒空的桶不作废,后面再遇到写同样数字的人,又从空桶重新攒起。整趟扫下来随到随分,既不排序也不回溯。
发完组为什么要连着倒空桶
发组和倒空必须是连着的一步。要是只把满桶的名单记进答案、却忘了清桶,下一个写同样数字的人还会接着往里塞,桶越攒越多,发出去的组人数就超了。倒空之后这只桶继续服役:同一个编号的桶,一整趟里会反复攒满、反复发货。还有个换语言才冒头的坑——Java、C++ 里若把桶这只列表直接塞进答案再清空,答案里存的是同一只列表的引用,清桶会把已发出去的那组一起抹掉;得先拷一份进答案,再清原桶。
把七人依次塞进桶
照着 groupSizes=[3,3,3,3,3,1,3] 走。第 0、1 个人都写 3,进 3 号桶,桶里先 1 人、再 2 人,还没满。第 2 个人又写 3,进去凑到 3 人,正好等于桶号,打包出 [0,1,2],3 号桶倒空。第 3、4 个人写 3,再进 3 号桶,攒到 2 人。第 5 个人写 1,进 1 号桶,一进就满员,发出 [5],1 号桶倒空。第 6 个人写 3,进那只刚攒了 2 人的 3 号桶,凑到 3 人,发出 [3,4,6]。七个人扫完,所有桶都空,答案 [[0,1,2],[5],[3,4,6]],三组正好覆盖七人。
扫一趟的开销,还有几个边界
从头到尾只扫一趟,每个人做一次入桶、一次满没满的判断,都是常数;满桶时把整组搬进答案,所有搬运合起来也不过是每人各搬一次,时间 O(n)。空间上,待成组的人加上已发出的组,峰值就是全部 n 个人各占一格,是 O(n)。边界上,写 1 的人一进桶就满、立刻单独成组;全体都写 1 时,每人各成一组、答案有 n 个单人组。题目保证有合法解,意味着写每个数 s 的人数一定是 s 的整数倍,所以扫到最后每只桶都恰好清空,不会剩下半桶凑不齐的人。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢这条主线:按纸条数字入桶、桶满即发组、发完清桶继续攒。下面每一帧都在套这三个动作。
- 4上面这排格子,位置 i 就是第 i 个人,格子里写的数就是他要求的组大小。右边一开始没有桶,扫到谁、需要几号桶,就现开一个。下面从位置 0 开始,一个人一个人地处理。
- 5先粗看一遍纸条:位置 0 到 4 还有位置 6 都写着 3,只有位置 5 写的是 1。写 3 的有六个人,正好能分成两组三人;写 1 的就一个人,自己成一组。心里有数了,正式开桶。
- 6轮到第 0 个人,他的纸条写着 3,意思是「我得待在一个 3 人的组里」。那就把他往 3 号桶里放。
- 7把第 0 个人丢进 3 号桶,桶里现在有 1 个人,离攒满 3 个还差 2 个,继续往下扫。
- 8轮到第 1 个人,他的纸条写着 3,意思是「我得待在一个 3 人的组里」。那就把他往 3 号桶里放。
- 9把第 1 个人丢进 3 号桶,桶里现在有 2 个人,离攒满 3 个还差 1 个,继续往下扫。
- 10轮到第 2 个人,他的纸条写着 3,意思是「我得待在一个 3 人的组里」。那就把他往 3 号桶里放。
- 11把第 2 个人丢进 3 号桶,桶里正好攒到 3 个人,达标了!下一步就把这一桶打包发出去。
- 123 号桶攒够 3 个人了,把整桶 [0,1,2] 当作完整的一组发出去,然后把 3 号桶倒空。注意桶并没有作废,后面再有写 3 的人,还会从空桶重新攒起。
- 13轮到第 3 个人,他的纸条写着 3,意思是「我得待在一个 3 人的组里」。那就把他往 3 号桶里放。
- 14把第 3 个人丢进 3 号桶,桶里现在有 1 个人,离攒满 3 个还差 2 个,继续往下扫。
- 15轮到第 4 个人,他的纸条写着 3,意思是「我得待在一个 3 人的组里」。那就把他往 3 号桶里放。
- 16把第 4 个人丢进 3 号桶,桶里现在有 2 个人,离攒满 3 个还差 1 个,继续往下扫。
- 17轮到第 5 个人,他的纸条写着 1,意思是「我得待在一个 1 人的组里」。那就把他往 1 号桶里放。
- 18把第 5 个人丢进 1 号桶,桶里正好攒到 1 个人,达标了!下一步就把这一桶打包发出去。
- 191 号桶攒够 1 个人了,把整桶 [5] 当作完整的一组发出去,然后把 1 号桶倒空。注意桶并没有作废,后面再有写 1 的人,还会从空桶重新攒起。
- 20轮到第 6 个人,他的纸条写着 3,意思是「我得待在一个 3 人的组里」。那就把他往 3 号桶里放。
- 21把第 6 个人丢进 3 号桶,桶里正好攒到 3 个人,达标了!下一步就把这一桶打包发出去。
- 223 号桶攒够 3 个人了,把整桶 [3,4,6] 当作完整的一组发出去,然后把 3 号桶倒空。注意桶并没有作废,后面再有写 3 的人,还会从空桶重新攒起。
- 23七个人一个不落都进了组,所有桶也恰好都空了。回看一下:[0,1,2] 和 [3,4,6] 都是三人组,对应那六个写 3 的人;[5] 是单人组,对应写 1 的那位。答案 [0,1,2]、[5]、[3,4,6] 成立。
- 24再做个数量校验:三组人数 3 加 1 加 3 等于 7,正好就是总人数,而且每个编号只在一组里出现一次。每个人所在组的大小也都和他纸条写的一致,这个分法稳稳合法。
⚠️ 容易写错的地方
✗ 错:把同样写 3 的人硬塞进同一组、超过 3 个也不发
✓ 对:桶一满 3 人就立刻发组清桶
不及时发组,桶会越攒越多,组大小就超了;攒满即发才能保证每组恰好 size 人
✗ 错:Java / C++ 直接把桶对象塞进答案再 clear
✓ 对:先拷贝一份桶内容进答案,再 clear 原桶
答案里存的是同一个列表引用,清原桶会连答案里那组一起清空,组就丢了
✗ 错:以为发完组桶就没用了、不再复用
✓ 对:清空的桶继续攒下一组同大小的人
示例里 3 号桶攒满 [0,1,2] 发掉后,又重新攒出 [3,4,6],同一个桶要反复用
完整代码(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 *
from string import *
from operator 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 groupThePeople(self, groupSizes: List[int]) -> List[List[int]]:
buckets = defaultdict(list)
ans = []
for i, size in enumerate(groupSizes):
buckets[size].append(i)
if len(buckets[size]) == size:
ans.append(buckets[size])
buckets[size] = []
return ansC++
#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:
vector<vector<int>> groupThePeople(vector<int>& groupSizes) {
unordered_map<int, vector<int>> buckets;
vector<vector<int>> ans;
for (int i = 0; i < (int)groupSizes.size(); ++i) {
int size = groupSizes[i];
buckets[size].push_back(i);
if ((int)buckets[size].size() == size) {
ans.push_back(buckets[size]);
buckets[size].clear();
}
}
return ans;
}
};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 List<List<Integer>> groupThePeople(int[] groupSizes) {
Map<Integer, List<Integer>> buckets = new HashMap<>();
List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i < groupSizes.length; i++) {
int size = groupSizes[i];
buckets.computeIfAbsent(size, k -> new ArrayList<>()).add(i);
if (buckets.get(size).size() == size) {
ans.add(new ArrayList<>(buckets.get(size)));
buckets.get(size).clear();
}
}
return ans;
}
}复杂度
时间
O(n)
n 为人数。从头到尾扫一趟,每个人做一次入桶、一次「是否满」判断,都是常数操作;满桶时把整组搬进答案,所有搬运加起来也只是把每个人各搬一次,总量仍是 O(n)
空间
O(n)
按峰值算:所有桶里待成组的人加上已发出的组,合计最多就是全部 n 个人各占一格,所以是 O(n)。哈希表本身键的种类不超过 n,也在 O(n) 内
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 用户分组 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么不用排序、也不用回溯,一趟扫描就够?+
因为每个人要进多大的组是写死的、彼此独立,而写同一个数的人可以随便互换。于是写 s 的人凑够 s 个就能立刻锁定一组,既不影响别的大小,也不需要反悔。题目又保证有合法解,写每个数 s 的人数必是 s 的整数倍,桶最后都能恰好清空,所以随到随分、一趟就够。
Java、C++ 为什么要先拷贝桶再清空,Python 却不用?+
因为答案里存的是指向那只列表的引用。直接把桶塞进答案再清空,答案里那组和桶指的是同一只列表,会被一起清掉,组就丢了。所以 Java、C++ 要先拷一份进答案,再清原桶。Python 则反过来,把旧列表挂进答案后,给桶换上一只全新的空列表,旧的自然不受影响。
只想要分组数量、不要具体名单,能更省吗?+
能省空间。这时不必记每个人的下标,只要对每种大小 s 存一个当前计数,计到 s 就把组数加一、计数清零。空间从 O(n) 降到不同大小的种数那么多,时间仍是一趟 O(n)。本题要返回具体名单,所以还是得把人的下标攒进桶里。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 用户分组 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。