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

ข้อ 25 · LC735 Asteroid Collision (ดาวเคราะห์น้อยชนกัน) 🟡

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

ดาวพุ่งขวา (+) กับพุ่งซ้าย (−) ชนกันบนเส้นตรง — ใช้ Stack เก็บผู้รอด แล้วให้ดวงใหม่ไล่ประลองกับตัวบนสุด

โจทย์ (LC735): กำหนด array asteroids ของจำนวนเต็มแทนดาวเคราะห์น้อยที่เรียงกันอยู่บนเส้นตรงเดียวกัน สำหรับดาวเคราะห์น้อยแต่ละดวง ค่าสัมบูรณ์คือขนาดของมัน และเครื่องหมายคือทิศทาง (บวก = เคลื่อนที่ไปทางขวา, ลบ = เคลื่อนที่ไปทางซ้าย) ทุกดวงเคลื่อนที่ด้วยความเร็วเท่ากัน ให้หาสภาพของดาวเคราะห์น้อยทั้งหมดหลังการชนจบลง กติกาการชน: ถ้าดาวเคราะห์น้อยสองดวงมาชนกัน ดวงที่ขนาดเล็กกว่าจะแตกหายไป ถ้าขนาดเท่ากันทั้งคู่จะแตกหายไปทั้งคู่ ดาวเคราะห์น้อยสองดวงที่เคลื่อนที่ไปทิศทางเดียวกันจะไม่มีวันชนกัน

Example 1
Input:
asteroids = [5, 10, -5]
Output:
[5, 10]
Explanation:
10 กับ -5 ชนกัน 10 ใหญ่กว่าจึงรอด (−5 แตก) ส่วน 5 กับ 10 วิ่งทิศทางเดียวกัน (ขวาทั้งคู่) จึงไม่มีวันชนกัน
Example 2
Input:
asteroids = [8, -8]
Output:
[]
Explanation:
8 กับ −8 ขนาดเท่ากัน ชนกันแล้วแตกหายไปทั้งคู่
Example 3
Input:
asteroids = [10, 2, -5]
Output:
[10]
Explanation:
2 กับ −5 ชนกันก่อน −5 ใหญ่กว่าจึงรอด (2 แตก) จากนั้น 10 กับ −5 ชนกัน 10 ใหญ่กว่าจึงรอด
Example 4
Input:
asteroids = [-2, -1, 1, 2]
Output:
[-2, -1, 1, 2]
Explanation:
−2 กับ −1 วิ่งไปทางซ้ายทั้งคู่ ส่วน 1 กับ 2 วิ่งไปทางขวาทั้งคู่ ดาวที่วิ่งทิศทางเดียวกันไม่มีวันชนกัน จึงไม่มีการชนเกิดขึ้นเลย
Constraints (ข้อจำกัด)
  • 2 <= asteroids.length <= 10^4
  • -1000 <= asteroids[i] <= 1000
  • asteroids[i] != 0 (เครื่องหมายบอกทิศ ค่าบอกขนาด)
เฉลยเต็ม · ซ่อนไว้ให้ลองเองก่อนพับไว้ด้านใน — คลิกเมื่อพร้อมดู

ข้อนี้เห็นภาพชัดว่าทำไมต้องใช้ Stack — เก็บผู้รอดไว้ในกระป๋อง แล้วให้ดวงใหม่ไล่ประลองกับตัวบนสุด

1. ปลดล็อกไอเดีย (Mindset Shift)

โจทย์จำลองอวกาศที่มีดาวเคราะห์น้อยเรียงแถวกัน:

  • ค่าบวก (+) = ดาวพุ่งไปทาง "ขวา"
  • ค่าลบ (−) = ดาวพุ่งไปทาง "ซ้าย"
  • ตัวเลข = ขนาดของดาว

หัวใจสำคัญ: ดาวชนกันได้กรณีเดียวเท่านั้น — "ดาวดวงซ้ายพุ่งขวา (+) และดาวดวงขวาพุ่งซ้าย (−)" วิ่งมาประสานงา (เหมือนรถวิ่งสวนเลน) · ชนแล้วดวงเล็กระเบิด · ขนาดเท่ากันระเบิดทั้งคู่

ใช้ Stack เป็นกระป๋องเก็บผู้รอดชีวิต — หยิบดาวดวงใหม่ทีละดวง ถ้าต้องชนกับตัวบนสุด ก็เปิดลานประลองว่าใครอยู่ใครไป

2. กฎเหล็ก 3 ข้อ (The Logic)

สร้าง Stack ว่าง แล้วหยิบดาวมาพิจารณาทีละดวง (a):

  1. กฎแห่งความสงบสุข — ดาวใหม่ลงกระป๋องได้ปลอดภัยเมื่อ: กระป๋องว่าง หรือ ดาวใหม่พุ่งขวา (+) หรือ ตัวบนสุดพุ่งซ้าย (−) (วิ่งแยกย้าย)
  2. กฎลานประลอง — เปิดเมื่อตัวบนสุดเป็น (+) แต่ตัวใหม่เป็น (−): ตัวบนเล็กกว่า → pop แล้วสู้ต่อ · ขนาดเท่ากัน → pop ทั้งคู่ (alive = False) · ตัวบนใหญ่กว่า → ตัวใหม่ตาย (alive = False)
  3. คนจริงเท่านั้นที่อยู่รอด — ถ้าดาวใหม่เคลียร์กระป๋องได้หมด หรือไม่มีใครให้สู้แล้ว ถึงจะมีสิทธิ์ append เข้า Stack

3. โค้ด Python (LeetCode Ready)

แปลงกฎลานประลองเป็นโค้ด โดยใช้ตัวแปร alive บอกว่าดาวดวงใหม่ยังรอดไหม:

คำตอบสำหรับวางใน LeetCodepython
class Solution:
    def asteroidCollision(self, asteroids: list[int]) -> list[int]:
        stack = []

        for a in asteroids:
            alive = True  # สมมติให้ดาวดวงใหม่รอดไว้ก่อน

            # เปิดลานประลอง: แชมป์เก่าพุ่งขวา (+) เจอ ผู้ท้าชิงพุ่งซ้าย (−)
            while stack and stack[-1] > 0 and a < 0:

                # เช็คขนาด: แชมป์เก่า VS ผู้ท้าชิง (abs ถอดเครื่องหมายก่อนเทียบ)
                if stack[-1] < abs(a):
                    stack.pop()  # แชมป์เก่าตัวเล็กกว่า → ระเบิด!
                    continue     # ผู้ท้าชิงชนะ วนไปสู้คนถัดไป

                elif stack[-1] == abs(a):
                    stack.pop()   # ขนาดเท่ากัน → แชมป์เก่าระเบิด...
                    alive = False # ...และผู้ท้าชิงก็ระเบิดคู่ด้วย

                else:
                    alive = False  # แชมป์เก่าใหญ่กว่า → ผู้ท้าชิงระเบิดตาย

                # ถ้าผู้ท้าชิงตายแล้ว จบการต่อสู้
                break

            # ถ้าผู้ท้าชิงรอด ก็เชิญเข้ากระป๋อง
            if alive:
                stack.append(a)

        return stack

3.5. อธิบายโค้ดฉบับละเอียด — บรรทัดต่อบรรทัด

โค้ดฉบับนี้ใช้หลักการ "เอาดาวดวงใหม่ ไปไล่ชนดาวที่อยู่ใน stack (กล่องเก็บดาว)" ทีละตัวจนกว่าจะรู้ผล ครบทุกบรรทัด:

โค้ดเดิม (อ้างอิง)python
class Solution:
    def asteroidCollision(self, asteroids: list[int]) -> list[int]:
        stack = []

        for a in asteroids:
            alive = True  # สมมติไว้ก่อนว่า ดาวดวงนี้ "รอดชีวิต"

            # เปิดลานประลอง: แชมป์เก่าพุ่งขวา (+) เจอ ผู้ท้าชิงพุ่งซ้าย (−)
            while stack and stack[-1] > 0 and a < 0:

                # เช็คขนาด: แชมป์เก่า VS ผู้ท้าชิง (abs ถอดเครื่องหมายก่อนเทียบ)
                if stack[-1] < abs(a):
                    stack.pop()  # แชมป์เก่าตัวเล็กกว่า → ระเบิด!
                    continue     # ผู้ท้าชิงชนะ วนไปสู้คนถัดไป

                elif stack[-1] == abs(a):
                    stack.pop()   # ขนาดเท่ากัน → แชมป์เก่าระเบิด...
                    alive = False # ...และผู้ท้าชิงก็ระเบิดคู่ด้วย

                else:
                    alive = False  # แชมป์เก่าใหญ่กว่า → ผู้ท้าชิงระเบิดตาย

                # ถ้าผู้ท้าชิงตายแล้ว จบการต่อสู้
                break

            # ถ้าผู้ท้าชิงรอด ก็เชิญเข้ากระป๋อง
            if alive:
                stack.append(a)

        return stack

<b>📦 บล็อก 1 · เตรียมกล่องเก็บดาว</b>

python
stack = []           # กล่องเก็บดาวเคราะห์ที่รอดชีวิต
# สำหรับ [10, 2, -5]: ยังไม่วนลูปเลย → stack = []
Output
stack = []

บรรทัดนี้สร้าง stack ว่าง — เปรียบเสมือนกระป๋อง Pringles ว่างๆ รอดรับดาวที่รอดชีวิต

<b>📦 บล็อก 2 · ลูปหยิบดาวทีละดวง</b>

python
for a in asteroids:    # หยิบดาวดวงใหม่ (a) มาทีละดวงจากซ้ายไปขวา
    alive = True       # สมมติไว้ก่อนว่า ดาวดวงนี้ "รอดชีวิต"
Output
# ดาวดวงที่ 1: a = 10, alive = True
# ดาวดวงที่ 2: a = 2, alive = True
# ดาวดวงที่ 3: a = -5, alive = True

ลูปนี้จะหยิบดาวจาก array ทีละดวง (a) แล้วตั้งตัวแปร alive = True ไว้ก่อน — สมมติไว้ก่อนว่าดาวดวงใหม่นี้รอดชีวิต จะมาแก้ทีหลังถ้าโดนชนตาย

<b>📦 บล็อก 3 · เงื่อนไขการเข้าสู่สนามชน (while loop)</b>

python
while stack and stack[-1] > 0 and a < 0:
Output
# เงื่อนไขครบ 3 ข้อถึงจะเข้าลูปชน:
# 1. stack: ต้องมีดาวเหลืออยู่ในกล่องให้ชน
# 2. stack[-1] > 0: ดาวตัวล่าสุดในกล่องต้องพุ่งไปทางขวา (➡️)
# 3. a < 0: ดาวดวงใหม่ต้องพุ่งไปทางซ้าย (⬅️)
# (ถ้าไม่ครบ 3 ข้อนี้นี้ แปลว่าไม่ชนกัน มันจะข้ามลูปนี้ไปเลย)

ลูปชนนี้จะทำงาน ก็ต่อเมื่อเข้าเงื่อนไขครบ 3 ข้อนั้นเท่านั้น — เปรียบเสมือนประตูสนามชนที่เปิดเฉพาะเมื่อดาวสองดวงพุ่งสวนทางกัน (➡️ชน⬅️) เท่านั้น

<b>📦 บล็อก 4 · ข้างในลูป: แบ่งเป็่น 3 เคสการชน</b>

python
if stack[-1] < abs(a):
    stack.pop()  # แชมป์เก่าตัวเล็กกว่า → ระเบิด!
    continue     # ผู้ท้าชิงชนะ วนไปสู้คนถัดไป
Output
# เคสที่ 1: ดาวดวงใหม่ใหญ่กว่า
# stack.pop() → เตะดาวตัวล่าสุดในกล่องทิ้ง (เพราะมันระเบิด)
# continue → สั่งให้วนลูป while ซ้ำ เพื่อดึงดาวใหม่ไปชนกับดาวตัวถัดไปในกล่องต่อ

เคสนี้ดาวดวงใหม่ใหญ่กว่า — ดาวในกล่องแตกกระจาย แล้ว continue ให้วนไปชนดาวตัวถัดไปในกล่องทันที (ยังไม่จบ!) เปรียบเสมือนดาวดวงใหม่ไล่ทำลายดาวในกล่องทีละดวง

python
elif stack[-1] == abs(a):
    stack.pop()   # ขนาดเท่ากัน → แชมป์เก่าระเบิด...
    alive = False # ...และผู้ท้าชิงก็ระเบิดคู่ด้วย
Output
# เคสที่ 2: ขนาดเท่ากันเป๊ะ
# stack.pop() → เตะดาวในกล่องทิ้ง (ระเบิด)
# alive = False → ดาวดวงใหม่ก็ตายด้วย (ระเบิดคู่)
# break → จบการชนทันที ข้ามไปดูดาวดวงถัดไปในระบบ

เคสนี้ดาวทั้งคู่ระเบิดพร้อมกัน — ทั้งดาวในกล่องและดาวดวงใหม่ตายหมด (alive = False) แล้ว break ออกจากลูปชน

python
else:
    alive = False  # แชมป์เก่าใหญ่กว่า → ผู้ท้าชิงระเบิดตาย
Output
# เคสที่ 3: ดาวในกล่องใหญ่กว่า
# alive = False → ดาวดวงใหม่สู้ไม่ได้ ระเบิดตายทันที
# break → จบการชนทันที (เพราะดาวใหม่ตายแล้ว ชนต่อไม่ได้)

เคสนี้ดาวในกล่องใหญ่กว่า — ดาวดวงใหม่ต่อสู้ไม่ได้ ระเบิดตายทันที แล้ว break ออกจากลูป

python
                break

# ถ้าผู้ท้าชิงรอด ก็เชิญเข้ากระป๋อง
if alive:
    stack.append(a)
Output
# break → จบการชน (อยู่ภายใต้ while loop)
# ถ้า alive ยังเป็น True → ดาวดวงใหม่ผ่านศึกมา alive → เก็บเข้ากล่อง
# ถ้า alive เป็น False → ดาวดวงใหม่ตาย → ไม่เก็บ

break อยู่ภายใต้ if/elif/else — จะจบลูปชนก็ต่อเมื่อเคสที่ 2 หรือ 3 เกิดขึ้น (มีผู้ตาย) ส่วนเคสที่ 1 จะใช้ continue แทน break เพื่อวนชนต่อ

<b>📦 บล็อก 5 · สรุปผลหลังจบการชน</b>

python
if alive:
    stack.append(a)  # ถ้าดาวดวงใหม่ผ่านศึกมาได้ ยังไม่ตาย ให้เก็บเข้ากล่อง
Output
# alive = True → เก็บดาวเข้า stack
# alive = False → ทิ้งดาวดวงนี้ไป

ถ้าดาวดวงใหม่รอดจากการชนทั้งหมด (alive ยังเป็น True) จึงเก็บเข้า stack — เปรียบเสมือนผู้รอดชีวิตที่ได้เข้าค่าย

<b>🎬 มาดดูเหตุนการณณ์จริงจำลองตามโค้ดนนี่ก้าน!</b>

สมมติโจทย์คือ [10, 2, -5]

python
# ===== ดาวดวงที่ 1 (a = 10) =====
# ลูป while ไม่ทำงาน เพราะ a ไม่น้อยกว่า 0 (10 > 0)
# alive ยังคงเป็น True → เก็บเข้ากล่อง → stack = [10]

# ===== ดาวดวงที่ 2 (a = 2) =====
# ลูป while ไม่ทำงาน เพราะ a ไม่น้อยกว่า 0 (2 > 0)
# alive ยังคงเป็น True → เก็บเข้ากล่อง → stack = [10, 2]

# ===== ดาวดวงที่ 3 (a = -5) =====
# เริ่มเข้า while loop เพราะ:
#   ✅ stack มีของ → [10, 2]
#   ✅ stack[-1] = 2 (> 0) พุ่งขวา ➡️
#   ✅ a = -5 (< 0) พุ่งซ้าย ⬅️

# รอบที่ 1 ใน while:
#   เช็ค if stack[-1] < abs(a) → 2 < 5 (ใช่! เคสที่ 1 ดาวใหม่ใหญ่กว่า)
#     โค้ดทำ stack.pop() → เตะ 2 ออก เหลือ stack = [10]
#     เจอ continue → วนลูป while อีกครั้งทันที!

# รอบที่ 2 ใน while: (ในกล่องเหลือ 10 พุ่งขวา, ดาวใหม่ -5 ยังรอดมาชนต่อ)
#   เช็คเงื่อนไข → เข้าเคส else (ดาวในกล่อง 10 ใหญ่กว่า ดาวใหม่ 5)
#     โค้ดทำ alive = False → ดาวใหม่ตายแล้ว!
#     เจอ break → หลุดออกจากลูป while ทันที

# หลังจบลูป: if alive: (เป็น False) → ไม่เก็บ -5 เข้ากล่อง

# 🎉 ผลลัพธ์สุดท้ายในกล่องเหลือแค่ [10]
Output
stack = []
stack = [10]
stack = [10, 2]
# a = -5 เข้า while loop
# รอบ 1: 2 < 5 → pop() → stack = [10], continue
# รอบ 2: 10 > 5 → alive = False, break
# if alive → False → ไม่ append
# ✅ ผลลัพธ์: [10]

การไล่โค้ดบรรทัดต่อบรรทัดคู่กับตัวอย่างแบบนี้ ช่วยให้เคลียร์และเข้าใจการทำงานของเวอร์ชันนี้ 100% แล้วหรือยังครับ? ถ้าเข้าใจแล้ว เราพร้อมเอาลอจิกนี้ไปประยุกต์กับโจทย์อื่นต่อกันได้เลยนะครับ!

4. จำลองการทำงาน — asteroids = [10, 2, -5]

ดาวทำอะไรstack หลังทำ
10พุ่งขวา ปลอดภัย → โยนลงกระป๋อง[10]
2พุ่งขวา ปลอดภัย → โยนลงกระป๋อง[10, 2]
−5พุ่งซ้าย! ตัวล่าสุด (2) พุ่งขวา → เกิดการชน[10, 2]
ยก 1แชมป์ 2 ปะทะ −5 → แชมป์แพ้! ดึง 2 ออก[10]
ยก 2แชมป์ 10 ปะทะ −5 → ผู้ท้าชิงแพ้ (alive = False)[10]
(จบ)−5 ไม่ได้เข้ากระป๋อง[10]

คำตอบสุดท้ายที่เหลือรอดคือ [10]

5. จุดระวังตกหลุมพราง (Edge Cases)

เคส "วิ่งหนีกัน (แยกย้าย)" — [-5, 5]:

  • ตัวแรก −5 (พุ่งซ้าย) เข้าไปก่อน → stack = [−5]
  • ตัวที่สอง 5 (พุ่งขวา) วิ่งแยกย้ายคนละทาง — ไม่ชน!
  • เงื่อนไข while stack[-1] > 0 กันไม่ให้เกิดการชน (ตัวบนเป็น −5) → ตอบ [−5, 5]

เคส "กวาดล้างทั้งบาง" — [1, 2, 3, -4]:

−4 เข้ามาตัวเดียวไล่ชน 3, 2, 1 ระเบิดทิ้งเรียบ → ครองกระป๋องคนเดียว ตอบ [−4]

6. Time & Space Complexity

  • Time O(n) — แม้ซ้อน while ใน for แต่ดาวแต่ละดวงถูก append แค่ 1 ครั้งและ pop แค่ 1 ครั้ง รวมเป็นเส้นตรง
  • Space O(n) — กรณีดาววิ่งแยกย้ายหรือวิ่งทางเดียวกันหมด ต้องยัดทุกดวงลง Stack
💡 สรุป pattern

"ของใหม่ต้องไปเคลียร์ของเก่าใน stack ก่อนถึงจะ push เข้าได้" — เก็บสิ่งที่รอถูกจัดการไว้ใน stack แล้วให้ของใหม่ไล่ pop จนถึงจุดสมดุล