ตัวจัดตารางงานและสถานีเติมน้ำมัน
ประยุกต์การให้เหตุผลแบบละโมบกับปัญหาระยะพักของตัวจัดตารางงาน CPU และปัญหาการตรวจสอบความเป็นไปได้ของสถานีเติมน้ำมันแบบวงกลม
ตัวจัดตารางงานและสถานีเติมน้ำมัน เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding 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) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ตัวจัดตารางงานและสถานีเติมน้ำมัน”
ประยุกต์การให้เหตุผลแบบละโมบกับปัญหาระยะพักของตัวจัดตารางงาน CPU และปัญหาการตรวจสอบความเป็นไปได้ของสถานีเติมน้ำมันแบบวงกลม คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “ตัวจัดตารางงานและสถานีเติมน้ำมัน” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- ละโมบกับ DP: ควรใช้แบบใด
- การจัดตารางช่วงเวลาและการรวมช่วง
- เกมกระโดด I และ II
- ตัวจัดตารางงานและสถานีเติมน้ำมัน