Notes & software courses · Free to learn
Aph's Blog
On this page

ข้อ 50 · LC2336 Smallest Number in Infinite Set (เลขน้อยสุดเซ็ตอนันต์) 🟡

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

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 กลับเข้าเซ็ต ถ้ามันยังไม่อยู่ในเซ็ตอยู่แล้ว

Example 1
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
Constraints (ข้อจำกัด)
  • เรียก 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 ออกก่อนได้เสมอ

  1. initialize current = 1 (เลขต่อไปในสาย infinity ที่ยังไม่เคย pop), added = min-heap ว่าง, in_heap = set ว่าง
  2. popSmallest: ถ้า heap มีของ (การันตีว่าเล็กกว่า current) pop จาก heap ก่อน แล้ว remove ออกจาก set
  3. ถ้า heap ว่าง pop current แล้ว increment current ขึ้นหนึ่ง
  4. 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:

operationcurrentheap (added)return
popSmallest1 → 2[]1
popSmallest2 → 3[]2
popSmallest3 → 4[]3
addBack(2)4[2]-
popSmallest4[]2
popSmallest4 → 5[]4
popSmallest5 → 6[]5
▶ เฉลยละเอียด (ลองเองก่อนนะ)
เฉลย (Python) — โค้ดนี้รันได้จริงpython
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())  # 5
Output
1
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 ทั้งหมด

💡 สรุป pattern

เมื่อเจอ set/range (ช่วง) ที่ใหญ่มากหรือ infinity อย่า store ทั้งหมด — ใช้ counter แทนช่วงที่ยัง sorted ดี แล้วใช้ heap เก็บเฉพาะ exception (ข้อยกเว้น คือเลขที่โดนเพิ่มกลับมา) ที่ทำให้ลำดับผิดจากปกติ