0Pricing
DSA Interview Prep · درس

جدولة المهام ومحطة الوقود

طبّق التفكير الجشع على مسألة فترة التبريد في جدولة مهام المعالج ومسألة إمكانية إكمال المسار الدائري بين محطات الوقود.

جدولة المهام ومحطة الوقود درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

مسألة Task Scheduler

Task Scheduler (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')

الصيغة الجشعة لـ Task Scheduler

الفكرة الأساسية: يحدد الزمن الإجمالي عدد مرات ظهور المهمة الأكثر تكرارًا. إذا ظهرت المهمة الأكثر تكرارًا f مرات، وكان عدد المهام ذات هذا التكرار هو max_count، فإن الزمن هو max(len(tasks), (f-1) * (n+1) + max_count). تنص الصيغة على إنشاء f-1 إطارات، حجم كل منها n+1، وملئها بالمهام الأخرى، ثم إضافة الدورة الأخيرة. إذا ملأت المهام الأخرى جميع الفترات الخاملة (بسبب تنوع المهام الكبير)، فنفّذ جميع المهام ببساطة من دون فترات خمول.

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-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(total_time × 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

مسألة Gas Station

Gas Station (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

الحل الجشع لمسألة Gas Station

الخوارزمية الجشعة: (1) إذا كان total gas < total cost، فلا يوجد حل (أعد -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

لماذا يصح اختيار محطة البدء بالجشع

حجة الصحة: إذا أصبح tank سالبًا بعد الوصول إلى المحطة i انطلاقًا من start، فلا يمكن لأي محطة بين start وi (شاملًا) أن تكون نقطة بدء صالحة — إذ سيكون لديها وقود أقل عند وصولها إلى المحطة i مما كان سيتوفر عند البدء من start. لذلك يمكننا تخطي جميع هذه المحطات بأمان وتجربة i+1. وبما أن حلًا موجود (total gas ≥ total cost)، فلا بد أن ينجح المرشح النهائي 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

القوة الغاشمة مقابل الجشع في Gas Station

تجرب القوة الغاشمة كل محطة بدء وتحاكي المسار الدائري كاملًا — بزمن 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، تكون الإجابة ببساطة len(tasks)، إذ لا نحتاج إلى فترات خمول. عندما تكون جميع المهام متماثلة، مثل أن تكون كلها '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) اتخاذ قرار أثناء المرور مرة واحدة باستخدام متغير جارٍ، مثل max_freq وtank. (3) إعادة البدء أو التصفير عند انتهاك أحد القيود. من مسائل الخوارزميات الجشعة الشائعة التي ينبغي معرفتها: Activity Selection وHuffman Coding وFractional Knapsack وJump Game وTask Scheduler وGas Station وMerge Intervals. ولكل منها برهان يعتمد على حجة الاستبدال أو على ثابت رياضي.

# 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

اختبار سريع

اختبر فهمك لمفاهيم Data Structures & Algorithms — Coding Interview Prep التي تناولها هذا الدرس.

مراجعة الدرس

في هذا الدرس تعلّمتَ ما يلي: إجابة جدولة المهام = max(total_tasks, (max_freq-1)*(n+1)+max_count) — وقد اشتُقت من ملء شبكات قائمة على الإطارات بالمهمة الأكثر تكرارًا، وتستخدم مسألة محطة الوقود مرورًا واحدًا، مع إعادة تعيين start=i+1 كلما أصبح tank سالبًا، وتكون صالحة عندما يكون إجمالي الوقود ≥ إجمالي التكلفة، كما أن كلتا المسألتين تعملان بزمن O(n) ومساحة O(1)، إذ تحددان ثابتًا رياضيًا بدلًا من البحث الشامل. بعد ذلك سندرس قالب التقسيم والحل وتطبيقاته التي تتجاوز فرز الدمج.

الأسئلة الشائعة

هل درس «جدولة المهام ومحطة الوقود» مجاني؟

نعم — نص درس «جدولة المهام ومحطة الوقود» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «جدولة المهام ومحطة الوقود»؟

طبّق التفكير الجشع على مسألة فترة التبريد في جدولة مهام المعالج ومسألة إمكانية إكمال المسار الدائري بين محطات الوقود. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟

لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «جدولة المهام ومحطة الوقود»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟

نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. الخوارزميات الجشعة مقابل البرمجة الديناميكية: متى تستخدم كلًّا منهما
  2. جدولة الفواصل ودمجها
  3. لعبة القفز I وII
  4. جدولة المهام ومحطة الوقود
← العودة إلى DSA Interview Prep