Dependency order
“Our build service receives named tasks and prerequisite relationships. It currently executes the submitted order, sometimes packaging files before compilation finishes. Return an order that respects every prerequisite, or reject the plan. What must we clarify before accepting a task graph?”
Write this:
def dependency_order(tasks, dependencies):
... # dependencies contains (prerequisite, dependent); cycles raise ValueError.
Constructed practice question; no company attribution. First solve independently.
Prerequisite: graph traversal. A directed edge a → b
means a must finish before b can start. An incoming-edge count is called indegree.
| Contract | Required behavior |
|---|---|
| Input | Distinct hashable task IDs; pairs (prerequisite, dependent) |
| Output | Every task once in a valid order; ties follow input/edge discovery order |
| Boundaries | Empty input returns []; duplicate edges count once |
| Failure | Unknown IDs, duplicate task IDs, or a cycle raise ValueError |
| Scope | Planning only; durations, retries, and parallel execution excluded |
Optional refresher · the underlying tool
Dependencies form directed arrows prerequisite → task. remaining_prerequisites[task] counts unfinished prerequisites; a task joins the ready queue exactly when that count reaches zero:
from collections import deque
ready = deque(t for t in tasks if remaining_prerequisites[t] == 0)
For A→C and B→C, C waits for both. Dedupe repeated edges before incrementing indegree; unfinished nodes after the queue empties indicate a cycle.
A design choice worth saying aloud
remaining_prerequisites declines as prerequisites finish, while the queue contains exactly tasks at zero. Deduplicate edges before counting so two copies of A → C do not require A to finish twice. A nonempty remainder after the queue drains is cycle evidence, not a partial success.
What the interviewer expects
The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.
Done means: Every task once in a valid order; ties follow input/edge discovery order.
Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.
Test-case scenarios to settle before coding
01 · Representative
- Input / starting state
- tasks fetch,parse,save,metrics; fetch→parse→save
- Expected result
[fetch,metrics,parse,save]
What it is testing: Independent tasks must not disappear.
02 · Empty
- Input / starting state
- no tasks or edges
- Expected result
[]
What it is testing: An empty plan is valid.
03 · Duplicate edge
- Input / starting state
- submit fetch→parse twice
- Expected result
- count the prerequisite once
What it is testing: Indegree represents unique requirements.
04 · Partial cycle
- Input / starting state
- fetch→parse→save→fetch plus metrics
- Expected result
ValueError
What it is testing: A runnable vertex does not make the whole plan valid.
05 · Unknown task
- Input / starting state
- edge mentions undeclared task
- Expected result
ValueError
What it is testing: The graph is closed over declared IDs.
06 · Long chain
- Input / starting state
- thousands of serial tasks
- Expected result
- every task once without recursion failure
What it is testing: Work should be O(V+E).
For each case, show which branch or state change produces that result.
For tasks [fetch, parse, save, metrics] and edges fetch → parse → save, return
[fetch, metrics, parse, save]. Metrics is independent and still belongs in the
answer. Adding save → fetch must fail, even though metrics can run. Ask whether the
caller needs any order, lexical order, or a cycle explanation: these change work.
Before opening the answer, predict the ready queue after fetch completes. Implement the function and explain why isolated tasks cannot disappear.
Solution, trace, and changing requirements
A baseline repeatedly scans all unfinished tasks, searching for one whose
prerequisites are complete. A chain of V tasks can require V scans of V tasks,
plus dependency checks. Sorting the supplied names has no relationship to edge
direction; [save, fetch] with fetch → save is a direct counterexample.
Store each task's outgoing neighbors and remaining indegree. Initialize a FIFO queue with zero-indegree tasks. Removing one represents completion: append it to the answer, decrement each dependent once, and enqueue newly zero counts.
each remaining indegree equals the number of prerequisites not yet emitted. Consequently every queued task is safe to emit. Every edge is removed once. If tasks remain when the queue empties, every remaining vertex has an incoming edge; repeatedly following predecessors in a finite graph must revisit a vertex. That establishes a cycle without confusing a cycle with a merely disconnected graph.
| Completed | Ready queue | Remaining parse/save counts |
|---|---|---|
| Nothing | fetch, metrics | 1 / 1 |
| fetch | metrics, parse | 0 / 1 |
| metrics | parse | 0 / 1 |
| parse | save | 0 / 0 |
Time is O(V + E) including duplicate-edge processing, with E counting submitted edges. Auxiliary space is O(V + U), where U is unique edges, excluding the O(V) returned order. No recursion stack is required. A min-heap gives lexical choices at an additional O(V log V) cost.
Follow-up 1 — explain the failure. Add save → fetch. Predict which tasks still
emit before inspecting the changed graph. The answer is metrics only. Kahn's
remaining set identifies blocked tasks, not necessarily exactly cycle members;
DFS colors and parent edges can extract one actual cycle.
Follow-up 2 — run tasks concurrently. Readiness still depends on indegree, but decrement after successful completion, not dispatch. A failure must block or explicitly cancel its dependents. Draw separate ready, running, and completed sets; worker capacity limits dispatch, while graph correctness limits eligibility.
A senior candidate proves cycle detection and tests disconnected/duplicate inputs. Lead depth adds ownership of cancellation and incremental plan changes; this local ordering function does not supply a distributed scheduler guarantee.
Reference: solution.py (download file, source below). Tests check edge order, duplicate normalization, partial cycles, invalid IDs, and a long chain.
"""Deterministic topological order with duplicate-edge normalization."""
from collections import deque
def dependency_order(tasks, dependencies):
"""dependencies contains (prerequisite, dependent); cycles raise ValueError."""
tasks = list(tasks)
if len(set(tasks)) != len(tasks):
raise ValueError("duplicate task")
dependents = {task: [] for task in tasks}
remaining_prerequisites = dict.fromkeys(tasks, 0)
unique_edges = set()
for before, after in dependencies:
if before not in dependents or after not in dependents:
raise ValueError("unknown task")
if (before, after) not in unique_edges:
unique_edges.add((before, after))
dependents[before].append(after)
remaining_prerequisites[after] += 1
ready = deque(task for task in tasks if remaining_prerequisites[task] == 0)
result = []
while ready:
task = ready.popleft()
result.append(task)
for child in dependents[task]:
remaining_prerequisites[child] -= 1
if remaining_prerequisites[child] == 0:
ready.append(child)
if len(result) != len(tasks):
raise ValueError("dependency cycle")
return result
python -m unittest discover -s curriculum/01-code/02-data-structures-algorithms/problems/21-dependency-order -p 'test_*.py'