Coding questions · Scale AI
Schedule Ready Tasks by Deadline
This question evaluates understanding of task scheduling and dependency management in directed acyclic graphs, along with algorithmic selection and data-structure-based optimization for repeated minimum-deadline retrieval.
You are building a workflow scheduler.
Each task has:
- a unique
task_id - an integer
deadline - an optional list of prerequisite tasks that must be completed before this task becomes ready
Assume the dependency graph is a DAG.
Implement the scheduler in three stages:
- Basic version: Given a list of tasks with no dependencies, return the task with the smallest deadline.
- Dependency-aware version: Tasks may have prerequisites. Repeatedly select the currently ready task with the smallest deadline, mark it as completed, and add any newly unlocked tasks to the ready set. Return a valid execution order.
- Optimized version: Improve the time complexity of selecting the next ready task by using an efficient data structure instead of scanning the full ready list each time. Analyze the time complexity.
If multiple ready tasks have the same deadline, you may break ties by task_id or return any consistent order.
Part 1: Earliest Deadline Task Without Dependencies
Find the task with the earliest deadline from a list of independent tasks (no dependencies).
What to implement
Implement solution(tasks).
- Input:
tasks— a list of(task_id, deadline)pairs (each pair is a 2-tuple of integers). - Output: the
task_id(an integer) of the task with the smallestdeadline, orNoneif the list is empty.
Rules
- Return the
task_idwhosedeadlineis the smallest. - Tie-break: if two or more tasks share the same smallest
deadline, return the one with the smallertask_id, so the answer is deterministic. - If
tasksis empty, returnNone.
Examples
[(101, 5), (102, 2), (103, 8)]→102— deadline2is the smallest.[(3, 4), (1, 4), (2, 6)]→1— tasks3and1both have deadline4; the smallertask_id(1) wins.[(7, 10)]→7— only one task.[]→None— empty input.
Constraints
0 <= len(tasks) <= 100000task_idvalues are unique integers.deadlineis an integer in the range[-10^9, 10^9](deadlines may be negative).
Hints
- A full sort is unnecessary; you only need to track the best task seen so far.
- Decide how to handle ties before you start scanning.
Approach
Approach: single-pass linear scan with a tie-break.
We need the task_id whose deadline is smallest, breaking ties toward the smaller task_id. A full sort is unnecessary — finding a minimum only requires one pass.
Steps:
- Empty guard: if
tasksis empty, returnNoneimmediately (the problem's required base case). - Seed: initialize
best_id, best_deadlinefromtasks[0]. Seeding from a real element avoids needing a sentinel like+infinityand keeps the comparison logic uniform. - Scan the rest: for each remaining
(task_id, deadline), replace the current best when either:deadline < best_deadline(a strictly earlier deadline always wins), ordeadline == best_deadline and task_id < best_id(same deadline, but this id is smaller).
Why it's correct: the loop maintains the invariant that (best_id, best_deadline) is the answer for everything seen so far, ordered first by deadline ascending, then by id ascending. Each new task is compared against that running best under exactly the same ordering, so after the final element the invariant holds over the whole list. The tie-break uses strict < on task_id, so an equal id never displaces itself, and since ids are unique the result is deterministic.
Edge cases handled: a single-element list returns that task's id directly; negative deadlines work because comparisons are plain integer <; the empty list returns None. Note the loop slices tasks[1:], which is a clean way to skip the already-seeded first element.
- Time complexity: O(n)
- Space complexity: O(1)
Part 2: Schedule Ready Tasks by Deadline
Schedule a set of tasks that have deadlines and prerequisite dependencies, always running the most urgent task that is currently allowed to run.
What to implement
Implement a function solution(tasks) that returns the order in which the tasks are executed, as a list of task_id values.
Input
tasks is a list of triples, one per task:
(task_id, deadline, prerequisites)
task_id— a unique integer identifying the task.deadline— an integer; smaller means more urgent.prerequisites— a list oftask_ids that must be completed before this task may run.
The dependency graph is guaranteed to be a DAG (no cycles). Every ID that appears in any prerequisites list also appears as a task in tasks.
Output
Return a list of task_ids giving the full execution order (every task appears exactly once).
If tasks is empty, return an empty list [].
Rules
A task is ready when all of its prerequisites have already been completed. A task with no prerequisites is ready from the start.
Process tasks one at a time:
- Look at the set of currently ready tasks.
- Choose the ready task with the smallest
deadline. - Tie-break: if several ready tasks share the smallest deadline, choose the one with the smallest
task_id. - Complete that task: append its
task_idto the output order, then unlock any task whose prerequisites are now all completed (those tasks become ready). - Repeat until every task has been scheduled.
Use a simple ready-list approach: each step, scan the current set of ready tasks to find the next task to run by the rules above.
Example
tasks = [(1, 4, []), (2, 2, []), (3, 3, [1, 2]), (4, 1, [2])]
solution(tasks) -> [2, 4, 1, 3]
- Initially ready: tasks
1(deadline 4) and2(deadline 2). Pick 2 (smaller deadline). - Completing
2unlocks4. Ready:1(deadline 4) and4(deadline 1). Pick 4. - Ready:
1. Pick 1, which unlocks3. - Ready:
3. Pick 3. Final order:[2, 4, 1, 3].
Constraints
0 <= number of tasks <= 30000 <= total number of prerequisite references <= 20000task_idvalues are unique integers.- Every prerequisite ID appears in the task list.
- The dependency graph is a DAG.
Hints
- Track how many prerequisites each task still needs; this is often called an indegree count.
- Keep a list of currently ready tasks, and linearly scan that list to find the next task to run.
Approach
This is a deadline-greedy topological sort over a DAG, using a simple ready-list that is rescanned each step.
Setup. We walk the task list once to record each task's deadline, its indegree (number of prerequisites, taken directly as len(prereqs)), and seed an empty adjacency list in graph. A second pass builds the edges: for every prereq pre of a task, we append the task to graph[pre], so graph maps prerequisite → dependents. Edges always point from a completed task to the tasks it unlocks.
Scheduling loop. ready starts with every task whose indegree == 0 (no prerequisites). While ready is non-empty:
- We linearly scan
readyto pick the best candidate — smallestdeadline, breaking ties by smallertask_id(deadline[a] < deadline[b] or (deadline[a] == deadline[b] and a < b)). - We
popthat task fromready, append it toorder, then for each dependentnxtingraph[task_id]we decrementindegree[nxt]. When a dependent's indegree hits0, all its prerequisites are done, so we push it ontoready.
Why it's correct. A task only becomes ready after every prerequisite is completed (indegree reaches 0), so prerequisite ordering is always respected. Among currently-eligible tasks, the explicit min-scan enforces the exact tie-break rule (deadline, then id). Because the input is a guaranteed DAG, every task eventually reaches indegree 0, so all n tasks are scheduled. The final len(order) != len(tasks) check is a safety guard (would catch a cycle), and the empty-input case returns [] early.
- Time complexity: O(n^2 + m), where n is the number of tasks and m is the total prerequisite references. The while loop runs n times, and each iteration linearly scans the ready list (up to O(n)), giving O(n^2); building the graph and decrementing indegrees over all edges contributes O(m).
- Space complexity: O(n + m): the deadline, indegree, and ready structures hold O(n) entries, while the adjacency lists in graph store O(m) edges in total.
Part 3: Optimized Deadline-Aware Task Scheduler
Schedule a set of tasks that have deadlines and prerequisite dependencies, always running the most urgent task that is currently runnable, and return the order in which the tasks execute.
This is the optimized version of the scheduler: instead of scanning the entire list of runnable tasks on every step to find the next one, you must use an efficient data structure (such as a min-heap / priority queue) to retrieve the next task in logarithmic time.
Function
def solution(tasks):
Input
tasks is a list of tasks. Each task is a tuple:
(task_id, deadline, prerequisites)
task_id— a unique integer identifying the task.deadline— an integer; smaller means more urgent.prerequisites— a list oftask_ids that must all be completed before this task can run. An empty list means the task has no prerequisites.
The prerequisite relationships form a DAG (directed acyclic graph), so there are no circular dependencies. Every task_id referenced as a prerequisite also appears as a task in the list.
Rules
- A task is ready (runnable) once all of its prerequisite tasks have been completed. A task with no prerequisites is ready from the start.
- At each step, among all currently ready tasks, execute the one with the smallest
deadline. If two ready tasks share the same deadline, execute the one with the smallertask_id. - Completing a task may make other tasks ready (when it was their last unmet prerequisite); those tasks then join the pool of ready tasks for future steps.
- Continue until all tasks have been executed.
Output
Return a list of task_ids giving the full execution order in which the tasks were run.
Edge cases
- If
tasksis empty, return an empty list[]. - A single task with no prerequisites simply produces a one-element list containing its
task_id.
Example
Given tasks = [(1, 4, []), (2, 2, []), (3, 3, [1, 2]), (4, 1, [2])]:
- Initially ready: tasks
1(deadline 4) and2(deadline 2). Run2(smaller deadline). - Completing
2makes4ready (deadline 1). Now ready:1(4),4(1). Run4. - Run
1(4). Completing1makes3ready (its other prerequisite2is already done). - Run
3.
The execution order is [2, 4, 1, 3].
Constraints
- 0 <= number of tasks <= 100000
- 0 <= total number of prerequisite references <= 200000
task_idvalues are unique integers- Every prerequisite ID appears in the task list
- The dependency graph is a DAG
Hints
- A min-heap is a natural way to repeatedly retrieve the smallest ready task.
- You still need the same graph bookkeeping as before: indegrees and edges from each prerequisite to the tasks it unlocks.
Approach
This is Kahn's topological sort driven by a min-heap instead of a FIFO queue, which gives the required deadline-aware ordering efficiently.
Setup. Three maps are built from the task list:
deadline[task_id]— each task's deadline.indegree[task_id]— number of prerequisites (the count of incoming edges).graph[pre]— adjacency list mapping a prerequisite to the tasks that depend on it.
The first loop records deadlines, indegrees, and an empty adjacency entry per task. The second loop fills graph: for every task_id with prerequisite pre, it appends task_id to graph[pre], so an edge points from prerequisite to dependent.
Selection. All tasks with indegree == 0 (no prerequisites) are pushed onto a heap as (deadline, task_id) tuples. Python's heapq orders by deadline first, then task_id, which exactly matches the rule "smallest deadline, ties broken by smaller task_id."
Main loop. Repeatedly pop the minimum tuple, append its task_id to order, then relax its outgoing edges: each dependent's indegree is decremented, and any dependent that reaches 0 becomes ready and is pushed. The heap always holds exactly the currently-ready tasks, so we never scan a full ready list — that is the optimization over the naive version.
Correctness. A task is emitted only after every prerequisite is popped (indegree hits 0), so all dependency constraints hold. Among ready tasks the heap guarantees the globally smallest (deadline, task_id) is chosen. The final len(order) != len(tasks) check would catch a cycle (returning []); for the guaranteed DAG it always passes. Empty input short-circuits to [].
- Time complexity: O(n log n + m)
- Space complexity: O(n + m)