分组的最大数量 图解题解
这道题到底在问什么
- 输入
- grades = [10,6,12,7,3,5]
- 输出
- 3 (人数 1,2,3 三组)
- 输入
- grades = [8,8]
- 输出
- 1 (分两组人数就会相等,不行)
最优解:为什么这么做
一句话答案:LeetCode 2358 分组的最大数量:分成有序几组、后组人数与总成绩递增,求最多组数。二分答案:升序切块让总成绩自动成立,只看人数——最大 k 使 k(k+1)/2 ≤ n,在 0~n 二分,O(log n) 时间、O(1) 空间。
分组要同时满足人数和总成绩递增,求的是什么
把学生按成绩分成有序几组,要求后一组人数比前一组多、后一组总成绩也更高,返回最多能分几组。人数除不尽也不碍事:多出的学生并进最后一组即可——只要求人数严格递增,末组更大不违规。题面 grades=[10,6,12,7,3,5] 有 6 个人,答案是 3(人数 1、2、3 三组);换成 grades=[8,8] 只两人,硬分两组人数就相等,最多 1 组。
为什么『后一组总成绩更高』这条根本不用管
先把成绩从小到大排好,再从左往右一块块切给各组:第一组拿最小的几个,第二组往后拿更多。后一组人数更多,且每人成绩都不小于前一组任何人——人更多、每个还不更小,总成绩必然更大。所以人数一严格递增,总成绩就自动跟着递增,成绩的具体数值不影响答案,干脆全扔掉、只盯人数。
问题缩成:最大的 k 使 k(k+1)/2 ≤ n
人数要严格递增,分成 k 组最省人的排法就是 1、2、3、…、k 一路加一,共要 k(k+1)/2 个人(三角数)。只要它不超过 n 就一定分得出来。所以能否分成 k 组等价于 k(k+1)/2 ≤ n,答案就是让它成立的最大 k。题面 n 是 6 时 1+2+3=6 恰好用完可分 3 组;再加一组要 10 个人就凑不齐。
逐个试 k 太慢,怎么换成二分
n 能到十万,从 k 取 1 逐个往上试,最坏试到答案那档,白扫一截。好在 k(k+1)/2 随 k 单调变大,所有 k 被切成两半:小的都 ≤ n、大的都超 n,答案正是分界上最后一个满足的 k。这种不套公式、而是猜一个 k 用 k(k+1)/2 ≤ n 验一验再往一边收的思路,就是二分答案(二分的是答案本身,不是下标)。参考代码两边乘 2 成 k²+k ≤ 2n,bisect_right 在 range(n+1) 上按 key x²+x 找第一个超过 2n 的位置,减 1 即最大可行 k。下面演算用手写 l、r 二分展开,bisect_right 就是它的库函数封装。
题面 6 个人这组,二分三轮各砍向哪半
总人数 6,两倍即 2n=12,在 0 到 6 间找最大的 k 使 k²+k ≤ 12。左界 l 起于 0、右界 r 起于 6。第一轮中点向上取整取 3,3²+3=12 不超 12,分 3 组可行、这个中点可能就是答案,把 l 拉到 3。第二轮 [3,6] 中点取 5,5²+5=30 超 12,分 5 组人不够,r 退到 4。第三轮 [3,4] 中点取 4,4²+4=20 仍超 12,r 压到 3。此时 l、r 都停在 3,二分结束,答案 3,正是前面切出的三组。
等号一丢、中点取整一错,答案就少一组
别真在成绩上凑总成绩——切块后它自动成立。等号也别丢:n 是 6 时 T(3) 正好等于 6,人刚好用完仍可行,写成严格小于会把这组判没、答案少一组。手写二分还藏个暗坑:找『最大可行』时中点要向上取整、配 l=mid 与 r=mid−1;若向下取整,区间剩两格时中点永远卡左端、l 推不动,就死循环。复杂度上,答案只由人数 n 定、不遍历成绩数组,时间 O(log n)、空间 O(1)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这句:只要人数 1,2,3,... 严格递增,总成绩自动跟着递增,答案就是最大的 k 使 1+2+...+k ≤ n。下面分三段演给你看。
- 4先看原始的 6 个成绩,乱序排着。直接盯着成绩去凑「总成绩递增」很难,我们换个角度:先把成绩从小到大排好。
- 5升序排好后是 [3,5,6,7,10,12]。接下来从左往右切:第 1 组拿 1 个人,第 2 组拿 2 个人,第 3 组拿 3 个人,人数严格递增。看看总成绩会不会自动跟着涨。
- 6第 1 组从左边接着拿 1 个人,拿到的是 [3]。蓝色是前面已经分好的组。
- 7第 1 组总成绩 3。它是第一组,先记着,和下一组比。
- 8第 2 组从左边接着拿 2 个人,拿到的是 [5, 6]。蓝色是前面已经分好的组。
- 9第 2 组总成绩 11,比上一组的 3 大。注意我们根本没刻意去凑,只因为这组人更多、每个人成绩都不小于上一组,总和自然更高。
- 10第 3 组从左边接着拿 3 个人,拿到的是 [7, 10, 12]。蓝色是前面已经分好的组。
- 11第 3 组总成绩 29,比上一组的 11 大。注意我们根本没刻意去凑,只因为这组人更多、每个人成绩都不小于上一组,总和自然更高。
- 12三组都切好了:人数 1、2、3 严格递增,总成绩 3、11、29 也严格递增,两个条件全满足。可见「总成绩」这条根本没拦住我们,真正的瓶颈只是「人数够不够凑出严格递增」。
- 13既然只跟人数有关,干脆把成绩全扔掉。这排格子代表候选组数 k,从 1 开始逐个试:分 k 组至少要 1+2+...+k 个人,记成 T(k)。只要 T(k) 不超过总人数 6,这个 k 就可行。
- 14试 k = 1:至少要 1 = 1 个人。不超过 6,可行,标绿。继续看更大的 k。
- 15试 k = 2:至少要 1+2 = 3 个人。不超过 6,可行,标绿。继续看更大的 k。
- 16试 k = 3:至少要 1+2+3 = 6 个人。正好等于 6,人刚好用完,仍然可行,标绿。继续看更大的 k。
- 17试 k = 4:至少要 1+2+3+4 = 10 个人,已经超过 6 了,凑不出来,标红。既然 T(k) 只增不减,再大的 k 更不可能,到此为止。最后一个绿色的 k = 3 就是答案。
- 18逐个试当然行,但 n 能到十万,得用二分。把条件 k(k+1)/2 ≤ n 两边乘 2,写成 k²+k ≤ 2n,这里 2n = 12。在 0 到 6 之间二分,找最大的满足它的 k。
- 19区间 [0, 6],中点向上取整 mid = 3。算 3²+3 = 12,和 12 比:不超过,说明分 3 组可行,这个 mid 本身可以成为答案。
- 20因为可行,把下界 l 拉到 3,绿色候选也跟着到 3。答案至少 3,还想更大,所以去 mid 右边接着找。
- 21区间 [3, 6],中点向上取整 mid = 5。算 5²+5 = 30,和 12 比:超过了,分 5 组人不够,mid 及它右边都不行。
- 22因为不可行,把上界 r 压到 mid - 1 = 4,mid 和它右边整段排除。区间越缩越小。
- 23区间 [3, 4],中点向上取整 mid = 4。算 4²+4 = 20,和 12 比:超过了,分 4 组人不够,mid 及它右边都不行。
- 24因为不可行,把上界 r 压到 mid - 1 = 3,mid 和它右边整段排除。区间越缩越小。
- 25l 和 r 碰头都停在 3,二分结束。绿色格 3 就是最大能分的组数,答案 = 3,和前面一块一块切出来的 3 组完全对上。
⚠️ 容易写错的地方
✗ 错:真去贪心凑总成绩
✓ 对:答案只跟人数 n 有关,成绩数值完全不影响
升序按块切,总成绩自动递增,别在成绩上白费力气
✗ 错:T(k) ≤ n 的等号漏掉
✓ 对:正好用完也算可行,要用 ≤
n=6 时 T(3)=6 恰好可行,写成严格小于会少一组
✗ 错:二分方向或取整写错
✓ 对:mid 向上取整 + l=mid / r=mid-1
找「最大可行」这类二分,mid 不向上取整会死循环
完整代码(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 Solution:
def maximumGroups(self, grades: List[int]) -> int:
n = len(grades)
return bisect_right(range(n + 1), n * 2, key=lambda x: x * x + x) - 1C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#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;
class Solution {
public:
int maximumGroups(vector<int>& grades) {
int n = grades.size();
int l = 0, r = n;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (1LL * mid * mid + mid > n * 2LL) {
r = mid - 1;
} else {
l = mid;
}
}
return l;
}
};Java
import java.util.*;
class Solution {
public int maximumGroups(int[] grades) {
int n = grades.length;
int l = 0, r = n;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (1L * mid * mid + mid > n * 2L) {
r = mid - 1;
} else {
l = mid;
}
}
return l;
}
}复杂度
时间
O(log n)
在 0..n 上二分,每次砍一半
空间
O(1)
只用 l、r、mid 几个变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 分组的最大数量 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么可以完全不管『后一组总成绩更高』这个条件?+
因为它是白给的。把成绩升序排好,再按人数从小到大一块块切给各组:后一组人数更多,且它里面每个人的成绩都不小于前一组的任何人。人更多、每个还都不更小,总和自然更大。所以人数一旦严格递增,总成绩必然跟着严格递增,这个条件不构成任何额外限制,可以直接忽略,把全部注意力放在人数够不够上。
为什么最大组数就是让 k(k+1)/2 ≤ n 成立的最大 k?+
人数必须严格递增,分成 k 组最省人的方案就是让各组人数是 1、2、3、…、k,加起来 k(k+1)/2 个人。只要这个最小需求不超过 n,就一定能分:把多出来的人全并进最后一组,人数递增照样成立。所以『能不能分 k 组』完全等价于 k(k+1)/2 ≤ n,那个让不等式成立的最大 k 就是答案。
为什么这里二分的中点要向上取整?+
这是『找最大可行值』型二分的固定配法。它的收缩写成可行时 l=mid(留住当前中点)、不可行时 r=mid−1;若中点按普通向下取整算,当区间只剩 l、r 相邻两格时,中点算出来永远等于 l,l=mid 等于没动,区间缩不动就无限循环。向上取整让中点在相邻两格时偏到右边那格,l 才推得动。反过来『找最小可行值』型用 l=mid+1、r=mid,中点则向下取整,两套配法别混。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 分组的最大数量 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。