2589. Minimum Time to Complete All Tasks
Problem Description
You have a computer that can run multiple tasks simultaneously. Given a 2D array tasks
where each tasks[i] = [start_i, end_i, duration_i]
represents:
start_i
: the earliest time the task can startend_i
: the latest time the task must finishduration_i
: the total seconds needed to complete the task
Each task must run for exactly duration_i
seconds within its time window [start_i, end_i]
(inclusive). The task doesn't need to run continuously - it can be split across multiple time intervals.
The computer can be turned on and off as needed. When it's on, it can run any number of tasks simultaneously. Your goal is to find the minimum total time the computer needs to be turned on to complete all tasks.
For example, if you have two tasks:
- Task 1:
[1, 3, 2]
- needs 2 seconds between time 1 and 3 - Task 2:
[2, 5, 3]
- needs 3 seconds between time 2 and 5
One optimal solution would be to turn on the computer at times 2 and 3 (2 seconds total) for task 1, and at times 2, 3, and 5 (reusing times 2 and 3) for task 2. The total time the computer is on would be 3 seconds (times 2, 3, and 5).
The key insight is that multiple tasks can share the same time slots when the computer is on, so you want to maximize the overlap between tasks to minimize the total running time.
Intuition
The key observation is that when multiple tasks have overlapping time windows, they can share the same time slots when the computer is on. This means we want to maximize the reuse of time slots across different tasks.
Consider which time slots we should prioritize for each task. If we have a task that needs to run for duration
seconds within [start, end]
, which specific seconds should we choose?
The greedy insight is to choose time slots as late as possible within each task's window. Why? Because later time slots are more likely to overlap with future tasks' time windows. If we sort tasks by their end times and process them in that order, choosing the latest possible slots for each task increases the chances that subsequent tasks (which have equal or later end times) can reuse those same slots.
For example, if Task A runs from [1, 5]
and needs 2 seconds, and Task B runs from [3, 7]
and needs 2 seconds:
- If we run Task A at times 1 and 2, Task B would need new slots at times 6 and 7 (total: 4 seconds)
- If we run Task A at times 4 and 5, Task B can reuse those same slots (total: 2 seconds)
This leads to our strategy:
- Sort tasks by end time to ensure we process tasks with earlier deadlines first
- For each task, select the required time slots starting from the end of its window and working backwards
- Mark selected time slots as "used" so subsequent tasks can reuse them
- Count only the newly selected time slots toward our total
By always selecting the latest possible slots and processing tasks in order of end time, we create maximum opportunity for slot reuse, thus minimizing the total time the computer needs to be on.
Learn more about Stack, Greedy, Binary Search and Sorting patterns.
Solution Approach
The implementation follows the greedy strategy we identified:
Step 1: Sort tasks by end time
tasks.sort(key=lambda x: x[1])
We sort all tasks in ascending order of their end times. This ensures we process tasks with earlier deadlines first, allowing later tasks to potentially reuse the time slots we select.
Step 2: Track selected time slots
vis = [0] * 2010
We create a boolean array vis
where vis[i] = 1
indicates that time slot i
has been selected (computer is on at time i
). The size 2010 covers all possible time values based on the problem constraints.
Step 3: Process each task greedily
For each task [start, end, duration]
:
First, we check how many time slots in the task's window have already been selected:
duration -= sum(vis[start : end + 1])
This reduces the duration
by the number of already-selected slots that this task can reuse.
Then, we select additional time slots if needed, starting from the end and working backwards:
i = end while i >= start and duration > 0: if not vis[i]: duration -= 1 vis[i] = 1 ans += 1 i -= 1
The algorithm iterates from end
down to start
:
- If time slot
i
is not yet selected (vis[i] == 0
), we select it - Mark
vis[i] = 1
to indicate this slot is now used - Increment our answer counter
ans
(total time computer is on) - Decrease
duration
as we've satisfied one more second of this task's requirement - Continue until we've selected enough slots (
duration == 0
)
Step 4: Return the result
The variable ans
tracks the total number of unique time slots selected across all tasks, which represents the minimum time the computer needs to be on.
The time complexity is O(n log n + n * m)
where n
is the number of tasks and m
is the maximum time range. The space complexity is O(m)
for the vis
array.
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 a concrete example with 3 tasks:
- Task A:
[1, 4, 2]
- needs 2 seconds between time 1 and 4 - Task B:
[2, 6, 3]
- needs 3 seconds between time 2 and 6 - Task C:
[5, 7, 1]
- needs 1 second between time 5 and 7
Step 1: Sort by end time
After sorting: [[1,4,2], [2,6,3], [5,7,1]]
(already sorted in this example)
Step 2: Initialize tracking array
vis = [0,0,0,0,0,0,0,0,...]
(all zeros initially)
Step 3: Process each task
Processing Task A [1,4,2]
:
- No slots used yet in range [1,4], so duration remains 2
- Select slots from end: choose times 4 and 3
vis = [0,0,0,1,1,0,0,0,...]
- Computer on time += 2 (total: 2)
Processing Task B [2,6,3]
:
- Check existing slots in [2,6]: times 3 and 4 are already selected
- Remaining duration = 3 - 2 = 1
- Select 1 more slot from end: choose time 6
vis = [0,0,0,1,1,0,1,0,...]
- Computer on time += 1 (total: 3)
Processing Task C [5,7,1]
:
- Check existing slots in [5,7]: time 6 is already selected
- Remaining duration = 1 - 1 = 0
- No new slots needed! Task C can reuse time 6
vis = [0,0,0,1,1,0,1,0,...]
- Computer on time += 0 (total: 3)
Result: The computer needs to be on for 3 seconds total (at times 3, 4, and 6).
This demonstrates the key insight: by selecting slots as late as possible (starting from the end of each task's window), we maximized reuse. Task B reused times 3 and 4 from Task A, and Task C completely reused time 6 from Task B, minimizing the total running time.
Solution Implementation
1class Solution:
2 def findMinimumTime(self, tasks: List[List[int]]) -> int:
3 # Sort tasks by their end time to process them in optimal order
4 # This ensures we can greedily assign time slots from the end
5 tasks.sort(key=lambda task: task[1])
6
7 # Track which time slots have been used (0 = unused, 1 = used)
8 # Size 2010 assumes end times won't exceed 2009
9 time_slot_used = [0] * 2010
10
11 # Total number of time units needed
12 total_time = 0
13
14 # Process each task
15 for start_time, end_time, required_duration in tasks:
16 # Calculate how many time slots within this task's window are already used
17 already_used_slots = sum(time_slot_used[start_time : end_time + 1])
18
19 # Reduce the required duration by the already used slots
20 remaining_duration = required_duration - already_used_slots
21
22 # Greedily assign time slots from the end of the task's window
23 # This maximizes potential sharing with future tasks
24 current_time = end_time
25 while current_time >= start_time and remaining_duration > 0:
26 # If this time slot hasn't been used yet
27 if not time_slot_used[current_time]:
28 # Mark it as used
29 time_slot_used[current_time] = 1
30 # Decrease remaining duration needed
31 remaining_duration -= 1
32 # Increment total time used
33 total_time += 1
34 # Move to earlier time slot
35 current_time -= 1
36
37 return total_time
38
1class Solution {
2 public int findMinimumTime(int[][] tasks) {
3 // Sort tasks by their end time in ascending order
4 // This greedy approach ensures we process tasks that end earlier first
5 Arrays.sort(tasks, (a, b) -> a[1] - b[1]);
6
7 // Track which time slots have been used (1 if used, 0 if not)
8 // Size 2010 covers the constraint range
9 int[] timeSlotUsed = new int[2010];
10
11 // Total minimum time needed to complete all tasks
12 int totalTime = 0;
13
14 // Process each task in sorted order
15 for (int[] task : tasks) {
16 int startTime = task[0];
17 int endTime = task[1];
18 int requiredDuration = task[2];
19
20 // Calculate how much of the required duration is already covered
21 // by previously scheduled time slots
22 for (int time = startTime; time <= endTime; time++) {
23 requiredDuration -= timeSlotUsed[time];
24 }
25
26 // Schedule remaining duration from the end time backwards
27 // This greedy choice maximizes overlap with future tasks
28 for (int time = endTime; time >= startTime && requiredDuration > 0; time--) {
29 // If this time slot is not yet used
30 if (timeSlotUsed[time] == 0) {
31 // Mark this time slot as used
32 timeSlotUsed[time] = 1;
33 requiredDuration--;
34 totalTime++;
35 }
36 }
37 }
38
39 return totalTime;
40 }
41}
42
1class Solution {
2public:
3 int findMinimumTime(vector<vector<int>>& tasks) {
4 // Sort tasks by their end time in ascending order
5 // This greedy approach ensures we process tasks that end earlier first
6 sort(tasks.begin(), tasks.end(), [&](auto& a, auto& b) {
7 return a[1] < b[1];
8 });
9
10 // Bitset to track which time slots have been used
11 // vis[i] = 1 means time slot i is already occupied
12 bitset<2010> timeSlotUsed;
13
14 // Total number of time units needed
15 int totalTime = 0;
16
17 // Process each task
18 for (auto& task : tasks) {
19 int startTime = task[0];
20 int endTime = task[1];
21 int requiredDuration = task[2];
22
23 // Count how many time slots within [startTime, endTime] are already used
24 // Subtract them from required duration since we can reuse those slots
25 for (int i = startTime; i <= endTime; ++i) {
26 requiredDuration -= timeSlotUsed[i];
27 }
28
29 // Greedily allocate remaining required time slots from end to start
30 // This strategy maximizes the chance of reusing slots for future tasks
31 for (int i = endTime; i >= startTime && requiredDuration > 0; --i) {
32 if (!timeSlotUsed[i]) {
33 // Mark this time slot as used
34 timeSlotUsed[i] = 1;
35 // Decrease remaining duration needed
36 --requiredDuration;
37 // Increment total time used
38 ++totalTime;
39 }
40 }
41 }
42
43 return totalTime;
44 }
45};
46
1/**
2 * Finds the minimum time required to complete all tasks
3 * Tasks can be executed in parallel if their time intervals overlap
4 * @param tasks - Array of tasks where each task is [start, end, duration]
5 * @returns The minimum total time needed to complete all tasks
6 */
7function findMinimumTime(tasks: number[][]): number {
8 // Sort tasks by their end time in ascending order
9 // This ensures we process tasks that end earlier first
10 tasks.sort((a: number[], b: number[]) => a[1] - b[1]);
11
12 // Array to track which time slots have been used (1 = used, 0 = unused)
13 // Size 2010 covers the constraint range
14 const timeSlotUsed: number[] = Array(2010).fill(0);
15
16 // Total minimum time required
17 let totalTime: number = 0;
18
19 // Process each task
20 for (const [startTime, endTime, requiredDuration] of tasks) {
21 // Calculate remaining duration needed by checking already used time slots
22 let remainingDuration: number = requiredDuration;
23 for (let timeSlot: number = startTime; timeSlot <= endTime; timeSlot++) {
24 remainingDuration -= timeSlotUsed[timeSlot];
25 }
26
27 // Greedily assign unused time slots from the end of the interval
28 // Starting from the end ensures optimal placement for future tasks
29 for (let timeSlot: number = endTime; timeSlot >= startTime && remainingDuration > 0; timeSlot--) {
30 if (timeSlotUsed[timeSlot] === 0) {
31 // Mark this time slot as used
32 timeSlotUsed[timeSlot] = 1;
33 remainingDuration--;
34 totalTime++;
35 }
36 }
37 }
38
39 return totalTime;
40}
41
Time and Space Complexity
Time Complexity: O(n × log n + n × m)
The time complexity consists of two main parts:
O(n × log n)
for sorting the tasks array, wheren
is the number of tasksO(n × m)
for processing all tasks, where:- For each of the
n
tasks, we perform:sum(vis[start : end + 1])
which takesO(m)
time in the worst case (whenend - start
can be up tom
)- A while loop that iterates from
end
tostart
, which is alsoO(m)
in the worst case
- Therefore, processing all tasks takes
O(n × m)
time
- For each of the
The overall time complexity is O(n × log n + n × m)
, where m = 2010
(the size of the vis
array).
Space Complexity: O(m)
The space complexity is determined by:
- The
vis
array of size2010
, which takesO(m)
space wherem = 2010
- The sorting operation uses
O(log n)
space for the recursive call stack in Python's Timsort - Since
m
is a constant (2010) and typically larger thanlog n
, the dominant space complexity isO(m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Array Slicing for Already-Used Slots Calculation
The Pitfall:
The line sum(time_slot_used[start_time : end_time + 1])
creates a new list slice every time, which can be inefficient. More critically, developers often make off-by-one errors with Python slicing, forgetting that the end index is exclusive. While the code correctly uses end_time + 1
, it's easy to mistakenly write sum(time_slot_used[start_time : end_time])
which would miss the last time slot.
Solution: Use an explicit loop to make the logic clearer and avoid slicing errors:
already_used_slots = 0
for t in range(start_time, end_time + 1):
if time_slot_used[t]:
already_used_slots += 1
2. Fixed Array Size Assumption
The Pitfall:
The code uses time_slot_used = [0] * 2010
with a hardcoded size. If the problem constraints change or if end times exceed 2009, this will cause an IndexError. This is a brittle assumption that makes the code less maintainable.
Solution: Calculate the maximum end time dynamically:
max_time = max(task[1] for task in tasks) if tasks else 0
time_slot_used = [0] * (max_time + 1)
3. Not Validating Task Duration Against Window Size
The Pitfall:
The code assumes that duration <= (end_time - start_time + 1)
for all tasks. If a task has a duration longer than its available window, the greedy algorithm will fail silently by not allocating enough time slots, leading to incorrect results.
Solution: Add validation before processing:
for start_time, end_time, required_duration in tasks: window_size = end_time - start_time + 1 if required_duration > window_size: # Handle impossible task - could return -1 or raise exception return -1 # Task cannot be completed within its window # ... rest of the processing
4. Inefficient Time Slot Selection for Large Time Ranges
The Pitfall:
When iterating backwards from end_time
to start_time
, if the time range is large and most slots are already used, the algorithm wastes time checking many already-used slots.
Solution: Maintain a set or list of available time slots within each window:
# Collect available slots first
available_slots = []
for t in range(start_time, end_time + 1):
if not time_slot_used[t]:
available_slots.append(t)
# Select from the end of available slots
slots_to_use = min(remaining_duration, len(available_slots))
for i in range(len(available_slots) - slots_to_use, len(available_slots)):
time_slot_used[available_slots[i]] = 1
total_time += 1
5. Not Handling Edge Cases
The Pitfall: The code doesn't explicitly handle edge cases like empty task lists, tasks with zero duration, or negative time values.
Solution: Add edge case handling at the beginning:
if not tasks: return 0 # Filter out invalid tasks valid_tasks = [task for task in tasks if task[2] > 0 and task[0] <= task[1] and task[0] >= 0]
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Want a Structured Path to Master System Design Too? Don’t Miss This!