0Pricing
DSA Interview Prep · Lesson

Task Scheduler and Gas Station

Apply greedy reasoning to the CPU task-scheduler cooling period problem and the circular gas-station feasibility problem.

Task Scheduler and Gas Station is a free DSA Interview Prep lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DSA Interview Prep learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Task Scheduler Problem

Task Scheduler (LeetCode 621): given a list of CPU tasks (each labeled A-Z) and a cooldown n, find the minimum number of CPU intervals to finish all tasks. The same task must wait at least n intervals before running again. Idle intervals are allowed. For tasks ['A','A','A','B','B','B'] with cooldown 2, the answer is 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')

Greedy Formula for Task Scheduler

Key insight: the total time is determined by the most frequent task. If the most frequent task appears f times with count max_count (number of tasks with frequency f), the time is max(len(tasks), (f-1) * (n+1) + max_count). The formula: create f-1 frames of size n+1, fill them with other tasks, and add the last cycle. If other tasks fill all idle slots (many diverse tasks), just execute all tasks with no idle time.

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

Why the Formula Works

Visualise the schedule as a grid with n+1 columns (one task slot + n cooldown slots). The most frequent task A (frequency f) needs f rows. Between the first and last occurrence, there are f-1 full frames of n+1 slots. Plus the last partial frame containing all tasks with maximum frequency. If there are enough diverse tasks, they fill all idle slots, and the actual task count exceeds the frame time — take the larger of the two.

# 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)}')

Heap Simulation Alternative

A heap-based simulation gives the actual schedule (not just the count). At each step, take the most frequent available task (max-heap). After executing, apply cooldown: don't reinsert until n steps later. Use a queue to track cooling tasks. This runs in O(total_time × log k) where k is the number of distinct tasks. While correct, the formula is faster. Know both — interviewers may ask for the schedule itself.

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 Problem

Gas Station (LeetCode 134): there are n gas stations in a circle. Station i has gas[i] gas and costs cost[i] to travel to the next station. Starting with an empty tank, find the starting station from which you can complete the circuit. If no such station exists, return -1. The problem guarantees at most one valid answer if one exists.

# 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

Greedy Solution for Gas Station

Greedy algorithm: (1) If total gas < total cost, no solution exists (return -1). (2) Otherwise, exactly one solution exists. Find it with a single pass: track tank (current fuel) and start (candidate starting station). If tank < 0 after visiting a station, the current start cannot reach that station — reset tank = 0 and set start = i + 1. The final start is the answer.

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

Why the Greedy Start is Correct

Correctness argument: if tank goes negative after reaching station i from start, then no station between start and i (inclusive) can be a valid starting point — they all have less fuel when they reach station i than starting from start would provide. So we safely skip all of them and try i+1. Since a solution exists (total gas ≥ total cost), the final candidate start must work.

# 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

Brute Force vs Greedy for Gas Station

Brute force tries each starting station and simulates the full circuit — O(n²) time. The greedy single-pass solution is O(n) time and O(1) space. For an array of 10⁵ stations, the difference is 10¹⁰ operations vs 10⁵. The key mathematical property enabling the greedy: if the total net fuel is non-negative, a valid start exists, and it is always the station right after the last point where the running sum went negative.

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))

Related: Minimum Cost to Complete Trips

Minimum Time to Complete Trips (LeetCode 2187) is an answer-space binary search problem. You binary search on the time value T: given time T, buses with time[i] complete floor(T/time[i]) trips. If total trips ≥ totalTrips, T is sufficient. Find the minimum such T. This shows that greed applies at the meta-level (binary searching over answers) when no direct greedy rule exists at the object level.

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

Edge Cases and Verification

Important edge cases for both problems: Task Scheduler — when cooldown n=0, answer is simply len(tasks) (no idle needed). When all tasks are the same (e.g., all 'A'), idle slots fill exactly. When tasks have many distinct types, idle slots may be 0 (tasks fill all frames). Gas Station — when total gas exactly equals total cost, exactly one valid start exists. When a single station has enough gas for the whole circuit, that station is the answer. Always verify your greedy answer on these degenerate cases.

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

Greedy Pattern Recognition

Both Task Scheduler and Gas Station follow the greedy pattern: (1) Identify the bottleneck (most frequent task / net fuel balance). (2) Make a single-pass decision with a running variable (max_freq, tank). (3) Restart or reset when a constraint is violated. Common greedy problems to know: Activity Selection, Huffman Coding, Fractional Knapsack, Jump Game, Task Scheduler, Gas Station, Merge Intervals. Each has a proof by exchange argument or a mathematical invariant.

# 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

Quick Check

Test your understanding of Data Structures & Algorithms — Coding Interview Prep concepts from this lesson.

Lesson Recap

In this lesson you learned: Task Scheduler answer = max(total_tasks, (max_freq-1)*(n+1)+max_count) — derived from filling frame-based grids with the most frequent task, Gas Station uses a single pass, resetting start=i+1 whenever tank goes negative, valid when total gas ≥ total cost, and both problems use O(n) time and O(1) space by identifying a mathematical invariant instead of exhaustive search. Next up we study the Divide and Conquer template and its applications beyond merge sort.

Frequently asked questions

Is the “Task Scheduler and Gas Station” lesson free?

Yes — the full text of “Task Scheduler and Gas Station” is free to read here on the web, and the DSA Interview Prep course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DSA Interview Prep course, upgrade to CoddyKit PRO.

What will I learn in “Task Scheduler and Gas Station”?

Apply greedy reasoning to the CPU task-scheduler cooling period problem and the circular gas-station feasibility problem. You practise DSA Interview Prep with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start DSA Interview Prep?

No prior experience is required. DSA Interview Prep on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Task Scheduler and Gas Station” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this DSA Interview Prep lesson?

Yes. Every DSA Interview Prep lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Greedy vs DP: When to Use Each
  2. Interval Scheduling and Merging
  3. Jump Game I and II
  4. Task Scheduler and Gas Station
← Back to DSA Interview Prep