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

ข้อ 73 · LC452 Minimum Number of Arrows to Burst Balloons (ลูกศรเจาะลูกโป่ง) 🟡

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

หาจำนวนลูกศรน้อยสุดที่เจาะลูกโป่งทุกลูก ด้วย greedy ยิงที่ end ของลูกที่จบเร็วสุด

โจทย์ (LC452): มีลูกโป่งทรงกลมติดอยู่บนกำแพงซึ่งแทนด้วยระนาบ XY แต่ละลูกแทนด้วย array 2 มิติชื่อ points โดย points[i] = [x_start, x_end] คือเส้นผ่านศูนย์กลางแนวนอนของลูกโป่งลูกที่ i ยิงลูกศรขึ้นตรง ๆ ในแนวแกน y จากตำแหน่ง x ใด ๆ ได้ไม่จำกัดจำนวน ลูกศรที่ยิงจากตำแหน่ง x จะเจาะทะลุลูกโป่งทุกลูกที่ x_start <= x <= x_end ไปเรื่อย ๆ ไม่มีที่สิ้นสุด ให้ return จำนวนลูกศรน้อยที่สุดที่ทำให้ลูกโป่งทุกลูกแตก

Example 1
Input:
points = [[10,16],[2,8],[1,6],[7,12]]
Output:
2
Explanation:
ยิงที่ x=6 แตกสองลูก [2,8] กับ [1,6] และยิงที่ x=11 แตกสองลูก [10,16] กับ [7,12]
Example 2
Input:
points = [[1,2],[3,4],[5,6],[7,8]]
Output:
4
Explanation:
ไม่มีลูกไหนซ้อนกันเลย ต้องยิงทีละลูก รวม 4 ดอก
Example 3
Input:
points = [[1,2],[2,3],[3,4],[4,5]]
Output:
2
Explanation:
ยิงที่ x=2 แตก [1,2] กับ [2,3] และยิงที่ x=4 แตก [3,4] กับ [4,5]
Constraints (ข้อจำกัด)
  • 1 <= points.length <= 10^5
  • points[i].length == 2
  • -2^31 <= x_start < x_end <= 2^31 - 1

แนวทาง — ต้องใช้อะไร & คิดยังไง

ข้อนี้ใช้ sort + greedy คล้ายข้อก่อน ต่างที่คราวนี้เรามองว่าลูกศรหนึ่งดอกเจาะได้ทุกลูกที่ครอบตำแหน่งเดียวกัน จึงอยากยิงให้แต่ละดอกครอบลูกโป่งได้มากที่สุด

ถ้าลองยิงมั่ว ๆ หรือยิงกลางลูกแต่ละลูกจะได้ลูกศรเยอะเกิน แต่ถ้า sort ตาม end แล้วยิงที่ end ของลูกโป่งที่จบเร็วสุดในแต่ละกลุ่ม จะครอบลูกที่ overlap กับมันได้มากที่สุด เพราะ end เป็นตำแหน่งขวาสุดที่ยังโดนลูกนั้นอยู่

  1. ถ้า points ว่างให้ return 0
  2. sort points ตาม end (x[1])
  3. ยิงลูกแรก arrows = 1 ที่ arrow_x = end ของลูกโป่งลูกแรก
  4. iterate ลูกที่เหลือ ถ้า start > arrow_x แปลว่าลูกนี้เริ่มหลังลูกศร ต้องยิงใหม่ (arrows += 1, arrow_x = end)
  5. ถ้า start <= arrow_x ลูกนี้แตกด้วยลูกศรเดิม ไม่ต้องทำอะไร สุดท้าย return arrows
จุดพลาดที่พบบ่อย

เงื่อนไขต้องเป็น start > arrow_x (ไม่ใช่ >=) เพราะถ้าปลายชนกันพอดี เช่น ลูกหนึ่งจบที่ 2 อีกลูกเริ่มที่ 2 ลูกศรที่ x=2 ยังทะลุได้ทั้งคู่ อีก edge case คือ empty list (ลิสต์ว่าง) ต้อง return 0

ไล่ทีละสเต็ป

จำลอง points = [[10,16],[2,8],[1,6],[7,12]] หลัง sort ตาม end ได้ [[1,6],[2,8],[7,12],[10,16]]

ลูกโป่งstartarrow_xstart > arrow_xการกระทำarrows
[1,6]-6-ยิงลูกแรกที่ 61
[2,8]26เท็จแตกด้วยลูกเดิม1
[7,12]76จริงยิงใหม่ที่ 122
[10,16]1012เท็จแตกด้วยลูกเดิม2
▶ เฉลยละเอียด (ลองเองก่อนนะ)
เฉลย (Python) — โค้ดนี้รันได้จริงpython
def find_min_arrows(points):
    if not points:
        return 0
    # sort ตามจุดจบ แล้วยิงลูกศรที่ปลายของลูกแรกในแต่ละกลุ่ม
    points.sort(key=lambda x: x[1])

    arrows = 1
    arrow_x = points[0][1]   # ยิงลูกแรกที่จุดจบของลูกโป่งลูกแรก
    for start, end in points[1:]:
        if start > arrow_x:
            # ลูกนี้เริ่มหลังลูกศรที่ยิงไป ต้องยิงลูกใหม่
            arrows += 1
            arrow_x = end
        # ถ้า start <= arrow_x ลูกนี้ก็แตกด้วยลูกศรเดิม
    return arrows

print(find_min_arrows([[10,16],[2,8],[1,6],[7,12]]))  # 2
print(find_min_arrows([[1,2],[3,4],[5,6],[7,8]]))     # 4
print(find_min_arrows([[1,2],[2,3],[3,4],[4,5]]))     # 2
Output
2
4
2

แนวคิดคล้ายข้อก่อน คือ sort ตามจุดจบแล้ว greedy การยิงลูกศรที่จุดจบของลูกโป่งที่จบเร็วที่สุด เป็นการยิงที่ครอบคลุมลูกโป่งที่ทับกับมันได้มากที่สุด เพราะจุดจบเป็นตำแหน่งขวาสุดที่ยังโดนลูกโป่งลูกนั้นอยู่ ลูกโป่งลูกถัดไปที่ยังมีจุดเริ่มไม่เกินตำแหน่งลูกศร ก็โดนลูกศรเดียวกันแตกไปด้วยฟรี ๆ

จุดที่ต้องระวังคือเงื่อนไข start > arrow_x ใช้ > (ไม่ใช่ >=) เพราะโจทย์นี้ถ้าปลายชนกันพอดี เช่น ลูกหนึ่งจบที่ 2 อีกลูกเริ่มที่ 2 ยังยิงลูกศรที่ x=2 ทะลุได้ทั้งคู่ จึงถือว่ายังแตกด้วยลูกศรเดิม อีก edge case คือลิสต์ว่างต้องคืน 0 (โค้ดเช็คไว้ต้นฟังก์ชัน)

Time O(n log n) มาจากการ sort ส่วนไล่ยิงเป็น O(n) · Space O(1) ใช้ตัวแปรนับลูกศรกับตำแหน่งลูกศรเท่านั้น

💡 สรุป pattern

โจทย์แนวหาจำนวนกลุ่มช่วงที่ทับกันน้อยที่สุด (เจาะ/คลุม) ให้ sort ตามจุดจบแล้วยึดจุดจบเป็นเส้นตัดสิน — พบว่าเริ่มเลยเส้นเมื่อไรก็เริ่มกลุ่มใหม่