Quick Overview

Given named tasks and before-and-after dependency pairs, return an order that satisfies every dependency, choosing the alphabetically smallest valid order, or an empty list when the dependencies contain a cycle. It tests topological sorting, cycle detection and deterministic tie-breaking.

Order Named Tasks by Dependencies, Choosing the Alphabetically Smallest Order

Company: ByteDance

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Onsite

You are given the names of a set of tasks and a list of dependencies, where a dependency `[before, after]` means task `before` must be done before task `after`. Return an order of all the tasks that satisfies every dependency. If the dependencies contain a cycle, so that no such order exists, return an empty list. When more than one order is valid, return the alphabetically smallest one: compare two valid orders name by name, and at the first position where they differ, the order whose name comes first alphabetically is the smaller one. ### Function Signature ```python def task_order(tasks: list[str], dependencies: list[list[str]]) -> list[str]: ``` ### Rules - Every task appears exactly once in the result, including tasks that have no dependencies. - Names are compared with ordinary string comparison (Python's `<` on `str`). - A dependency may be listed more than once; repeats mean the same as a single copy. - A task that depends on itself is a cycle. If any cycle exists anywhere, return `[]`, even if some tasks could still be ordered. ### Constraints - `1 <= len(tasks) <= 10^4` - `0 <= len(dependencies) <= 5 * 10^4` - task names are distinct and consist of 1 to 20 lowercase English letters - every name in `dependencies` appears in `tasks` ### Examples **Example 1** ```text Input: tasks = ["deploy", "build", "test", "lint", "fetch"] dependencies = [["fetch", "build"], ["build", "test"], ["lint", "test"], ["test", "deploy"]] Output: ["fetch", "build", "lint", "test", "deploy"] ``` Only `fetch` and `lint` have no prerequisites, and `fetch` comes first alphabetically. After it, `build` is allowed and comes before `lint`. `test` waits for both `build` and `lint`, and `deploy` waits for `test`. **Example 2** ```text Input: tasks = ["a", "b", "c"] dependencies = [["a", "b"], ["b", "c"], ["c", "b"]] Output: [] ``` `b` and `c` each wait for the other. Task `a` could be done, but no complete order exists. **Example 3** ```text Input: tasks = ["zeta", "alpha", "mid"] dependencies = [] Output: ["alpha", "mid", "zeta"] ``` With no dependencies, the answer is simply alphabetical; the input order of `tasks` does not matter.

Overview: Given named tasks and before-and-after dependency pairs, return an order that satisfies every dependency, choosing the alphabetically smallest valid order, or an empty list when the dependencies contain a cycle. It tests topological sorting, cycle detection and deterministic tie-breaking.

You are given the names of a set of tasks, `tasks`, and a list of dependencies, `dependencies`. Each dependency is a pair `[before, after]` meaning task `before` must be done before task `after`. Return an order of all the tasks (a list of task names) that satisfies every dependency. If the dependencies contain a cycle, so that no such order exists, return an empty list. When more than one order is valid, return the alphabetically smallest one: compare two valid orders name by name, and at the first position where they differ, the order whose name comes first alphabetically is the smaller one. **Rules** - Every task appears exactly once in the result, including tasks that have no dependencies. - Names are compared with ordinary string comparison (Python's `<` on `str`); for example `"ab" < "abc" < "b"`. - A dependency may be listed more than once; repeats mean the same as a single copy. - A task that depends on itself is a cycle. If any cycle exists anywhere, return `[]`, even if some tasks could still be ordered. **Example 1** ``` Input: tasks = ["deploy", "build", "test", "lint", "fetch"] dependencies = [["fetch", "build"], ["build", "test"], ["lint", "test"], ["test", "deploy"]] Output: ["fetch", "build", "lint", "test", "deploy"] ``` Only `fetch` and `lint` have no prerequisites, and `fetch` comes first alphabetically. After it, `build` is allowed and comes before `lint`. `test` waits for both `build` and `lint`, and `deploy` waits for `test`. **Example 2** ``` Input: tasks = ["a", "b", "c"] dependencies = [["a", "b"], ["b", "c"], ["c", "b"]] Output: [] ``` `b` and `c` each wait for the other. Task `a` could be done, but no complete order exists. **Constraints** - `1 <= len(tasks) <= 10^4` - `0 <= len(dependencies) <= 5 * 10^4` - Task names are distinct and consist of 1 to 20 lowercase English letters. - Every name in `dependencies` appears in `tasks`. No numeric value is passed in or returned, so nothing here can exceed 2^31 - 1.

Constraints

  • 1 <= len(tasks) <= 10^4
  • 0 <= len(dependencies) <= 5 * 10^4
  • Task names are distinct and consist of 1 to 20 lowercase English letters.
  • Every name in dependencies appears in tasks.
  • Each dependency is a pair [before, after]; a pair may repeat, and before may equal after (a self-dependency, which is a cycle).

Examples

Input: (['deploy', 'build', 'test', 'lint', 'fetch'], [['fetch', 'build'], ['build', 'test'], ['lint', 'test'], ['test', 'deploy']])

Expected Output: ['fetch', 'build', 'lint', 'test', 'deploy']

Explanation: Source example 1: fetch beats lint among the first ready tasks, then the newly unlocked build beats lint, test waits for both build and lint, and deploy waits for test.

Input: (['a', 'b', 'c'], [['a', 'b'], ['b', 'c'], ['c', 'b']])

Expected Output: []

Explanation: Source example 2: b and c wait for each other, so no complete order exists even though a could be done.

Hints

  1. A task can be placed only after every task it waits for has been placed. At any moment, which tasks are allowed to go next?
  2. Two orders are compared at the first position where they differ. What does that say about the best choice for the very next position, given what has already been placed?
  3. If tasks remain but none of them is allowed to go next, what does that tell you about the dependencies?

Loading coding console...

Show the approach

Approach

Use Kahn's topological sort with a min-priority queue keyed by task name. For every task, count how many dependency entries point at it (its in-degree), and record each entry in the adjacency list of its before task. A task is ready exactly when every task it waits for has been placed, which means its remaining in-degree is 0. Put all initially ready tasks in the queue. Then repeatedly remove the alphabetically smallest ready task, append it to the answer, and decrement the in-degree of every task it unlocks, adding any task whose in-degree reaches 0.

Invariant: the queue holds exactly the not-yet-placed tasks whose prerequisites are all placed.

Correctness: in any valid order, the next task must be a ready task. When there is no cycle, placing any ready task leaves an acyclic remainder that can still be completed. Orders are compared at their first differing position, so the smallest valid order must put the smallest ready task at the current position. By induction, the greedy order is the smallest. The choice has to be remade after every single placement: a task unlocked by the last placement can be smaller than tasks that were ready earlier, so processing whole levels at once or using a FIFO queue gives a wrong answer.

Edge cases: tasks on a cycle never reach in-degree 0. That includes a self-dependency, whose own edge only the task itself could release. So fewer than n tasks get placed, and the function returns []. Repeated dependencies are counted in both the in-degree and the adjacency list, and one placement releases all copies together, so they behave like a single copy. Tasks with no dependencies are ready from the start and are interleaved purely by name. A single task with no dependencies is returned on its own.

Time complexity:
O(n log n + m), where n = len(tasks) and m = len(dependencies); each name comparison costs at most 20 characters
Space complexity:
O(n + m)