Return a Valid Dependency Order for Named Tasks
CitadelSoftware Engineer · Technical ScreenLast updated
Implement a deterministic topological sort for a directed acyclic graph of named tasks with cycle detection and lexicographical tie-breaking.
Problem Statement
You are building an execution engine for an automated build pipeline. You are given a list of task dependencies dependencies, where each element is a pair (u, v) indicating that task u must be completed before task v can begin.
Write a function to return the execution order of all tasks such that every dependency requirement is satisfied. Your solution must satisfy the following requirements:
- Completeness: Every unique task appearing in
dependencies(either as prerequisiteuor dependentv) must be included in the output. - Deduplication: Duplicate dependency pairs in the input must be handled correctly without affecting in-degree calculations.
- Deterministic Order: If multiple tasks are eligible to run at the same time (i.e., all their prerequisites have finished), pick the task with the lexicographically smallest name first.
- Cycle Detection: If a circular dependency exists such that not all tasks can be completed, return an empty list
[].
Function Signature
def find_task_order(dependencies: list[tuple[str, str]]) -> list[str]:
pass
Input & Output Format
- Input:
dependencies, a list of tuples(u, v)whereuandvare non-empty strings representing task names. - Output: A list of strings representing the ordered task sequence, or
[]if a cycle prevents scheduling all tasks.
Constraints
0 <= len(dependencies) <= 10^5- Task names consist of lowercase and uppercase English letters, digits, and underscores.
1 <= len(task_name) <= 50
Examples
Example 1
Input: dependencies = [
("build", "test"),
("lint", "test"),
("fetch", "build"),
("setup", "lint")
]
Output: ["fetch", "setup", "build", "lint", "test"]
Explanation: Tasks "fetch" and "setup" have no prerequisites. "fetch" < "setup", so "fetch" runs first, unblocking "build". Next, among available tasks {"build", "setup"}, "build" comes before "setup". Following this logic with lexicographical tie-breaking yields ["fetch", "setup", "build", "lint", "test"].
Example 2
Input: dependencies = [
("A", "B"),
("B", "C"),
("C", "A")
]
Output: []
Explanation: There is a cycle A -> B -> C -> A. No task can ever have zero prerequisites satisfied, so return [].
Edge Cases
- Empty input list
dependencies = []should return[]. - Redundant duplicate edges like
[("A", "B"), ("A", "B")]should not artificially inflateB's in-degree. - Disconnected graph components should all be scheduled, ordered by lexicographical priority among currently available roots.
Hints
Hint 1
Identify all unique tasks first, and make sure duplicate dependency pairs are filtered using a set so that in-degrees are accurately calculated.
Hint 2
Use Kahn's algorithm with a min-heap (priority queue) instead of a standard FIFO queue to always extract the lexicographically smallest eligible task next.
Hint 3
Compare the number of tasks in your generated execution order against the total number of unique tasks to detect cycles.
Follow-up questions
- How would you modify the solution to support isolated tasks that have neither incoming nor outgoing dependencies?
- If tasks could be executed concurrently on K worker machines, how would you produce the schedule that minimizes total elapsed time?
- How does the time complexity change if task names are arbitrary strings of length L, and how can you optimize the heap operations?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.