2365. Task Scheduler II
Problem Description
The problem provides us with an array tasks
, where each element represents a type of task that needs to be completed in the order they are given. Furthermore, we are given an integer space
which denotes the minimum number of days we must wait before we can complete a task of the same type again.
Our goal is to find the minimum number of days required to complete all tasks. It's important to note that we have the option to either:
- Complete the next task in the array, or
- Take a break if the next task can't be performed due to the space constraint.
Intuition
The intuition behind the solution is to track when a task of a certain type can next be performed while iterating over the tasks
array. Here's the thought process:
- Each day, we can perform a task or take a break. If we decide to complete a task, we need to make sure enough days have passed according to
space
if we previously completed the same type of task. - A good way to track the earliest day we can perform each type of task again is by storing this information in a dictionary where the keys are task types and the values are the earliest day the task can be carried out again.
- We can initialize a counter, called
ans
, to keep track of the current day. For each task, we increment this counter by 1 to represent doing the task that day. - Before performing a task, we check whether the current day (
ans
) is at least as large as the least day this task is allowed to be performed on again (which can be found from the dictionary). - If we've already performed this task type and the earliest day we can perform it again is in the future (greater than
ans
), then we must fast-forwardans
to that day. - After completing a task, we update the dictionary entry for that task to the current day plus
space
, plus one more day to account for the completion day. - We continue this process for each task in the tasks array. The final value of
ans
after processing all tasks will be the minimum number of days required.
The provided solution uses a dictionary called day
for tracking, and updates this dictionary and ans
with each iteration over the tasks array to reflect the minimum days needed. The efficient use of the dictionary data structure and the iteration pattern yields an optimized approach to solving this problem without the need for complex data structures or algorithms, relying on clever bookkeeping instead.
Solution Approach
The solution to this task scheduling problem involves a simple yet effective approach. Here's how the algorithm is implemented, leveraging the Python defaultdict
data structure for efficient look-ups and updates:
-
A
defaultdict
of typeint
namedday
is created. This is used to store the minimum day on which a task of a particular type can next be executed. Since it's adefaultdict
, it will default any unset keys to0
which is a convenient starting point for ourans
since we need to start counting from day1
. -
A variable
ans
is initialized to0
. This variable represents the current day and it is incremented for each task, thus simulating the passage of each day as we perform tasks. -
We loop through each
task
in thetasks
list with afor
loop. The loop represents sequential task completion and ensures we respect the order of tasks as specified. -
For each task, we do the following:
- Increment the current day (
ans
) by1
since we're considering a task for completion on this new day. - The
max
function is used to identify if we should fast-forward the current day. It comparesans
and the value ofday[task]
, meaning it checks whether we've waited enough days (according tospace
) to perform the same type of task again. - If the value in
day[task]
is larger (i.e., the next permitted day for this type of task is in the future),ans
is updated today[task]
. Otherwise,ans
remains the same as it just got incremented. - After potentially performing the task (because we can perform it today or we have fast-forwarded to a day when we can), we update the dictionary entry
day[task]
toans + space + 1
. This is the next day we're allowed to perform a task of this type, which isspace
days from the current day plus one for the day the task is completed.
- Increment the current day (
-
Once we have processed all tasks, the value of
ans
is the total number of days required to complete all tasks, as it's been incremented and fast-forwarded as necessary according to the task ordering and spacing rules. The function returnsans
.
By utilizing the defaultdict
to keep track of the waiting period for each task type, and by incrementing and possibly fast-forwarding ans
on each iteration, the implementation minimizes the number of days required as per the space
constraint. The space-time efficiency of this approach is optimal since it processes each task in a single pass and updates the dictionary entries in constant time.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's take a small example to illustrate the solution approach. Suppose the array tasks
is [1, 1, 2, 1]
and space
is 2
. This means we need to wait at least 2 days before we can repeat the same task.
We initiate a defaultdict
named day
which will store the next available day a task can be performed. We also initiate a variable ans
as 0
representing the current day.
Now let's walk through the tasks:
-
The current task is
1
. Sinceday[1]
defaults to0
, andans
is0
, we incrementans
to1
and then update theday[1]
toans + space + 1
, which equals1 + 2 + 1 = 4
. Now,day[1] = 4
. -
The next task is again
1
.day[1]
is4
, butans
is currently1
, so we can't perform this task today. We then fast-forwardans
to4
which is the next available day to perform task1
. We complete the task, and updateday[1]
again to4 + 2 + 1 = 7
because now the next time we can perform task1
is after space days from day4
. -
Our next task is
2
. Checkingday[2]
which is0
since this task has not been performed yet andans
is4
. We can perform this task since the space constraint is met. We incrementans
to5
and setday[2]
to5 + 2 + 1 = 8
. -
The final task is
1
. We checkday[1]
which is currently7
.ans
is5
, meaning we need to fast-forward to day7
to perform task1
. We incrementans
to7
and perform the task updatingday[1]
now to7 + 2 + 1 = 10
.
Having processed all tasks, ans
is 7
indicating the minimum number of days required to complete all tasks given their order and the space constraint.
This walk-through illustrates how the solution methodically progresses through tasks, respecting the space
constraint by using a dictionary to track availability and appropriately fast-forwarding the current day when needed.
Solution Implementation
1from collections import defaultdict
2
3class Solution:
4 def taskSchedulerII(self, tasks: List[int], space: int) -> int:
5 # Dictionary to store the next eligible day on which a task can be performed
6 next_eligible_day = defaultdict(int)
7 # Initialize the current day counter
8 current_day = 0
9
10 # Loop through the given list of tasks
11 for task in tasks:
12 # Increment the day counter as we're performing a task each day
13 current_day += 1
14 # If the task has been performed before, make sure to respect the waiting period
15 # Update the current day to the maximum of itself and the next eligible day for the task
16 current_day = max(current_day, next_eligible_day[task])
17 # Update the next eligible day for the current task by adding the space interval
18 next_eligible_day[task] = current_day + space + 1
19
20 # After processing all tasks, return the last day count as the total days required
21 return current_day
22```
23**Note:** Before running this code, you will need to replace `List[int]` with the appropriate import from the `typing` module, like this:
24
25```python
26from typing import List
27
1class Solution {
2 public long taskSchedulerII(int[] tasks, int space) {
3 // Map to store the next valid day a task can be scheduled
4 Map<Integer, Long> nextValidDay = new HashMap<>();
5 long currentDay = 0; // Represents the current day
6
7 // Iterate through all tasks
8 for (int task : tasks) {
9 currentDay++; // Move to the next day
10 // Check if we need to wait for a cooldown for the current task, and if necessary, jump to the next valid day
11 currentDay = Math.max(currentDay, nextValidDay.getOrDefault(task, 0L));
12 // Calculate and store the next valid day the current task can be executed based on the space (cooldown period)
13 nextValidDay.put(task, currentDay + space + 1);
14 }
15
16 // The last value of currentDay will be the total time taken to complete all tasks
17 return currentDay;
18 }
19}
20
1#include <vector>
2#include <unordered_map>
3
4class Solution {
5public:
6 // Function to determine the minimum number of days required to complete all tasks.
7 // Each identical task needs to be executed with at least a 'space' day gap between them.
8 long long taskSchedulerII(vector<int>& tasks, int space) {
9 unordered_map<int, long long> nextAvailableDay; // Maps a task to the next day it can be executed.
10 long long currentDay = 0; // Tracks the current day.
11
12 // Iterate through each task in tasks vector.
13 for (int task : tasks) {
14 ++currentDay; // Increment the current day by one.
15
16 // Check if we need to wait for a task based on its space requirement.
17 // If so, move the current day to the next available day for that task.
18 if (nextAvailableDay.find(task) != nextAvailableDay.end()) {
19 currentDay = max(currentDay, nextAvailableDay[task]);
20 }
21
22 // Update the next available day for the current task to be space days out.
23 nextAvailableDay[task] = currentDay + space + 1;
24 }
25
26 // The result is the last day we finish a task.
27 return currentDay;
28 }
29};
30
1// Function to calculate the number of days needed to complete given tasks
2// with a cooldown period between tasks of the same type.
3function taskSchedulerII(tasks: number[], space: number): number {
4 // Map to keep track of the next available day for each task to be scheduled
5 const nextAvailableDay = new Map<number, number>();
6 // Variable to keep track of the current day
7 let currentDay = 0;
8
9 // Loop through each task in the given tasks array
10 for (const task of tasks) {
11 // Move to the next day
12 currentDay++;
13
14 // Check if there's a previously scheduled day for the current task and
15 // update the current day if necessary, to respect the cooling period
16 currentDay = Math.max(currentDay, nextAvailableDay.get(task) ?? 0);
17
18 // Set the next available day for this task to current day plus space days
19 nextAvailableDay.set(task, currentDay + space + 1);
20 }
21
22 // Return the total number of days needed to complete the tasks
23 return currentDay;
24}
25
Time and Space Complexity
The given Python code defines a function taskSchedulerII
which takes a list of tasks and a cooldown period (space
) and returns the total number of days needed to complete all tasks following the cooldown restriction for each task.
Time Complexity
The time complexity of the code is O(n)
, where n
is the number of tasks. This is because there is a single for-loop iterating over each task exactly once. Inside the for-loop, dictionary operations (access and assignment) are utilized, which run in average-case O(1)
time. Hence, the loop's operations do not depend on the size of the input beyond the iteration over the tasks, resulting in a linear time complexity.
The key line to note for the time complexity inside the loop is:
ans = max(ans, day[task])
This is a constant-time operation and does not change the overall linear time complexity of the algorithm.
Space Complexity
The space complexity of the code is O(m)
, where m
is the number of unique tasks. A dictionary named day
is used to store the next available day for each unique task after respecting the cooldown period. In the worst case, if all tasks are unique, the dictionary will have an entry for each one, resulting in space complexity proportional to the number of unique tasks.
The key part of the code for analyzing space complexity is the dictionary:
day = defaultdict(int)
The day
dictionary only ever stores at the most one key-value pair per unique task, giving us the O(m)
space complexity.
Conclusion
The function is efficient in terms of time complexity, operating in linear time relative to the number of tasks. It is also relatively space efficient, although the space needed can grow with the number of unique tasks.
Learn more about how to find time and space complexity quickly using problem constraints.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
LeetCode 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 we
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
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!