LeetCode 2336. 无限集中的最小数字
题目描述


题意分析
集合一开始包含所有正整数。
popSmallest()取出并删除当前最小元素,addBack(num)在该正整数不在集合中时将它放回。集合不能包含重复元素,重复加回没有效果;从未被取出的数本来就在集合中,也无需重新添加。虽然范围无限,但操作只会改变有限数量的已取出值,不需要真的存下全部正整数。
解法:连续尾部边界与加回堆
核心思路
[!blue]
用边界
next表示从未被取出的连续后缀[next, +∞),其中全部数都还在集合中。比next小的数已经至少被取出过,它们只有在后来被加回、并且还未再次弹出时,才仍属于集合。把这部分加回的较小值放入最小堆
back。整个集合就表示为“堆中有限个离散值”加“边界开始的无限连续后缀”。堆中所有值都小于next,所以堆非空时全局最小值一定是堆顶;堆空时才返回next,并将后缀边界加一。
addBack只需处理num < next的值,较大值原本还在连续后缀中。为了防止同一个值重复入堆,另外用哈希集合inBack记录堆中的成员;不在其中才同时加入集合与堆。弹出堆顶时也从集合删除,让这个数将来可以再次被加回。边界只在第一次取出连续后缀的最小值时前进,不会因弹出加回值而移动。这样每次操作都维持两个部分互不重叠、合起来恰好是当前无限集合的状态。
解题步骤
- 初始化
next = 1,加回堆与判重集合都为空。- 弹出最小时,堆非空就取堆顶,并删除其判重标记。
- 堆为空则返回当前
next,再将边界加一。- 加回时,只有值小于
next且尚不在堆中,才同时加入判重集合与最小堆。- 其他加回操作不改变状态。
代码实现
class SmallestInfiniteSet {
// 不变量:[next, +∞) 全在集合中;小于 next 的数只有在 back 里的才在集合中。
private int next = 1;
private PriorityQueue<Integer> back = new PriorityQueue<>();
// 堆查存在性是 O(n),用哈希集合专门负责判重,两者内容始终同步。
private Set<Integer> inBack = new HashSet<>();
public SmallestInfiniteSet() {}
public int popSmallest() {
if (!back.isEmpty()) {
// back 里的数一律小于 next,非空时全局最小值必在其中。
int num = back.poll();
inBack.remove(num);
return num;
}
// 集合退化成 [next, +∞),取走后边界右移一格。
return next++;
}
public void addBack(int num) {
// num >= next 说明它从未被弹出,仍在集合中,按题意忽略。
if (num < next && inBack.add(num)) {
back.offer(num);
}
}
}
import (
"container/heap"
)
type SmallestInfiniteSet struct {
// 不变量:[next, +∞) 全在集合中;小于 next 的数只有在 back 里的才在集合中。
next int
back *IntHeap
inBack map[int]bool
}
func Constructor() SmallestInfiniteSet {
return SmallestInfiniteSet{next: 1, back: &IntHeap{}, inBack: map[int]bool{}}
}
func (s *SmallestInfiniteSet) PopSmallest() int {
if s.back.Len() > 0 {
// back 里的数一律小于 next,非空时全局最小值必在其中。
num := heap.Pop(s.back).(int)
delete(s.inBack, num)
return num
}
// 集合退化成 [next, +∞),取走后边界右移一格。
s.next++
return s.next - 1
}
func (s *SmallestInfiniteSet) AddBack(num int) {
// num >= next 说明它从未被弹出,仍在集合中,按题意忽略。
if num < s.next && !s.inBack[num] {
s.inBack[num] = true
heap.Push(s.back, num)
}
}
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
复杂度分析
- 时间复杂度:构造为 $O(1)$;设操作期间加回堆的最大规模为
q,实际入堆、出堆为 $O(\log(q+1))$,从连续后缀取数或忽略重复添加为期望 $O(1)$。- 空间复杂度:$O(q+1)$,堆与判重集合只保存有限的加回元素,连续无限后缀只用一个边界表示。这里用最大规模计入容器可能保留的容量。
关键点总结
[!green]
- 未取过的连续无限部分不逐个存储,边界就能完整表示。
- 加回堆中值都小于边界,决定弹出时必须优先看堆。
- 堆负责最小值,集合负责去重,两者成员始终同步。
- 同一个值可以多次经历取出和加回,但同时最多存在一份。
易错点总结
[!yellow]
- 将
num >= next的值也加入堆,它本来就在无限后缀,会制造重复元素。- 不对加回值去重,连续多次添加后同一个数可能被重复弹出。
- 弹出堆顶后不删除判重标记,之后合法的再次加回会被错误拒绝。
- 弹出加回值时也推进
next,跳过了仍然存在的后缀首项。- 试图预先创建整个无限集合,或者随意截取固定上限,偏离了有限操作状态的表示方式。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 1845. 座位预约管理系统 | 中等 | 同样分配最小可用编号并支持归还,原题编号范围有限,本题用边界表示无限尾部。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!