On this page
ข้อ 56 · LC875 Koko Eating Bananas (โกโกะกินกล้วย) 🟡
ตัวอย่างคลาสสิกของ binary search on answer — guess (เดา) speed แล้ว check ว่ากินทันไหม หา speed น้อยสุดที่ยังทัน
โจทย์ (LC875): โกโกะมีกล้วย n กอง โดยกองที่ i มี piles[i] ลูก ยามจะกลับมาใน h ชั่วโมง โกโกะเลือกความเร็วในการกิน k (กล้วยต่อชั่วโมง) แต่ละชั่วโมงเลือกกินจากกองใดกองหนึ่งได้สูงสุด k ลูก ถ้ากองนั้นมีน้อยกว่า k ลูก โกโกะจะกินหมดกองแล้วไม่กินกองอื่นต่อในชั่วโมงนั้น ให้หาความเร็ว k ที่น้อยที่สุดที่ทำให้กินกล้วยหมดทุกกองได้ภายใน h ชั่วโมง
- Input:
- piles = [3, 6, 7, 11], h = 8
- Output:
- 4
- Explanation:
- ที่ความเร็ว 4 ใช้เวลา ceil(3/4)+ceil(6/4)+ceil(7/4)+ceil(11/4) = 1+2+2+3 = 8 ชั่วโมง พอดี h = 8 และเป็นความเร็วน้อยสุดที่ยังทัน
- Input:
- piles = [30, 11, 23, 4, 20], h = 5
- Output:
- 30
- Explanation:
- เหลือเวลาแค่ 5 ชั่วโมงสำหรับ 5 กอง เท่ากับกินได้กองละ 1 ชั่วโมงเท่านั้น จึงต้องเร็วพอจะกินกองใหญ่สุด (30 ลูก) ให้หมดภายในชั่วโมงเดียว
- Input:
- piles = [30, 11, 23, 4, 20], h = 6
- Output:
- 23
- Explanation:
- มีเวลาเพิ่มมาอีก 1 ชั่วโมง (6 ชั่วโมงสำหรับ 5 กอง) ทำให้กองใหญ่สุดใช้ 2 ชั่วโมงได้ ความเร็ว 23 จึงเพียงพอและน้อยกว่าความเร็ว 30 ในตัวอย่างก่อนหน้า
- 1 <= piles.length <= 10^4
- piles.length <= h <= 10^9
- 1 <= piles[i] <= 10^9
แนวทาง — ต้องใช้อะไร & คิดยังไง
โครงสร้างที่ใช้: binary search on answer (ค้นบนช่วงคำตอบ) เราไม่ได้ search (ค้นหา) ใน array แต่ค้นบน answer space (ช่วงคำตอบ) คือ speed k ที่เป็นไปได้ speed k ยิ่งมากยิ่งกินทันแน่ ๆ (monotonic เท็จ-จริง แบบขั้นบันได) จึง binary search หา speed น้อยสุดที่ทัน
คิดแบบง่าย/ช้าก่อน: วิธี naive คือ iterate (วน) ลอง k = 1, 2, 3, ... จนกว่าจะทัน เป็น O(max(piles) * n) เพราะความสัมพันธ์เป็นขั้นบันได (ถ้า k ทันแล้ว k ที่มากกว่าก็ทัน) เราจึง binary search หา boundary (จุดพลิก) จากไม่ทันเป็นทัน เหลือ O(n log(max))
- เขียน function (ฟังก์ชัน) hours_needed(k) = ผลรวมของ ceil(pile / k) ทุกกอง (ปัดขึ้นเพราะกินข้ามกองไม่ได้)
- ตั้ง answer space lo = 1, hi = max(piles) (เร็วสุดที่จำเป็น)
- ระหว่าง lo < hi: compute mid ถ้า hours_needed(mid) <= h แสดงว่า feasible (ทัน) ลองช้าลงอีก ตั้ง hi = mid
- ไม่งั้น mid ช้าไป กินไม่ทัน ตั้ง lo = mid + 1; จบ loop return lo คือ speed น้อยสุดที่ทัน
ลืมปัดขึ้น (ใช้หารปกติจะได้เวลาน้อยกว่าจริงเพราะเศษกล้วยก็ยังต้องใช้อีกหนึ่งชั่วโมง) หรือใช้ while lo <= hi กับ hi = mid ทำให้วนไม่จบ ต้องใช้ while lo < hi คู่กับ hi = mid
ไล่ทีละสเต็ป
จำลอง piles = [3,6,7,11], h = 8 ช่วง lo = 1, hi = 11:
| lo | hi | mid | hours_needed(mid) | <= 8? | ทำอะไรต่อ |
|---|---|---|---|---|---|
| 1 | 11 | 6 | 1+1+2+2 = 6 | ทัน | hi = 6 |
| 1 | 6 | 3 | 1+2+3+4 = 10 | ไม่ทัน | lo = 4 |
| 4 | 6 | 5 | 1+2+2+3 = 8 | ทัน | hi = 5 |
| 4 | 5 | 4 | 1+2+2+3 = 8 | ทัน | hi = 4 |
| 4 | 4 | - | - | - | return 4 |
▶ เฉลยละเอียด (ลองเองก่อนนะ)
import math
def min_eating_speed(piles, h):
def hours_needed(k):
# ที่ความเร็ว k แต่ละกองใช้เวลา ceil(pile / k) ชั่วโมง
return sum(math.ceil(pile / k) for pile in piles)
lo, hi = 1, max(piles) # ช่วงความเร็วที่เป็นไปได้
while lo < hi:
mid = (lo + hi) // 2
if hours_needed(mid) <= h:
hi = mid # เร็ว mid ก็ทัน ลองช้าลงอีก (เก็บ mid ไว้)
else:
lo = mid + 1 # mid ช้าไป กินไม่ทัน ต้องเร็วขึ้น
return lo # ความเร็วน้อยสุดที่ยังทัน
print(min_eating_speed([3, 6, 7, 11], 8)) # 4
print(min_eating_speed([30, 11, 23, 4, 20], 5)) # 304
30แทนที่จะ search ใน array เราค้นบน answer space คือ speed k ที่เป็นไปได้ตั้งแต่ 1 (ช้าสุดที่มีความหมาย) ถึง max(piles) (เร็วสุดที่จำเป็น เพราะเร็วกว่านี้ก็กินกองใหญ่สุดได้ในชั่วโมงเดียวอยู่แล้ว) กุญแจคือความสัมพันธ์แบบ monotonic (ขั้นบันได): ยิ่ง k มากยิ่งใช้เวลาน้อย ดังนั้นถ้า k ตัวหนึ่ง feasible (กินทัน เวลา <= h) แล้ว k ที่มากกว่าก็ทันด้วยเสมอ เราจึง binary search หา boundary จาก ไม่ทัน เป็น ทัน ตัวแรก
function hours_needed compute (คำนวณ) เวลาที่ต้องใช้ที่ speed k โดยแต่ละกองใช้ ceil(pile / k) ชั่วโมง (ต้องปัดขึ้นเพราะเศษกล้วยก็ยังต้องใช้อีกหนึ่งชั่วโมง และกินข้ามกองไม่ได้) เมื่อ mid ทำเวลาได้ <= h เราเก็บ mid ไว้แล้วลองหาที่ช้ากว่า (hi = mid) ถ้าไม่ทันก็ต้องเร็วขึ้น (lo = mid + 1) ถ้าลืมปัดขึ้นจะได้เวลาน้อยกว่าจริงและตอบ speed ผิดต่ำเกินไป
Time O(n log(max(piles))) binary search ราว log(max) รอบ แต่ละรอบเรียก hours_needed ที่ iterate ทุกกอง O(n) · Space O(1) ไม่มีโครงสร้างเสริม
ถ้าโจทย์ถามหา minimum/maximum (ค่าน้อยสุด/มากสุด) ที่ยังทำ condition (เงื่อนไข) ได้ และเงื่อนไขมีลักษณะ monotonic (ขั้นบันได พอผ่านแล้วผ่านตลอด) ให้ binary search บน answer space โดยเขียน function feasible() check แต่ละค่าที่ guess นี่คือหัวใจของ binary search on answer