2365. Task Scheduler II
Problem Description
You have an array of tasks that must be completed in the exact order they appear. Each task has a type (represented by a positive integer), and there's a cooling period between tasks of the same type.
Given:
- A 0-indexed array
tasks
wheretasks[i]
represents the type of thei-th
task - A positive integer
space
representing the minimum number of days that must pass after completing a task before you can do another task of the same type
Each day you can either:
- Complete the next task in the array (in order)
- Take a break (do nothing)
The goal is to find the minimum number of days needed to complete all tasks while respecting the cooling period constraint.
For example, if space = 2
and you complete a task of type 1
on day 1, the earliest you can do another task of type 1
is on day 4 (with at least 2 days in between).
The solution uses a hash table to track when each task type can next be executed. As we process each task:
- We move forward at least one day (
ans += 1
) - If the task type has a cooling period restriction, we may need to wait longer (take breaks) until that task type is available
- We update
ans
to be the maximum of the current day or the earliest day this task type can be executed - We record that the next time this task type can be done is
ans + space + 1
days
The final answer is the total number of days needed to complete all tasks in order.
Intuition
The key insight is that we need to track when each task type becomes available again after being executed. Since tasks must be completed in order, we're essentially simulating the process day by day.
Think of it this way: when we complete a task of type X
, we create a "cooldown window" where we cannot do another task of type X
for space
days. If we encounter another task of type X
during this cooldown, we must wait (take breaks) until the cooldown expires.
The natural approach is to maintain a record of "when can I next execute each task type?" For each task type, we store the earliest day it can be executed again.
As we process each task in order:
- At minimum, we need one more day than the previous task (since we do one task per day)
- But if this task type is still in cooldown, we might need to wait longer
The clever part is using max(current_day, cooldown_end_day)
to handle both cases - either we can do the task immediately (current day is later than cooldown), or we need to wait until the cooldown ends.
After executing a task on day ans
, that task type cannot be done again until day ans + space + 1
. The +1
is crucial because if we do a task on day 5 with space = 2
, we need 2 full days of gap (days 6 and 7), so the earliest we can do it again is day 8, which is 5 + 2 + 1
.
This greedy approach works because we always complete tasks as early as possible while respecting constraints, and since tasks must be done in order, there's no benefit to delaying a task unnecessarily.
Solution Approach
We use a hash table combined with simulation to solve this problem efficiently.
Data Structure:
- Hash table
day
: Maps each task type to the next available day it can be executed. Initially, all values are0
(any task can be done from day 0 onwards). - Variable
ans
: Tracks the current day/time.
Algorithm Steps:
-
Initialize the tracking structures:
- Create a
defaultdict(int)
namedday
to store the next available day for each task type - Set
ans = 0
to represent the current time
- Create a
-
Process each task in order: For each
task
in thetasks
array:a. Advance time by at least one day:
ans += 1
This represents doing the current task on the next available day.
b. Check cooling period constraint:
ans = max(ans, day[task])
If
day[task] > ans
, it means this task type is still in cooldown. We must wait untilday[task]
to execute it. Otherwise, we can execute it on the current dayans
.c. Update the next available time for this task type:
day[task] = ans + space + 1
After executing the task on day
ans
, the next time we can execute the same task type isans + space + 1
(current day + cooling period + 1). -
Return the result: After processing all tasks,
ans
contains the total number of days needed.
Why this works:
- We process tasks in the required order (no reordering allowed)
- For each task, we find the earliest possible day to execute it
- The
max
operation ensures we respect both the sequential constraint (at least one day after the previous task) and the cooling period constraint - By updating
day[task]
after each execution, we maintain accurate cooldown information for future tasks of the same type
Time Complexity: O(n)
where n
is the number of tasks
Space Complexity: O(m)
where m
is the number of unique task types
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through an example with tasks = [1, 1, 2, 1]
and space = 2
.
Initial State:
day = {}
(empty hash table, all task types can be executed from day 0)ans = 0
(starting at day 0)
Processing Task 0 (type 1):
- Advance time:
ans = 0 + 1 = 1
- Check cooling constraint:
ans = max(1, day[1]) = max(1, 0) = 1
- Execute task type 1 on day 1
- Update next available:
day[1] = 1 + 2 + 1 = 4
- State:
day = {1: 4}
,ans = 1
Processing Task 1 (type 1):
- Advance time:
ans = 1 + 1 = 2
- Check cooling constraint:
ans = max(2, day[1]) = max(2, 4) = 4
- Must wait until day 4 to execute task type 1 again (days 2, 3 are breaks)
- Update next available:
day[1] = 4 + 2 + 1 = 7
- State:
day = {1: 7}
,ans = 4
Processing Task 2 (type 2):
- Advance time:
ans = 4 + 1 = 5
- Check cooling constraint:
ans = max(5, day[2]) = max(5, 0) = 5
- Execute task type 2 on day 5 (no cooling needed, first time doing type 2)
- Update next available:
day[2] = 5 + 2 + 1 = 8
- State:
day = {1: 7, 2: 8}
,ans = 5
Processing Task 3 (type 1):
- Advance time:
ans = 5 + 1 = 6
- Check cooling constraint:
ans = max(6, day[1]) = max(6, 7) = 7
- Must wait until day 7 to execute task type 1 again
- Update next available:
day[1] = 7 + 2 + 1 = 10
- State:
day = {1: 10, 2: 8}
,ans = 7
Final Result: 7 days
The schedule would be:
- Day 1: Task 0 (type 1)
- Day 2-3: Break (cooling period for type 1)
- Day 4: Task 1 (type 1)
- Day 5: Task 2 (type 2)
- Day 6: Break (cooling period for type 1)
- Day 7: Task 3 (type 1)
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def taskSchedulerII(self, tasks: List[int], space: int) -> int:
6 # Dictionary to store the earliest day each task type can be executed again
7 next_available_day = defaultdict(int)
8
9 # Current day counter (representing the total days needed)
10 current_day = 0
11
12 # Process each task in order
13 for task in tasks:
14 # Move to the next day (at minimum)
15 current_day += 1
16
17 # If this task type was executed before, we might need to wait
18 # Update current_day to be at least the next available day for this task
19 current_day = max(current_day, next_available_day[task])
20
21 # After executing this task on current_day, calculate when
22 # this task type can be executed again (current day + space + 1)
23 next_available_day[task] = current_day + space + 1
24
25 # Return the total number of days needed to complete all tasks
26 return current_day
27
1class Solution {
2 public long taskSchedulerII(int[] tasks, int space) {
3 // Map to store the earliest day each task type can be executed next
4 Map<Integer, Long> nextAvailableDay = new HashMap<>();
5
6 // Current day counter (represents the answer)
7 long currentDay = 0;
8
9 // Process each task in order
10 for (int taskType : tasks) {
11 // Move to the next day (at minimum)
12 currentDay++;
13
14 // If this task type was executed before, we might need to wait
15 // Take the maximum of current day or the earliest day this task can be executed
16 currentDay = Math.max(currentDay, nextAvailableDay.getOrDefault(taskType, 0L));
17
18 // Update when this task type can be executed next
19 // It will be current day + space + 1 (accounting for the cooldown period)
20 nextAvailableDay.put(taskType, currentDay + space + 1);
21 }
22
23 return currentDay;
24 }
25}
26
1class Solution {
2public:
3 long long taskSchedulerII(vector<int>& tasks, int space) {
4 // Map to store the earliest day each task type can be executed next
5 unordered_map<int, long long> nextAvailableDay;
6
7 // Current day counter (represents the answer)
8 long long currentDay = 0;
9
10 // Process each task in order
11 for (int& task : tasks) {
12 // Move to the next day at minimum
13 ++currentDay;
14
15 // If this task type was done before, we might need to wait longer
16 // Ensure we're at least at the day when this task can be executed
17 currentDay = max(currentDay, nextAvailableDay[task]);
18
19 // Update when this task type can be executed next
20 // It will be current day + space + 1 (space days gap + 1 for next execution)
21 nextAvailableDay[task] = currentDay + space + 1;
22 }
23
24 return currentDay;
25 }
26};
27
1/**
2 * Calculates the minimum number of days needed to complete all tasks with cooldown periods
3 * @param tasks - Array of task IDs to be completed
4 * @param space - Minimum number of days that must pass before the same task can be done again
5 * @returns The minimum number of days to complete all tasks
6 */
7function taskSchedulerII(tasks: number[], space: number): number {
8 // Map to store the next available day for each task type
9 const nextAvailableDay = new Map<number, number>();
10
11 // Current day counter
12 let currentDay = 0;
13
14 // Process each task in order
15 for (const taskId of tasks) {
16 // Move to the next day
17 currentDay++;
18
19 // If this task type has a cooldown, we might need to wait longer
20 // Use the maximum of current day or the next available day for this task
21 currentDay = Math.max(currentDay, nextAvailableDay.get(taskId) ?? 0);
22
23 // Set the next available day for this task type (current day + space + 1)
24 // This ensures the cooldown period is respected
25 nextAvailableDay.set(taskId, currentDay + space + 1);
26 }
27
28 return currentDay;
29}
30
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array tasks
. The algorithm iterates through the tasks array exactly once, and for each task, it performs constant-time operations: accessing and updating a dictionary entry, comparing two values with max()
, and performing basic arithmetic operations.
The space complexity is O(n)
in the worst case, where n
is the length of the array tasks
. This occurs when all tasks in the array are unique, requiring the day
dictionary to store up to n
different task types as keys with their corresponding values. In the best case where all tasks are identical, the space complexity would be O(1)
, but we consider the worst-case scenario for space complexity analysis.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Off-by-One Error in Cooling Period Calculation
The Problem: A common mistake is incorrectly calculating when a task type becomes available again. Developers often write:
next_available_day[task] = current_day + space # WRONG!
This incorrectly allows the same task type to be executed with only space-1
days in between, violating the constraint.
Why it happens:
The confusion arises from misunderstanding what "space" represents. If space = 2
, there must be at least 2 days between tasks of the same type, not including the days when the tasks are executed.
Example of the error:
- Task type 1 executed on day 1
- With incorrect formula: next available = 1 + 2 = day 3
- This gives only 1 day of gap (day 2), not 2 days
The Correct Solution:
next_available_day[task] = current_day + space + 1 # CORRECT!
This ensures exactly space
days between task executions:
- Task executed on day 1
- Next available = 1 + 2 + 1 = day 4
- Gap days: day 2 and day 3 (exactly 2 days)
Pitfall 2: Forgetting to Increment the Day Counter
The Problem: Some implementations forget to always move forward in time, leading to incorrect results:
# WRONG approach
for task in tasks:
current_day = max(current_day, next_available_day[task]) # Missing increment!
next_available_day[task] = current_day + space + 1
Why it fails:
Without incrementing current_day
first, multiple consecutive tasks of different types could be assigned to the same day, which violates the constraint that you can only complete one task per day.
The Correct Solution: Always increment the day counter before checking constraints:
for task in tasks:
current_day += 1 # Always move forward at least one day
current_day = max(current_day, next_available_day[task])
next_available_day[task] = current_day + space + 1
Pitfall 3: Using Regular Dictionary Instead of DefaultDict
The Problem: Using a regular dictionary requires checking if a key exists before accessing it:
# More error-prone approach
next_available_day = {}
for task in tasks:
current_day += 1
if task in next_available_day: # Extra check needed
current_day = max(current_day, next_available_day[task])
next_available_day[task] = current_day + space + 1
Why it's problematic: While not incorrect, this approach adds unnecessary complexity and potential for bugs if you forget the existence check.
The Better Solution:
Use defaultdict(int)
which automatically returns 0 for missing keys:
next_available_day = defaultdict(int) # Cleaner and safer
This simplifies the code and eliminates a potential source of KeyError exceptions.
How does quick sort divide the problem into subproblems?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!