On this page
ข้อ 50 · LC2336 Smallest Number in Infinite Set (เลขน้อยสุดเซ็ตอนันต์) 🟡
design (ออกแบบ) class จัดการ set (เซ็ต) ของ positive integer (จำนวนเต็มบวก) ทั้งหมด ด้วย counter (ตัวนับ) current + min-heap สำหรับเลขที่ addBack กลับมา
โจทย์ (LC2336): ให้ออกแบบ class ชื่อ SmallestInfiniteSet ที่แทนเซ็ตซึ่งบรรจุ positive integer (จำนวนเต็มบวก) ทุกตัวตั้งแต่ 1, 2, 3, ... ไปจนถึงอนันต์ตั้งแต่เริ่มต้น โดยต้อง support (รองรับ) สอง method คือ popSmallest() ซึ่ง remove และ return ค่าที่น้อยที่สุดที่ยังอยู่ในเซ็ต และ addBack(num) ซึ่งเพิ่มจำนวนเต็มบวก num กลับเข้าเซ็ต ถ้ามันยังไม่อยู่ในเซ็ตอยู่แล้ว
- Input:
- new SmallestInfiniteSet(); addBack(2); popSmallest(); popSmallest(); popSmallest(); addBack(1); popSmallest(); popSmallest(); popSmallest()
- Output:
- null, null, 1, 2, 3, null, 1, 4, 5
- Explanation:
- addBack(2) ไม่มีผลเพราะ 2 ยังอยู่ในเซ็ตอยู่แล้ว popSmallest สามครั้งแรกคืน 1, 2, 3 ตามลำดับ จากนั้น addBack(1) เพิ่ม 1 กลับเข้าไป popSmallest ครั้งถัดไปจึงคืน 1 (ตัวที่เพิ่งเพิ่มกลับ เพราะเล็กกว่า 4) แล้วค่อยเดินหน้าต่อที่ 4 และ 5
- เรียก popSmallest และ addBack รวมกันได้มากสุด 1000 ครั้ง
- 1 <= num <= 1000
แนวทาง — ต้องใช้อะไร & คิดยังไง
โครงสร้างที่ใช้: counter (ตัวนับ) current หนึ่งตัว + min-heap หนึ่งอัน + set กันเลขซ้ำ เราไม่ต้อง store เลข infinity จริง ๆ เพราะเลขในช่วง current, current+1, ... ยัง sorted (เรียงเป็นระเบียบ) อยู่แล้ว แค่ track (จำ) ว่าถึงไหนก็พอ
คิดแบบง่าย/ช้าก่อน: ถ้า store เลขทั้งหมดจริง ๆ จะเป็นไปไม่ได้เพราะ infinity ปัญหาเดียวที่ต้อง handle แยกคือเลขที่ถูก addBack กลับมา ซึ่งอาจเล็กกว่า current เราจึง store มันใน min-heap เพื่อให้ pop ตัว minimum ออกก่อนได้เสมอ
- initialize current = 1 (เลขต่อไปในสาย infinity ที่ยังไม่เคย pop), added = min-heap ว่าง, in_heap = set ว่าง
- popSmallest: ถ้า heap มีของ (การันตีว่าเล็กกว่า current) pop จาก heap ก่อน แล้ว remove ออกจาก set
- ถ้า heap ว่าง pop current แล้ว increment current ขึ้นหนึ่ง
- addBack(num): เพิ่มกลับได้เฉพาะเลขที่ถูก pop ไปแล้ว (num < current) และยังไม่อยู่ใน heap จึง push เข้า heap และ add เข้า set
ลืมกันเลขซ้ำใน heap — ถ้า addBack(2) สองครั้งโดยไม่มี set คุม heap จะมีเลข 2 สองตัว ทำให้ popSmallest return 2 ซ้ำ ผิดความหมายของ set และ addBack เลขที่ยังไม่เคย pop (num >= current) ต้องไม่ทำอะไร เพราะมันยังอยู่ใน set อยู่แล้ว
ไล่ทีละสเต็ป
จำลอง sequence ของ operation pop, pop, pop, addBack(2), pop, pop, pop:
| operation | current | heap (added) | return |
|---|---|---|---|
| popSmallest | 1 → 2 | [] | 1 |
| popSmallest | 2 → 3 | [] | 2 |
| popSmallest | 3 → 4 | [] | 3 |
| addBack(2) | 4 | [2] | - |
| popSmallest | 4 | [] | 2 |
| popSmallest | 4 → 5 | [] | 4 |
| popSmallest | 5 → 6 | [] | 5 |
▶ เฉลยละเอียด (ลองเองก่อนนะ)
import heapq
class SmallestInfiniteSet:
def __init__(self):
self.current = 1 # เลขต่อไปในสาย 1,2,3,... ที่ยังไม่เคยหยิบ
self.added = [] # min-heap ของเลขที่ถูก addBack กลับมา
self.in_heap = set() # กันไม่ให้ heap มีเลขซ้ำ
def popSmallest(self):
# ถ้ามีเลขที่ addBack กลับมา และมันเล็กกว่า current ให้หยิบจาก heap ก่อน
if self.added:
x = heapq.heappop(self.added)
self.in_heap.discard(x)
return x
# ไม่งั้นหยิบตัวถัดไปจากสายอนันต์
x = self.current
self.current += 1
return x
def addBack(self, num):
# เพิ่มกลับได้เฉพาะเลขที่ถูกหยิบออกไปแล้ว (num < current) และยังไม่อยู่ใน heap
if num < self.current and num not in self.in_heap:
heapq.heappush(self.added, num)
self.in_heap.add(num)
s = SmallestInfiniteSet()
print(s.popSmallest()) # 1
print(s.popSmallest()) # 2
print(s.popSmallest()) # 3
s.addBack(2)
print(s.popSmallest()) # 2
print(s.popSmallest()) # 4
print(s.popSmallest()) # 51
2
3
2
4
5กุญแจของโจทย์คือ เราไม่จำเป็นต้อง store เลข infinity จริง เพราะเลขในช่วง current เป็นต้นไปยัง sorted อยู่แล้ว แค่ track ว่าถึงไหนก็พอ ปัญหาเดียวคือเลขที่ถูก addBack กลับมา ซึ่งอาจเล็กกว่า current เราจึง store มันแยกใน min-heap เพื่อให้ pop ตัว minimum ออกก่อนได้เสมอ
เวลา popSmallest เราจึง compare (เทียบ) ง่าย ๆ: ถ้ามีของใน heap (ซึ่งการันตีว่าเล็กกว่า current) pop จาก heap ก่อน ไม่งั้นค่อยเดินสาย infinity ต่อ ถ้าตัด set in_heap ออก โค้ดจะยอมให้ addBack เลขเดิมซ้ำได้ ทำให้ heap มีค่าซ้ำและ popSmallest return ค่าเดียวกันสองครั้ง ผิดนิยามของ set
Time popSmallest O(log n) และ addBack O(log n) โดย n คือจำนวนเลขใน heap · Space O(n) store เฉพาะเลขที่ถูก addBack กลับมา ไม่ใช่เลข infinity ทั้งหมด
เมื่อเจอ set/range (ช่วง) ที่ใหญ่มากหรือ infinity อย่า store ทั้งหมด — ใช้ counter แทนช่วงที่ยัง sorted ดี แล้วใช้ heap เก็บเฉพาะ exception (ข้อยกเว้น คือเลขที่โดนเพิ่มกลับมา) ที่ทำให้ลำดับผิดจากปกติ