On this page
ข้อ 73 · LC452 Minimum Number of Arrows to Burst Balloons (ลูกศรเจาะลูกโป่ง) 🟡
หาจำนวนลูกศรน้อยสุดที่เจาะลูกโป่งทุกลูก ด้วย greedy ยิงที่ end ของลูกที่จบเร็วสุด
โจทย์ (LC452): มีลูกโป่งทรงกลมติดอยู่บนกำแพงซึ่งแทนด้วยระนาบ XY แต่ละลูกแทนด้วย array 2 มิติชื่อ points โดย points[i] = [x_start, x_end] คือเส้นผ่านศูนย์กลางแนวนอนของลูกโป่งลูกที่ i ยิงลูกศรขึ้นตรง ๆ ในแนวแกน y จากตำแหน่ง x ใด ๆ ได้ไม่จำกัดจำนวน ลูกศรที่ยิงจากตำแหน่ง x จะเจาะทะลุลูกโป่งทุกลูกที่ x_start <= x <= x_end ไปเรื่อย ๆ ไม่มีที่สิ้นสุด ให้ return จำนวนลูกศรน้อยที่สุดที่ทำให้ลูกโป่งทุกลูกแตก
- 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]
- Input:
- points = [[1,2],[3,4],[5,6],[7,8]]
- Output:
- 4
- Explanation:
- ไม่มีลูกไหนซ้อนกันเลย ต้องยิงทีละลูก รวม 4 ดอก
- 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]
- 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 เป็นตำแหน่งขวาสุดที่ยังโดนลูกนั้นอยู่
- ถ้า points ว่างให้ return 0
- sort points ตาม end (x[1])
- ยิงลูกแรก arrows = 1 ที่ arrow_x = end ของลูกโป่งลูกแรก
- iterate ลูกที่เหลือ ถ้า start > arrow_x แปลว่าลูกนี้เริ่มหลังลูกศร ต้องยิงใหม่ (arrows += 1, arrow_x = end)
- ถ้า 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]]
| ลูกโป่ง | start | arrow_x | start > arrow_x | การกระทำ | arrows |
|---|---|---|---|---|---|
| [1,6] | - | 6 | - | ยิงลูกแรกที่ 6 | 1 |
| [2,8] | 2 | 6 | เท็จ | แตกด้วยลูกเดิม | 1 |
| [7,12] | 7 | 6 | จริง | ยิงใหม่ที่ 12 | 2 |
| [10,16] | 10 | 12 | เท็จ | แตกด้วยลูกเดิม | 2 |
▶ เฉลยละเอียด (ลองเองก่อนนะ)
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]])) # 22
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) ใช้ตัวแปรนับลูกศรกับตำแหน่งลูกศรเท่านั้น
โจทย์แนวหาจำนวนกลุ่มช่วงที่ทับกันน้อยที่สุด (เจาะ/คลุม) ให้ sort ตามจุดจบแล้วยึดจุดจบเป็นเส้นตัดสิน — พบว่าเริ่มเลยเส้นเมื่อไรก็เริ่มกลุ่มใหม่