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

ข้อ 56 · LC875 Koko Eating Bananas (โกโกะกินกล้วย) 🟡

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

ตัวอย่างคลาสสิกของ binary search on answer — guess (เดา) speed แล้ว check ว่ากินทันไหม หา speed น้อยสุดที่ยังทัน

โจทย์ (LC875): โกโกะมีกล้วย n กอง โดยกองที่ i มี piles[i] ลูก ยามจะกลับมาใน h ชั่วโมง โกโกะเลือกความเร็วในการกิน k (กล้วยต่อชั่วโมง) แต่ละชั่วโมงเลือกกินจากกองใดกองหนึ่งได้สูงสุด k ลูก ถ้ากองนั้นมีน้อยกว่า k ลูก โกโกะจะกินหมดกองแล้วไม่กินกองอื่นต่อในชั่วโมงนั้น ให้หาความเร็ว k ที่น้อยที่สุดที่ทำให้กินกล้วยหมดทุกกองได้ภายใน h ชั่วโมง

Example 1
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 และเป็นความเร็วน้อยสุดที่ยังทัน
Example 2
Input:
piles = [30, 11, 23, 4, 20], h = 5
Output:
30
Explanation:
เหลือเวลาแค่ 5 ชั่วโมงสำหรับ 5 กอง เท่ากับกินได้กองละ 1 ชั่วโมงเท่านั้น จึงต้องเร็วพอจะกินกองใหญ่สุด (30 ลูก) ให้หมดภายในชั่วโมงเดียว
Example 3
Input:
piles = [30, 11, 23, 4, 20], h = 6
Output:
23
Explanation:
มีเวลาเพิ่มมาอีก 1 ชั่วโมง (6 ชั่วโมงสำหรับ 5 กอง) ทำให้กองใหญ่สุดใช้ 2 ชั่วโมงได้ ความเร็ว 23 จึงเพียงพอและน้อยกว่าความเร็ว 30 ในตัวอย่างก่อนหน้า
Constraints (ข้อจำกัด)
  • 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))

  1. เขียน function (ฟังก์ชัน) hours_needed(k) = ผลรวมของ ceil(pile / k) ทุกกอง (ปัดขึ้นเพราะกินข้ามกองไม่ได้)
  2. ตั้ง answer space lo = 1, hi = max(piles) (เร็วสุดที่จำเป็น)
  3. ระหว่าง lo < hi: compute mid ถ้า hours_needed(mid) <= h แสดงว่า feasible (ทัน) ลองช้าลงอีก ตั้ง hi = mid
  4. ไม่งั้น 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:

lohimidhours_needed(mid)<= 8?ทำอะไรต่อ
11161+1+2+2 = 6ทันhi = 6
1631+2+3+4 = 10ไม่ทันlo = 4
4651+2+2+3 = 8ทันhi = 5
4541+2+2+3 = 8ทันhi = 4
44---return 4
▶ เฉลยละเอียด (ลองเองก่อนนะ)
เฉลย (Python) — โค้ดนี้รันได้จริงpython
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))  # 30
Output
4
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) ไม่มีโครงสร้างเสริม

💡 สรุป pattern

ถ้าโจทย์ถามหา minimum/maximum (ค่าน้อยสุด/มากสุด) ที่ยังทำ condition (เงื่อนไข) ได้ และเงื่อนไขมีลักษณะ monotonic (ขั้นบันได พอผ่านแล้วผ่านตลอด) ให้ binary search บน answer space โดยเขียน function feasible() check แต่ละค่าที่ guess นี่คือหัวใจของ binary search on answer