0Pricing
DSA Interview Prep · บทเรียน

ตัวจัดตารางงานและสถานีเติมน้ำมัน

ประยุกต์การให้เหตุผลแบบละโมบกับปัญหาระยะพักของตัวจัดตารางงาน CPU และปัญหาการตรวจสอบความเป็นไปได้ของสถานีเติมน้ำมันแบบวงกลม

ตัวจัดตารางงานและสถานีเติมน้ำมัน เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

ปัญหาตัวจัดตารางงาน

ตัวจัดตารางงาน (LeetCode 621): กำหนดรายการงานของ CPU (แต่ละงานมีป้ายกำกับ A-Z) และช่วงพัก n ให้ค้นหาจำนวนช่วงเวลาของ CPU ที่น้อยที่สุดเพื่อทำงานทั้งหมดให้เสร็จ งานเดียวกันต้องรออย่างน้อย n ช่วงเวลาก่อนทำงานอีกครั้ง อนุญาตให้มีช่วงเวลาว่างได้ สำหรับงาน ['A','A','A','B','B','B'] ที่มีช่วงพัก 2 คำตอบคือ 8: A→B→idle→A→B→idle→A→B

# Task Scheduler example
tasks = ['A','A','A','B','B','B']
n = 2  # cooldown
# One optimal schedule: A B _ A B _ A B
# Intervals: 1 2 3 4 5 6 7 8 → answer = 8

# Another example: tasks=['A','A','A','B','B','C'] n=2
# A B C A B _ A → 7 intervals
print('Understanding the cooldown constraint')
print('Same task needs n intervals gap between runs')

สูตรแบบละโมบสำหรับตัวจัดตารางงาน

แนวคิดสำคัญคือ เวลารวมถูกกำหนดโดย งานที่ปรากฏบ่อยที่สุด หากงานที่ปรากฏบ่อยที่สุดปรากฏ f ครั้ง โดยมีงานจำนวน max_count งานที่มีความถี่ f เวลาในการทำงานคือ max(len(tasks), (f-1) * (n+1) + max_count) สูตรนี้ทำงานโดยสร้างกรอบจำนวน f-1 กรอบ แต่ละกรอบมีขนาด n+1 เติมงานอื่นลงไป แล้ว add รอบสุดท้าย หากงานอื่นเติมช่องว่างทั้งหมดได้ (มีงานหลากหลายจำนวนมาก) ก็ให้ทำงานทั้งหมดโดยไม่มีช่วงเวลาว่าง

from collections import Counter

def least_interval(tasks, n):
    count = Counter(tasks)
    max_freq = max(count.values())
    # How many tasks have the maximum frequency?
    max_count = sum(1 for c in count.values() if c == max_freq)
    # Formula: max of total tasks (no idle) or frame-based calculation
    frame_time = (max_freq - 1) * (n + 1) + max_count
    return max(len(tasks), frame_time)

print(least_interval(['A','A','A','B','B','B'], 2))  # 8
print(least_interval(['A','A','A','B','B','B'], 0))  # 6 (no cooldown)
print(least_interval(['A','A','A','A','B','C'], 3))  # 10

เหตุใดสูตรนี้จึงใช้ได้

ลองนึกภาพตารางงานเป็นตารางที่มี n+1 คอลัมน์ (หนึ่งช่องสำหรับงาน + n ช่องสำหรับช่วงพัก) งาน A ที่ปรากฏบ่อยที่สุด (ความถี่ f) ต้องใช้ f แถว ระหว่างการปรากฏครั้งแรกและครั้งสุดท้าย จะมีกรอบเต็มจำนวน f-1 กรอบ แต่ละกรอบมี n+1 ช่อง และยังมีกรอบสุดท้ายบางส่วนที่ประกอบด้วยงานทั้งหมดซึ่งมีความถี่สูงสุด หากมีงานที่หลากหลายเพียงพอ งานเหล่านั้นจะเติมช่องว่างทั้งหมด และจำนวนงานจริงจะมากกว่าเวลาตามกรอบ — ให้เลือกค่าที่มากกว่าจากสองค่านี้

# Visualise frame structure for AAABBB, n=2
# Frame size = n+1 = 3
# f = 3 (A appears 3 times), max_count = 2 (A and B both appear 3 times)
# Grid:
# [A B _]  ← frame 1
# [A B _]  ← frame 2  
# [A B  ]  ← last partial frame (max_count=2 cells)
# Total = (3-1)*3 + 2 = 6 + 2 = 8

# If tasks = AAAABBCC, n=2: max_freq=4 (A), max_count=1
# (4-1)*(2+1)+1 = 9+1 = 10
# But len(tasks)=8 < 10, so answer is 10
tasks2 = ['A','A','A','A','B','B','C','C']
from collections import Counter
count = Counter(tasks2)
mf = max(count.values())
mc = sum(1 for c in count.values() if c == mf)
print(f'Frame formula: ({mf}-1)*{2+1}+{mc} = {(mf-1)*(2+1)+mc}')
print(f'Max(len={len(tasks2)}, frame={max(len(tasks2),(mf-1)*3+mc)}) = {max(len(tasks2),(mf-1)*3+mc)}')

ทางเลือกแบบจำลองด้วยฮีป

การจำลองโดยใช้ฮีปจะให้ตารางงานจริง (ไม่ใช่แค่จำนวน) ในแต่ละขั้น ให้เลือกงานที่พร้อมและมีความถี่สูงที่สุด (ฮีปสูงสุด) หลังจากทำงานแล้ว ให้ใช้ช่วงพัก: อย่นำงานกลับเข้าไปจนกว่าจะผ่านไป n ขั้น ใช้คิวติดตามงานที่อยู่ในช่วงพัก วิธีนี้ใช้เวลา O(เวลารวม × log k) โดย k คือจำนวนงานที่แตกต่างกัน แม้จะถูกต้อง แต่สูตรเร็วกกว่า ควรรู้จักทั้งสองวิธี — ผู้สัมภาษณ์อาจขอให้สร้างตารางงานจริง

import heapq
from collections import deque, Counter

def task_scheduler_simulate(tasks, n):
    count = Counter(tasks)
    heap = [-c for c in count.values()]  # max-heap using negation
    heapq.heapify(heap)
    time = 0
    cooldown = deque()  # (available_at, neg_count)
    while heap or cooldown:
        time += 1
        if heap:
            c = heapq.heappop(heap) + 1  # use one instance
            if c < 0:  # still has remaining tasks
                cooldown.append((time + n, c))
        if cooldown and cooldown[0][0] == time:
            heapq.heappush(heap, cooldown.popleft()[1])
    return time

print(task_scheduler_simulate(['A','A','A','B','B','B'], 2))  # 8

ปัญหาสถานีบริการน้ำมัน

สถานีบริการน้ำมัน (LeetCode 134): มีสถานีบริการน้ำมันจำนวน n สถานีเรียงเป็นวงกลม สถานีที่ i มีน้ำมัน gas[i] และต้องใช้ cost[i] เพื่อเดินทางไปยังสถานีถัดไป เมื่อเริ่มต้นด้วยถังว่าง ให้ค้นหาสถานีเริ่มต้นที่ทำให้เดินทางครบรอบได้ หากไม่มีสถานีดังกล่าว ให้คืนค่า -1 โจทย์รับประกันว่าหากมีคำตอบ จะมีคำตอบที่ใช้ได้ไม่เกินหนึ่งคำตอบ

# Example:
gas  = [1, 2, 3, 4, 5]
cost = [3, 4, 5, 1, 2]
# net gain per station: gas[i] - cost[i]
net = [g - c for g, c in zip(gas, cost)]
print('Net gain per station:', net)  # [-2, -2, -2, 3, 3]
# Only possible start: station 3 (index 3)
# Tank: 0 +3=3 → 3-1=2 → 2+1=3-2=... let's verify
print('Sum of net:', sum(net))  # 1 > 0 means solution exists

วิธีแบบละโมบสำหรับสถานีบริการน้ำมัน

อัลกอริทึมแบบละโมบ: (1) หากปริมาณน้ำมันรวม < ต้นทุนรวม จะไม่มีคำตอบ (คืนค่า -1) (2) มิฉะนั้นจะมีคำตอบเพียงหนึ่งเดียว ให้ค้นหาด้วยการไล่ผ่านเพียงครั้งเดียว โดยติดตาม tank (เชื้อเพลิงปัจจุบัน) และ start (สถานีเริ่มต้นที่เป็นตัวเลือก) หาก tank < 0 หลังจากผ่านสถานีหนึ่งแล้ว แสดงว่า start ปัจจุบันไม่สามารถไปถึงสถานีนั้นได้ — ตั้งค่า tank = 0 ใหม่ และกำหนด start = i + 1 ค่า start สุดท้ายคือคำตอบ

def can_complete_circuit(gas, cost):
    if sum(gas) < sum(cost):
        return -1  # impossible
    tank = 0
    start = 0
    for i in range(len(gas)):
        tank += gas[i] - cost[i]
        if tank < 0:
            tank = 0
            start = i + 1  # current start failed, try next
    return start

gas  = [1, 2, 3, 4, 5]
cost = [3, 4, 5, 1, 2]
print(can_complete_circuit(gas, cost))  # 3

gas2  = [2, 3, 4]
cost2 = [3, 4, 3]
print(can_complete_circuit(gas2, cost2))  # -1

เหตุใดสถานีเริ่มต้นจากวิธีแบบละโมบจึงถูกต้อง

เหตุผลด้านความถูกต้อง: หากถังมีน้ำมันติดลบหลังจากไปถึงสถานี i โดยเริ่มจาก start สถานีใด ๆ ระหว่าง start และ i (รวมทั้งสองสถานี) ก็ไม่สามารถเป็นจุดเริ่มต้นที่ใช้ได้ เพราะเมื่อไปถึงสถานี i สถานีเหล่านั้นจะมีน้ำมันน้อยกว่าการเริ่มจาก start ดังนั้นเราจึงข้ามสถานีทั้งหมดได้อย่างปลอดภัยและลองที่ i+1 เนื่องจากมีคำตอบอยู่ (ปริมาณน้ำมันรวม ≥ ต้นทุนรวม) ตัวเลือก start สุดท้ายจึงต้องใช้ได้

# Proof sketch: why start=i+1 is correct after tank<0 at station i
# If we start at station j (start <= j <= i), tank at j is tank_from_start(j)
# After stations start..j: tank_from_j starts at 0, but we've already used gas[start..j-1]
# Starting at j means: tank_at_i = sum(net[j..i]) = sum(net[start..i]) - sum(net[start..j-1])
# Since sum(net[start..i]) < 0 AND sum(net[start..j-1]) >= 0 (no reset before i),
# tank_at_i when starting at j is even more negative → j cannot work either

def verify_gas_solution(gas, cost, start):
    tank = 0
    n = len(gas)
    for i in range(n):
        idx = (start + i) % n
        tank += gas[idx] - cost[idx]
        if tank < 0: return False
    return True

print(verify_gas_solution([1,2,3,4,5],[3,4,5,1,2], 3))  # True

วิธีลองทุกกรณีเทียบกับวิธีแบบละโมบสำหรับสถานีบริการน้ำมัน

วิธีลองทุกกรณีจะลองสถานีเริ่มต้นแต่ละแห่งและจำลองการเดินทางครบรอบทั้งหมด ใช้เวลา O(n²) วิธีแบบละโมบที่ไล่ผ่านเพียงครั้งเดียวใช้เวลา O(n) และพื้นที่ O(1) สำหรับอาร์เรย์ที่มีสถานี 10⁵ แห่ง ความแตกต่างคือการดำเนินการ 10¹⁰ ครั้งเทียบกับ 10⁵ ครั้ง คุณสมบัติทางคณิตศาสตร์สำคัญที่ทำให้วิธีแบบละโมบใช้ได้คือ หากเชื้อเพลิงสุทธิรวมไม่ติดลบ จะมีจุดเริ่มต้นที่ใช้ได้ และจุดนั้นจะเป็นสถานีที่อยู่ถัดจากตำแหน่งสุดท้ายที่ผลรวมสะสมติดลบเสมอ

def brute_force_gas(gas, cost):
    n = len(gas)
    for start in range(n):
        tank = 0
        valid = True
        for i in range(n):
            idx = (start + i) % n
            tank += gas[idx] - cost[idx]
            if tank < 0: valid = False; break
        if valid: return start
    return -1

def greedy_gas(gas, cost):
    if sum(gas) < sum(cost): return -1
    tank = start = 0
    for i, (g, c) in enumerate(zip(gas, cost)):
        tank += g - c
        if tank < 0: tank = 0; start = i + 1
    return start

gas = [1,2,3,4,5]; cost = [3,4,5,1,2]
print('Brute:', brute_force_gas(gas,cost), '== Greedy:', greedy_gas(gas,cost))

ที่เกี่ยวข้อง: ต้นทุนขั้นต่ำเพื่อให้เที่ยวเสร็จสิ้น

เวลาขั้นต่ำเพื่อให้เที่ยวเสร็จสิ้น (LeetCode 2187) เป็นปัญหาการค้นหาแบบทวิภาคบนพื้นที่คำตอบ คุณใช้การค้นหาแบบทวิภาคกับค่าของเวลา T: เมื่อกำหนดเวลา T รถโดยสารที่มี time[i] จะให้บริการได้ floor(T/time[i]) เที่ยว หากจำนวนเที่ยวทั้งหมด ≥ totalTrips แสดงว่า T เพียงพอ จงหา T ที่น้อยที่สุด การศึกษานี้แสดงให้เห็นว่าวิธีละโมบสามารถใช้ในระดับเมตาได้ กล่าวคือค้นหาแบบทวิภาคบนคำตอบ แม้จะไม่มีกฎแบบละโมบโดยตรงในระดับวัตถุ

def minimum_time(time, total_trips):
    def can_complete(t):
        return sum(t // bus for bus in time) >= total_trips
    
    lo, hi = 1, min(time) * total_trips  # upper bound
    while lo < hi:
        mid = (lo + hi) // 2
        if can_complete(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

print(minimum_time([1, 2, 3], 5))   # 3 (3/1=3 + 3/2=1 + 3/3=1 = 5)
print(minimum_time([2], 1))          # 2

กรณีขอบและการตรวจสอบ

กรณีขอบที่สำคัญของปัญหาทั้งสองมีดังนี้: ตัวจัดตารางงาน — เมื่อช่วงพัก n=0 คำตอบก็คือจำนวนงานทั้งหมด (ไม่จำเป็นต้องมีช่วงว่าง) เมื่อมีงานชนิดเดียวกันทั้งหมด เช่น เป็น 'A' ทั้งหมด ช่องว่างจะถูกเติมจนพอดี เมื่อมีงานหลายชนิด ช่องว่างอาจเป็น 0 ได้ เพราะงานเติมเต็มกรอบเวลาทั้งหมด สถานีเติมน้ำมัน — เมื่อปริมาณน้ำมันทั้งหมดเท่ากับต้นทุนทั้งหมดพอดี จะมีจุดเริ่มต้นที่ถูกต้องเพียงจุดเดียว เมื่อสถานีใดสถานีหนึ่งมีน้ำมันเพียงพอสำหรับเดินทางครบรอบ สถานีนั้นคือคำตอบ ควรตรวจสอบคำตอบแบบละโมบกับกรณีเสื่อมเหล่านี้เสมอ

from collections import Counter

def least_interval(tasks, n):
    if n == 0: return len(tasks)  # no cooldown
    cnt = Counter(tasks)
    mf = max(cnt.values())
    mc = sum(1 for c in cnt.values() if c == mf)
    return max(len(tasks), (mf-1)*(n+1)+mc)

# Edge cases for task scheduler
print(least_interval(['A','A','A'], 2))   # 7: A _ _ A _ _ A
print(least_interval(['A','A','B','B'], 0)) # 4: no idle
print(least_interval(['A','B','C','D'], 3))  # 4: all diff, no idle needed

# Edge case for gas station
def gas_station(gas, cost):
    if sum(gas) < sum(cost): return -1
    tank = start = 0
    for i,(g,c) in enumerate(zip(gas,cost)):
        tank += g-c
        if tank < 0: tank=0; start=i+1
    return start

print(gas_station([5,1,2,3,4],[4,4,1,5,1]))  # 4

การจดจำรูปแบบแบบละโมบ

ทั้งตัวจัดตารางงานและสถานีเติมน้ำมันใช้รูปแบบแบบละโมบดังนี้: (1) ระบุคอขวด ได้แก่ งานที่ปรากฏบ่อยที่สุดหรือดุลน้ำมันสุทธิ (2) ตัดสินใจในการวนรอบครั้งเดียวโดยใช้ตัวแปรสะสม ได้แก่ ความถี่สูงสุดและปริมาณน้ำมันในถัง (3) เริ่มต้นใหม่หรือรีเซ็ตเมื่อเงื่อนไขถูกละเมิด ปัญหาแบบละโมบที่ควรรู้จัก ได้แก่ การเลือกกิจกรรม การเข้ารหัสฮัฟฟ์แมน ปัญหากระเป๋าเป้แบบเศษส่วน เกมกระโดด ตัวจัดตารางงาน สถานีเติมน้ำมัน และการรวมช่วง แต่ละปัญหามีการพิสูจน์ด้วยอาร์กิวเมนต์การสับเปลี่ยนหรือค่าคงที่ทางคณิตศาสตร์

# Greedy pattern summary
# Task Scheduler:
#   Bottleneck: max frequency task
#   Formula: max(total_tasks, (max_freq-1)*(n+1)+max_count)
#   O(n) time, O(1) space

# Gas Station:
#   Bottleneck: running sum of (gas-cost) going negative
#   Reset start when tank < 0, valid if total sum >= 0
#   O(n) time, O(1) space

# Both avoid the need for DP by using a clever single-pass insight
from collections import Counter
def combined_demo(tasks, n, gas, cost):
    ti = max(len(tasks), (max(Counter(tasks).values())-1)*(n+1) +
             sum(1 for c in Counter(tasks).values() if c==max(Counter(tasks).values())))
    tank = start = 0
    gs = sum(g-c for g,c in zip(gas,cost)) >= 0
    return ti, start if gs else -1

ตรวจสอบความเข้าใจอย่างรวดเร็ว

ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์เขียนโปรแกรมจากบทเรียนนี้

สรุปบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า: คำตอบของตัวจัดตารางงานคือค่าสูงสุดระหว่างจำนวนงานทั้งหมดกับ (ความถี่สูงสุด−1)×(n+1)+จำนวนงานที่มีความถี่สูงสุด ซึ่งได้มาจากการเติมตารางกรอบเวลาด้วยงานที่ปรากฏบ่อยที่สุด, สถานีเติมน้ำมันใช้การวนรอบครั้งเดียว โดยตั้งจุดเริ่มต้นใหม่เป็น i+1 ทุกครั้งที่ปริมาณน้ำมันในถังติดลบ และจะมีคำตอบที่ถูกต้องเมื่อปริมาณน้ำมันทั้งหมด ≥ ต้นทุนทั้งหมด และ ปัญหาทั้งสองใช้เวลา O(n) และพื้นที่ O(1) โดยระบุค่าคงที่ทางคณิตศาสตร์แทนการค้นหาอย่างครบถ้วน บทถัดไป เราจะศึกษาแม่แบบการแบ่งแยกและพิชิต รวมถึงการประยุกต์ใช้นอกเหนือจากการเรียงลำดับแบบผสาน

คำถามที่พบบ่อย

บทเรียน “ตัวจัดตารางงานและสถานีเติมน้ำมัน” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “ตัวจัดตารางงานและสถานีเติมน้ำมัน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ตัวจัดตารางงานและสถานีเติมน้ำมัน”

ประยุกต์การให้เหตุผลแบบละโมบกับปัญหาระยะพักของตัวจัดตารางงาน CPU และปัญหาการตรวจสอบความเป็นไปได้ของสถานีเติมน้ำมันแบบวงกลม คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “ตัวจัดตารางงานและสถานีเติมน้ำมัน” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม

ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. ละโมบกับ DP: ควรใช้แบบใด
  2. การจัดตารางช่วงเวลาและการรวมช่วง
  3. เกมกระโดด I และ II
  4. ตัวจัดตารางงานและสถานีเติมน้ำมัน
← กลับไปที่ DSA Interview Prep