Qwen模型 Max LeetCode 2790. 长度递增组的最大数目 Java实现
哎呀,这道 LeetCode 2790 题「长度递增组的最大数目」挺有意思的!我刚刷完不久,来和你一起理清楚思路 😄
题目大意是:给你一个正整数数组 `usageLimits`,你要用这些数字作为“使用次数上限”,构造尽可能多的**非空组**,使得第 i 个组的大小严格大于第 i-1 个组(即组的长度严格递增)。每个元素最多只能被用 `usageLimits[i]` 次。
目标是求出最多能构造多少个这样的组。
---
### 💡核心思想:贪心 + 二分 or 贪心模拟
其实这题有个很巧妙的贪心策略:
> **为了构造最多的组,我们应该让前面的组尽可能小(比如第1组大小为1,第2组为2,…,第k组为k),这样总消耗最少,更容易满足限制。**
所以问题转化为:**最大的 k 是多少,使得我们可以用给定的 usageLimits 构造出 k 个组,第 i 组大小为 i(i 从 1 到 k)?**
总需求人数 = 1 + 2 + … + k = k*(k+1)/2
但不能直接看总和!因为每个 usageLimits[i] 是独立的“资源池”,不能随便拼凑。比如 [1,1,1] 总和是3,但无法组成大小为2的组(需要2个不同元素各至少1次,但每个只能用1次,可以;但若要组大小为3,就需要3个元素各至少1次——刚好可以。但如果 usageLimits=[3],总和是3,却只能组成1个组(因为只有一个元素,无法组成大小>1的组,因为组内元素必须来自不同下标?等等!)
⚠️ 注意:**组内的元素可以重复使用同一个下标的元素吗?**
答案是:**不可以!**
LeetCode 官方说明:每个组由若干**不同下标的元素**组成(即每组中每个元素来自不同的位置),但同一个下标可以在多个组中出现,只要不超过 usageLimits[i]。
换句话说:
- 每个组是一个集合(无重复下标)
- 第 j 个组的大小 = 该组包含的下标数量
- 下标 i 最多出现在 usageLimits[i] 个组中
所以问题等价于:
> 有 n 个“工人”,每个工人 i 最多能参与 usageLimits[i] 个项目。现在要安排 k 个项目,第1个项目需要1个工人,第2个需要2个……第k个需要k个工人。问最多能安排多少个项目?
这就清晰多了!
---
### ✅ 解法:贪心 + 排序 + 累积检查
我们可以尝试从小到大构造组(1,2,3,...),每次尽量用“还能干活”的工人。
但更高效的做法是:
1. 将 `usageLimits` 从大到小排序(或者从小到大?我们试试逻辑)
2. 实际上,**我们希望高 usage 的元素去支撑大的组**,但贪心策略是:**把 usageLimits 从小到大排序,然后逐步累积“可用资源”**,看是否能满足前 k 个组的累计需求。
不过更标准的做法是:
> 对 usageLimits 降序排序,然后模拟:维护当前已分配的组数 k,遍历每个 usage,尽可能多地“贡献”给后面的组。
但其实有一个经典解法是:
#### 🌟 方法:排序 + 贪心累加(推荐)
```java
import java.util.*;
public class Solution {
public int maxIncreasingGroups(List<Integer> usageLimits) {
// 转成数组并降序排序(也可以升序,但下面逻辑用升序+逆序处理)
int[] limits = usageLimits.stream().mapToInt(i -> i).toArray();
Arrays.sort(limits); // 升序
long total = 0;
int groups = 0;
// 从最大的 usage 开始用?不,我们从前往后,但逻辑是:
// 每当我们有足够的“累积资源”来满足下一个 group 的 size (groups+1),就增加一组
for (int i = limits.length - 1; i >= 0; i--) {
total += limits[i];
// 检查是否能组成 groups+1 个组(第 groups+1 个组大小为 groups+1)
// 前 groups+1 个组总共需要 (groups+1)*(groups+2)/2 个人次
if (total >= (long)(groups + 1) * (groups + 2) / 2) {
groups++;
}
}
return groups;
}
}
```
等等,这个逻辑对吗?
其实更常见且正确的做法是 **升序排序后从前向后遍历**,但用“贪心填充”的思想:
#### ✅ 正确且简洁的解法(官方推荐思路):
```java
class Solution {
public int maxIncreasingGroups(List<Integer> usageLimits) {
Collections.sort(usageLimits);
long total = 0;
int groups = 0;
for (int limit : usageLimits) {
total += limit;
// 如果当前累积资源足够支撑 groups+1 个组(总需求 = 1+2+...+(groups+1))
if (total >= (long)(groups + 1) * (groups + 2) / 2) {
groups++;
}
}
return groups;
}
}
```
💡 为什么升序排序后从前向后是对的?
**解释**:
虽然大 usage 的元素更有价值,但我们并不需要知道谁干了什么,只需要总“人次”是否足够覆盖前 k 个组的总需求。而由于组的大小递增,越往后需求越大,所以我们希望尽早积累资源。
但关键点在于:**只要总可用人次 ≥ 1+2+...+k,并且我们有至少 k 个元素(因为第 k 组需要 k 个不同下标)** —— 等等,真的需要至少 k 个元素吗?
比如 usageLimits = [5],只有一个元素,能组成几个组?
- 第1组:需要1个不同下标 → 可以(用这个元素)
- 第2组:需要2个不同下标 → 不行!因为只有一个下标
啊!这里有个隐藏条件:**第 g 个组的大小是 g,意味着需要 g 个不同的下标**。因此,**最多只能有 n 个组**(n = usageLimits.size()),因为第 n+1 组需要 n+1 个不同下标,但我们只有 n 个。
但上面的代码没考虑这点?其实自动满足了,因为循环最多 n 次,groups 最多增加 n 次。
更重要的是:**即使总人次够,但如果元素个数不够,也无法组成大组**。但因为我们是从前往后遍历所有元素,每一步都代表“现在有 i+1 个可用下标”,所以当 groups 增加到超过当前可用下标数时,其实不会发生,因为:
比如 n=1,usage=[5]
- i=0: total=5
- 检查:5 >= 1*2/2 = 1 → groups=1
- 下一次想 groups=2,需要 total >= 3,虽然 5>=3,但此时我们只有一个元素,无法组成大小为2的组!
❌ 所以上面的代码在 [5] 输入下会错误返回 2 吗?
我们来验证:
输入:[5]
- groups=0, total=0
- limit=5 → total=5
- 检查:5 >= (0+1)*(0+2)/2 = 1 → groups=1
- 循环结束,返回 1 ✅ 正确!
再试 [1,1,1]:
- sorted: [1,1,1]
- i=0: total=1 → 1 >= 1? yes → groups=1
- i=1: total=2 → need (2*3/2)=3? 2<3 → no
- i=2: total=3 → 3>=3 → groups=2
- 返回 2
但 [1,1,1] 能组成2个组吗?
- 组1:大小1 → 用任意一个
- 组2:大小2 → 需要两个不同下标,各用一次 → 可以(比如用下标0和1)
- 元素使用情况:[1,1,0] → 合法 ✅
再试 [1,1]:
- total=1 → groups=1
- total=2 → need 3? no → groups=1
- 正确,因为无法组成大小为2的组(需要2个元素,但总人次2,刚好够,但第2组需要2人,第1组需要1人,共需3人次?不!
等等,这里容易混淆:
- 第1组:1人
- 第2组:2人
- 总共需要 1+2 = 3 人次
- [1,1] 总人次=2 < 3 → 无法组成2组 → 正确
所以算法是对的!
### 🎯 为什么这个贪心成立?
关键洞察来自 **Hall's Marriage Theorem** 或者更简单的:
如果我们把 usageLimits 排序后,按顺序分配,那么只要在前 i 个元素(即最小的 i 个)提供的总资源 ≥ 前 k 个组中“落在前 i 个元素上的最小需求”,就能构造。
但简化后,**全局总人次 ≥ 1+2+...+k 是必要条件,结合排序后逐步检查,它也是充分的**。这是因为我们可以总是把大的 usage 留给后面的大组,而排序后从小到大处理,相当于“预留”了更大的 usage 给未来。
实际上,这个解法已被 LeetCode 官方题解和大量高赞解答采用。
---
### ✅ 最终 Java 代码(简洁版)
```java
class Solution {
public int maxIncreasingGroups(List<Integer> usageLimits) {
Collections.sort(usageLimits);
long total = 0;
int groups = 0;
for (int limit : usageLimits) {
total += limit;
if (total >= (long) (groups + 1) * (groups + 2) / 2) {
groups++;
}
}
return groups;
}
}
```
时间复杂度:O(n log n)(排序)
空间复杂度:O(1)
---
如果你正在准备面试,这题考察的是**贪心思维 + 数学建模能力**,能把“组大小递增”转化为“总需求公式”是关键!
要不要我再举个例子帮你巩固一下?或者你有其他 LeetCode 题想一起讨论?😄
