On this page
ข้อ 52 · LC2462 Total Cost to Hire K Workers (ต้นทุนจ้าง k คน) 🟡
แต่ละรอบ hire (จ้าง) คนถูกสุดจากหัวหรือท้ายแถว ใช้ min-heap สองอันคุมสองฝั่ง เติมคนจากตรงกลางเข้ามาแทน
โจทย์ (LC2462): กำหนด array จำนวนเต็ม costs โดย costs[i] คือค่าจ้างของคนงานคนที่ i พร้อมจำนวนเต็ม k และ candidates ให้ทำการ hire (จ้างงาน) ทั้งหมด k รอบ รอบละหนึ่งคน แต่ละรอบให้เลือกคนที่ค่าจ้างถูกที่สุดจาก candidates คนแรกสุดของแถวที่เหลือ หรือ candidates คนท้ายสุดของแถวที่เหลือ (ถ้าเท่ากันให้เลือก index น้อยกว่า) ถ้าคนที่เหลือมีน้อยกว่า candidates คน ให้เลือกจากคนที่เหลือทั้งหมด คนแต่ละคนถูกจ้างได้ครั้งเดียว ให้ return ผลรวมต้นทุนการจ้างทั้งหมด
- Input:
- costs = [17, 12, 10, 2, 7, 2, 11, 20, 8], k = 3, candidates = 4
- Output:
- 11
- Explanation:
- รอบแรกเลือกจาก candidates 4 คนแรก [17,12,10,2] หรือ 4 คนท้าย [7,2,11,20,8] คนถูกสุดคือ 2 ที่ index 3 (ตัดสินด้วย index น้อยกว่าเมื่อเสมอกับ index 5) จ่าย 2 วนไปจนครบ 3 รอบได้ต้นทุนรวม 11
- Input:
- costs = [1, 2, 4, 1], k = 3, candidates = 3
- Output:
- 4
- Explanation:
- candidates = 3 เกือบเท่าจำนวนคนทั้งหมด (n = 4) ทำให้เห็นคนเกือบทั้งหมดตั้งแต่ต้น จ้าง 3 คนที่ถูกที่สุดคือ 1, 1, 2 รวมเป็น 4
- 1 <= costs.length <= 10^5
- 1 <= costs[i] <= 10^5
- 1 <= k, candidates <= costs.length
แนวทาง — ต้องใช้อะไร & คิดยังไง
โครงสร้างที่ใช้: min-heap สองอัน อันหนึ่งคุมฝั่งหัวแถว อีกอันคุมฝั่งท้ายแถว การหาตัวถูกสุด (minimum) ของแต่ละฝั่งซ้ำ ๆ คือหน้าที่ของ min-heap พอดี
คิดแบบง่าย/ช้าก่อน: ถ้าแต่ละรอบ scan (ไล่สแกน) หาค่าถูกสุดใน window (หน้าต่าง) หัว-ท้ายเองจะเป็น O(k * candidates) และการ remove/เลื่อนคนใน array ก็แพง เราจึงใช้ heap สองอันให้ pop ตัวถูกสุดของแต่ละฝั่งได้ที่ O(log candidates)
- สร้าง head จาก candidates คนแรก และ tail จาก candidates คนท้าย โดยกันช่วงทับกันด้วยจุดเริ่ม max(candidates, n - candidates) แล้ว heapify ทั้งสอง
- initialize pointer (ตัวชี้) left และ right ชี้คนตรงกลางที่ยังไม่ถูกดึงเข้า heap
- iterate k รอบ: compare head[0] กับ tail[0] เลือกฝั่งที่ถูกกว่า (ถ้าเท่ากันเลือกหัว เพราะ index น้อยกว่า) pop ออกแล้วบวกเข้า total
- หลัง hire ฝั่งไหน ถ้า left <= right ยังไม่ชนกัน ให้ push คนใหม่จากตรงกลาง (costs[left] หรือ costs[right]) เข้า heap ฝั่งนั้น แล้วขยับ pointer
ให้ช่วงหัวกับท้ายทับกันเมื่อ 2*candidates มากกว่า n ต้องใช้ max(candidates, n - candidates) เป็นจุดเริ่มของ tail และต้อง push คนใหม่ก็ต่อเมื่อ left <= right เท่านั้น ถ้าเลยจุดนี้แปลว่าคนตรงกลางถูกดึงเข้า heap ครบแล้ว ไม่มีใครให้เติมอีก
ไล่ทีละสเต็ป
จำลอง costs = [17,12,10,2,7,2,11,20,8], k = 3, candidates = 4 (n = 9) เริ่ม head = [17,12,10,2], tail = [7,2,11,20,8] ที่ index 4..8 (จุดเริ่ม max(4,5)=5 → จริง ๆ tail = [2,11,20,8]), left = 4, right = 4:
| รอบ | head[0] | tail[0] | select (จ่าย) | total | push คนใหม่ |
|---|---|---|---|---|---|
| 1 | 2 | 2 | หัว จ่าย 2 | 2 | costs[4]=7 เข้าหัว, left=5 |
| 2 | 7 | 2 | ท้าย จ่าย 2 | 4 | left(5) > right(4) ไม่เติม |
| 3 | 7 | 8 | หัว จ่าย 7 | 11 | ไม่เติม |
total = 11
▶ เฉลยละเอียด (ลองเองก่อนนะ)
import heapq
def total_cost(costs, k, candidates):
n = len(costs)
left = candidates # pointer ถัดไปฝั่งหัว
right = n - 1 - candidates # pointer ถัดไปฝั่งท้าย
head = costs[:candidates] # candidates คนแรก
tail = costs[max(candidates, n - candidates):] # candidates คนท้าย (ไม่ทับกับหัว)
heapq.heapify(head)
heapq.heapify(tail)
total = 0
for _ in range(k):
# เลือกฝั่งที่ถูกกว่า ถ้าเท่ากันเลือกฝั่งหัว (index น้อยกว่า)
if not tail or (head and head[0] <= tail[0]):
total += heapq.heappop(head)
# เติมคนใหม่จากตรงกลางเข้าฝั่งหัว ถ้ายังไม่ชนกัน
if left <= right:
heapq.heappush(head, costs[left])
left += 1
else:
total += heapq.heappop(tail)
if left <= right:
heapq.heappush(tail, costs[right])
right -= 1
return total
print(total_cost([17, 12, 10, 2, 7, 2, 11, 20, 8], 3, 4)) # 11
print(total_cost([1, 2, 4, 1], 3, 3)) # 411
4ไอเดียคือ ในแต่ละรอบเราต้อง select คนถูกสุดจากสองฝั่งของแถว (หัว candidates คน และท้าย candidates คน) การหาตัว minimum ของแต่ละฝั่งซ้ำ ๆ คือหน้าที่ของ min-heap พอดี เราจึงสร้าง heap สองอันคุมสองฝั่ง แต่ละรอบ compare head[0] กับ tail[0] เลือกตัวที่น้อยกว่า (เท่ากันเลือกหัวเพื่อให้ index น้อยกว่าตามกติกา) แล้วบวกเข้า total cost
จุดที่ต้องระวังที่สุดคือการ push คนใหม่จากตรงกลาง และการไม่ให้สองฝั่งนับคนซ้ำ ตอนสร้าง tail เราใช้ max(candidates, n - candidates) เป็นจุดเริ่ม เพื่อกันไม่ให้ช่วงหัวกับท้าย overlap (ทับกัน) เมื่อ 2*candidates มากกว่า n ส่วน pointer left และ right จะเดินเข้าหากันตรงกลาง เรา push คนใหม่ก็ต่อเมื่อ left <= right เท่านั้น
Time O((candidates + k) log candidates) สร้าง heap สองอันเป็น O(candidates) และ iterate k รอบ แต่ละรอบ push/pop เป็น O(log candidates) · Space O(candidates) สำหรับ heap สองอันรวมกัน
เมื่อต้อง select ตัวสุดขั้วจากหลายกลุ่มพร้อมกัน (หัว/ท้าย) ให้แต่ละกลุ่มมี heap ของตัวเอง แล้ว compare ยอดของแต่ละ heap ในแต่ละรอบ เป็น pattern ที่ต่อยอดไปโจทย์ merge k lists ได้