Implement a Dependency-Aware Task Scheduler
Company: Scale AI
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: easy
Interview Round: Technical Screen
Quick Answer: This question evaluates skills in dependency management, deadline-driven scheduling and algorithmic data-structure design, focusing on handling directed acyclic graphs and ordering constraints.
Constraints
- 1 <= total number of tasks across all add operations <= 200000
- 0 <= total number of dependency links across all tasks <= 200000
- 0 <= ddl <= 10^9
- Task IDs are unique integers
- The dependency graph is acyclic and all prerequisite IDs are valid task IDs that may appear in earlier or later add operations
Examples
Input: [('add', [{'id': 1, 'ddl': 5, 'subTasks': []}, {'id': 2, 'ddl': 3, 'subTasks': []}]), ('consume',), ('consume',), ('consume',)]
Expected Output: [2, 1, 'NO_TASK']
Explanation: Tasks 1 and 2 are immediately executable. Task 2 has the earlier deadline, then task 1, then nothing remains.
Input: [('add', [{'id': 1, 'ddl': 5, 'subTasks': []}, {'id': 2, 'ddl': 1, 'subTasks': [1]}, {'id': 3, 'ddl': 4, 'subTasks': []}]), ('consume',), ('consume',), ('consume',), ('consume',)]
Expected Output: [3, 1, 2, 'NO_TASK']
Explanation: Task 2 has the smallest deadline but is blocked by task 1. So task 3 is consumed first, then 1, which unlocks 2.
Input: [('add', [{'id': 2, 'ddl': 1, 'subTasks': [1]}]), ('consume',), ('add', [{'id': 1, 'ddl': 2, 'subTasks': []}]), ('consume',), ('consume',)]
Expected Output: ['NO_TASK', 1, 2]
Explanation: Task 2 is added before its prerequisite task 1, so the first consume finds nothing executable. After task 1 is added and consumed, task 2 becomes executable.
Input: [('add', [{'id': 1, 'ddl': 10, 'subTasks': []}]), ('consume',), ('add', [{'id': 3, 'ddl': 2, 'subTasks': [1]}, {'id': 2, 'ddl': 1, 'subTasks': []}]), ('consume',), ('consume',)]
Expected Output: [1, 2, 3]
Explanation: Task 1 is consumed before task 3 is added. Since task 3 depends only on 1, it is immediately executable when added. Task 2 still goes first because it has the smaller deadline.
Input: [('add', [{'id': 1, 'ddl': 5, 'subTasks': []}, {'id': 4, 'ddl': 5, 'subTasks': []}, {'id': 3, 'ddl': 5, 'subTasks': []}]), ('consume',), ('consume',), ('consume',)]
Expected Output: [1, 3, 4]
Explanation: All tasks are executable with the same deadline, so ties are broken by smaller task ID.
Input: [('add', [{'id': 1, 'ddl': 7, 'subTasks': []}, {'id': 2, 'ddl': 4, 'subTasks': []}, {'id': 3, 'ddl': 6, 'subTasks': [1, 2]}, {'id': 4, 'ddl': 2, 'subTasks': [2]}, {'id': 5, 'ddl': 1, 'subTasks': [3, 4]}]), ('consume',), ('consume',), ('consume',), ('consume',), ('consume',)]
Expected Output: [2, 4, 1, 3, 5]
Explanation: Task 2 is consumed first, unlocking 4. After 1 is later consumed, task 3 becomes executable. Once both 3 and 4 are done, task 5 is unlocked.
Input: []
Expected Output: []
Explanation: No operations means there are no consume results to return.
Hints
- You need one structure to quickly get the executable task with the smallest deadline, and another structure to update tasks that become executable after a prerequisite is consumed.
- Track, for each task, how many prerequisites are still unfinished, and keep reverse edges from each prerequisite to the tasks that depend on it.