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

ข้อ 52 · LC2462 Total Cost to Hire K Workers (ต้นทุนจ้าง k คน) 🟡

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

แต่ละรอบ hire (จ้าง) คนถูกสุดจากหัวหรือท้ายแถว ใช้ min-heap สองอันคุมสองฝั่ง เติมคนจากตรงกลางเข้ามาแทน

โจทย์ (LC2462): กำหนด array จำนวนเต็ม costs โดย costs[i] คือค่าจ้างของคนงานคนที่ i พร้อมจำนวนเต็ม k และ candidates ให้ทำการ hire (จ้างงาน) ทั้งหมด k รอบ รอบละหนึ่งคน แต่ละรอบให้เลือกคนที่ค่าจ้างถูกที่สุดจาก candidates คนแรกสุดของแถวที่เหลือ หรือ candidates คนท้ายสุดของแถวที่เหลือ (ถ้าเท่ากันให้เลือก index น้อยกว่า) ถ้าคนที่เหลือมีน้อยกว่า candidates คน ให้เลือกจากคนที่เหลือทั้งหมด คนแต่ละคนถูกจ้างได้ครั้งเดียว ให้ return ผลรวมต้นทุนการจ้างทั้งหมด

Example 1
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
Example 2
Input:
costs = [1, 2, 4, 1], k = 3, candidates = 3
Output:
4
Explanation:
candidates = 3 เกือบเท่าจำนวนคนทั้งหมด (n = 4) ทำให้เห็นคนเกือบทั้งหมดตั้งแต่ต้น จ้าง 3 คนที่ถูกที่สุดคือ 1, 1, 2 รวมเป็น 4
Constraints (ข้อจำกัด)
  • 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)

  1. สร้าง head จาก candidates คนแรก และ tail จาก candidates คนท้าย โดยกันช่วงทับกันด้วยจุดเริ่ม max(candidates, n - candidates) แล้ว heapify ทั้งสอง
  2. initialize pointer (ตัวชี้) left และ right ชี้คนตรงกลางที่ยังไม่ถูกดึงเข้า heap
  3. iterate k รอบ: compare head[0] กับ tail[0] เลือกฝั่งที่ถูกกว่า (ถ้าเท่ากันเลือกหัว เพราะ index น้อยกว่า) pop ออกแล้วบวกเข้า total
  4. หลัง 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 (จ่าย)totalpush คนใหม่
122หัว จ่าย 22costs[4]=7 เข้าหัว, left=5
272ท้าย จ่าย 24left(5) > right(4) ไม่เติม
378หัว จ่าย 711ไม่เติม

total = 11

▶ เฉลยละเอียด (ลองเองก่อนนะ)
เฉลย (Python) — โค้ดนี้รันได้จริงpython
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))  # 4
Output
11
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 สองอันรวมกัน

💡 สรุป pattern

เมื่อต้อง select ตัวสุดขั้วจากหลายกลุ่มพร้อมกัน (หัว/ท้าย) ให้แต่ละกลุ่มมี heap ของตัวเอง แล้ว compare ยอดของแต่ละ heap ในแต่ละรอบ เป็น pattern ที่ต่อยอดไปโจทย์ merge k lists ได้