1665. Minimum Initial Energy to Finish Tasks
Problem Description
You have an array of tasks where each task is represented as [actual, minimum]
:
actual
is the energy consumed when completing the taskminimum
is the minimum energy level required to start the task
To complete a task, your current energy must be at least equal to the minimum
value. After completing the task, your energy decreases by the actual
amount.
For example, with task [10, 12]
:
- If your current energy is 11, you cannot start the task (11 < 12)
- If your current energy is 13, you can start the task, and after completion you'll have 13 - 10 = 3 energy remaining
You can complete the tasks in any order you choose. The goal is to find the minimum initial energy needed to complete all tasks.
The solution uses a greedy approach with custom sorting. The key insight is that to minimize the initial energy, we should complete tasks in a specific order based on the difference actual - minimum
. By sorting tasks in ascending order of actual[i] - minimum[i]
, we ensure that tasks requiring more "buffer" energy (where minimum
is much larger than actual
) are done later when we have more flexibility.
The algorithm tracks the current energy level and whenever it falls below a task's minimum requirement, it adds just enough energy to meet that requirement. This ensures we use the absolute minimum initial energy needed to complete all tasks in the optimal order.
Intuition
Let's think about what happens when we complete tasks in different orders. The key observation is that the total energy consumed (sum of all actual
values) is fixed regardless of order, but the initial energy requirement changes based on the sequence.
Consider two adjacent tasks in our sequence. If we have tasks A and B, should we do A first or B first? Let's analyze what initial energy we need for each ordering:
If we do A then B:
- We need at least
minimum_A
energy to start - After A, we have energy reduced by
actual_A
- To do B next, we need the remaining energy to be at least
minimum_B
- So initial energy must be at least
max(minimum_A, actual_A + minimum_B)
If we do B then A:
- Initial energy must be at least
max(minimum_B, actual_B + minimum_A)
To minimize initial energy, we want to choose the ordering that gives us the smaller requirement. Through mathematical analysis, we find that we should do A before B when actual_A - minimum_A < actual_B - minimum_B
.
This leads to the insight: tasks with smaller actual - minimum
difference should be done first. Why? Because these tasks give us less "energy buffer" after completion. Tasks where minimum
is much larger than actual
are like "energy gates" - they require a lot to start but don't consume much. It's better to save these for last when we've already accumulated enough energy.
By sorting tasks by actual - minimum
in ascending order, we ensure we're always making the optimal local choice, which leads to the global minimum initial energy. As we traverse the sorted tasks, we only add energy when absolutely necessary (when current energy falls below a task's minimum), ensuring we use the least possible initial energy.
Solution Approach
The implementation follows the greedy strategy with custom sorting that we identified from our intuition.
Step 1: Sort the tasks
We sort the tasks based on actual - minimum
in ascending order using a lambda function:
sorted(tasks, key=lambda x: x[0] - x[1])
This ensures tasks with smaller difference (less energy buffer) are processed first.
Step 2: Initialize variables
ans
: tracks the minimum initial energy required (this is what we'll return)cur
: tracks the current energy level as we process tasks
Step 3: Process tasks in sorted order
For each task [a, m]
in the sorted list:
-
Check if we have enough energy to start: If
cur < m
, we don't have enough energy to begin this task. We need to add energy to our initial amount:if cur < m: ans += m - cur # Add just enough to meet the minimum cur = m # Update current energy to the minimum required
-
Complete the task: After ensuring we have enough energy to start, we complete the task and reduce our current energy by the actual amount consumed:
cur -= a
Why this works:
As proven in the reference approach, for any sequence of tasks, the minimum initial energy needed is:
Where the last task determines the additional buffer needed beyond the total energy consumed. By sorting tasks to minimize m_n - a_n
(equivalently, maximize a_n - m_n
), we minimize the required initial energy.
The algorithm cleverly tracks this by only adding energy when absolutely necessary, ensuring the final ans
represents the minimum initial energy needed to complete all tasks in the optimal order.
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 tasks: [[1, 3], [2, 4], [10, 11], [10, 12], [8, 9]]
Step 1: Sort by (actual - minimum)
Calculate the differences:
- Task
[1, 3]
: difference = 1 - 3 = -2 - Task
[2, 4]
: difference = 2 - 4 = -2 - Task
[10, 11]
: difference = 10 - 11 = -1 - Task
[10, 12]
: difference = 10 - 12 = -2 - Task
[8, 9]
: difference = 8 - 9 = -1
After sorting in ascending order: [[1, 3], [2, 4], [10, 12], [10, 11], [8, 9]]
Step 2: Initialize tracking variables
ans = 0
(minimum initial energy needed)cur = 0
(current energy level)
Step 3: Process each task in sorted order
Task 1: [1, 3]
- Current energy: 0, Required minimum: 3
- Since 0 < 3, we need more energy
- Add to initial:
ans = 0 + (3 - 0) = 3
- Update current:
cur = 3
- Complete task:
cur = 3 - 1 = 2
Task 2: [2, 4]
- Current energy: 2, Required minimum: 4
- Since 2 < 4, we need more energy
- Add to initial:
ans = 3 + (4 - 2) = 5
- Update current:
cur = 4
- Complete task:
cur = 4 - 2 = 2
Task 3: [10, 12]
- Current energy: 2, Required minimum: 12
- Since 2 < 12, we need more energy
- Add to initial:
ans = 5 + (12 - 2) = 15
- Update current:
cur = 12
- Complete task:
cur = 12 - 10 = 2
Task 4: [10, 11]
- Current energy: 2, Required minimum: 11
- Since 2 < 11, we need more energy
- Add to initial:
ans = 15 + (11 - 2) = 24
- Update current:
cur = 11
- Complete task:
cur = 11 - 10 = 1
Task 5: [8, 9]
- Current energy: 1, Required minimum: 9
- Since 1 < 9, we need more energy
- Add to initial:
ans = 24 + (9 - 1) = 32
- Update current:
cur = 9
- Complete task:
cur = 9 - 8 = 1
Result: The minimum initial energy needed is 32.
This example demonstrates how the algorithm greedily adds just enough energy whenever needed, ensuring we find the absolute minimum initial energy required to complete all tasks in the optimal order.
Solution Implementation
1class Solution:
2 def minimumEffort(self, tasks: List[List[int]]) -> int:
3 # Sort tasks by the difference (actual - minimum) in ascending order
4 # This ensures we handle tasks with larger "extra" requirements first
5 sorted_tasks = sorted(tasks, key=lambda task: task[0] - task[1])
6
7 total_initial_effort = 0 # Total initial effort needed
8 current_energy = 0 # Current available energy
9
10 # Process each task in the sorted order
11 for actual_effort, minimum_effort in sorted_tasks:
12 # If current energy is less than minimum required to start this task
13 if current_energy < minimum_effort:
14 # Add the deficit to our total initial effort
15 effort_deficit = minimum_effort - current_energy
16 total_initial_effort += effort_deficit
17 # Update current energy to the minimum required
18 current_energy = minimum_effort
19
20 # Consume the actual effort for this task
21 current_energy -= actual_effort
22
23 return total_initial_effort
24
1class Solution {
2 public int minimumEffort(int[][] tasks) {
3 // Sort tasks by the difference between actual effort and minimum required effort
4 // This ensures we complete tasks with smaller "extra effort needed" first
5 // Sorting formula: actualEffort[i] - (minRequired[i] - actualEffort[i]) = 2*actualEffort[i] - minRequired[i]
6 Arrays.sort(tasks, (task1, task2) ->
7 (task1[0] - task1[1]) - (task2[0] - task2[1])
8 );
9
10 int totalMinimumEffort = 0; // The minimum initial effort needed
11 int currentEffort = 0; // Current available effort after completing tasks
12
13 // Process each task in sorted order
14 for (int[] task : tasks) {
15 int actualEffortNeeded = task[0]; // Effort consumed to complete the task
16 int minimumEffortRequired = task[1]; // Minimum effort required to start the task
17
18 // If current effort is less than minimum required, we need to add more initial effort
19 if (currentEffort < minimumEffortRequired) {
20 int additionalEffortNeeded = minimumEffortRequired - currentEffort;
21 totalMinimumEffort += additionalEffortNeeded;
22 currentEffort = minimumEffortRequired;
23 }
24
25 // Consume the actual effort to complete the task
26 currentEffort -= actualEffortNeeded;
27 }
28
29 return totalMinimumEffort;
30 }
31}
32
1class Solution {
2public:
3 int minimumEffort(vector<vector<int>>& tasks) {
4 // Sort tasks by the difference between actual effort and minimum effort (ascending)
5 // This ensures we do tasks with smaller "effort buffer" first
6 sort(tasks.begin(), tasks.end(), [](const auto& taskA, const auto& taskB) {
7 return taskA[0] - taskA[1] < taskB[0] - taskB[1];
8 });
9
10 int totalEffortNeeded = 0; // Total initial effort required
11 int currentEffort = 0; // Current available effort
12
13 // Process each task in sorted order
14 for (auto& task : tasks) {
15 int actualEffort = task[0]; // Effort consumed by this task
16 int minimumEffort = task[1]; // Minimum effort required to start this task
17
18 // If current effort is less than minimum required, add the difference
19 if (currentEffort < minimumEffort) {
20 totalEffortNeeded += minimumEffort - currentEffort;
21 currentEffort = minimumEffort;
22 }
23
24 // Consume the actual effort for this task
25 currentEffort -= actualEffort;
26 }
27
28 return totalEffortNeeded;
29 }
30};
31
1/**
2 * Calculates the minimum initial effort required to complete all tasks
3 * @param tasks - Array of tasks where each task is [actualEffort, minimumEffort]
4 * @returns The minimum initial effort needed
5 */
6function minimumEffort(tasks: number[][]): number {
7 // Sort tasks by the difference between actual effort and minimum effort (ascending)
8 // This ensures we tackle tasks with smaller effort gaps first
9 tasks.sort((taskA: number[], taskB: number[]) => {
10 const gapA: number = taskA[0] - taskA[1];
11 const gapB: number = taskB[0] - taskB[1];
12 return gapA - gapB;
13 });
14
15 // Track the total effort we need to add
16 let totalEffortNeeded: number = 0;
17 // Track our current available effort
18 let currentEffort: number = 0;
19
20 // Process each task in sorted order
21 for (const task of tasks) {
22 const [actualEffortRequired, minimumEffortRequired]: [number, number] = [task[0], task[1]];
23
24 // If current effort is less than minimum required, add the difference
25 if (currentEffort < minimumEffortRequired) {
26 const effortToAdd: number = minimumEffortRequired - currentEffort;
27 totalEffortNeeded += effortToAdd;
28 currentEffort = minimumEffortRequired;
29 }
30
31 // Consume the actual effort for this task
32 currentEffort -= actualEffortRequired;
33 }
34
35 return totalEffortNeeded;
36}
37
Time and Space Complexity
The time complexity is O(n ร log n)
, where n
is the number of tasks. This is dominated by the sorting operation sorted(tasks, key=lambda x: x[0] - x[1])
, which uses a comparison-based sorting algorithm (typically Timsort in Python) that has O(n ร log n)
time complexity. The subsequent for loop iterates through all tasks once, contributing O(n)
time, but this is absorbed by the larger sorting complexity.
The space complexity is O(n)
. While the reference answer mentions O(1)
when ignoring the space overhead of sorting, the sorted()
function in Python creates a new sorted list rather than sorting in-place, which requires O(n)
additional space to store the sorted copy of the tasks list. If we were to ignore this sorting overhead as suggested in the reference, then the remaining operations use only a constant amount of extra space (ans
, cur
, and loop variables), resulting in O(1)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Sorting Logic - Sorting by Wrong Criteria
The Pitfall: A common mistake is sorting by the wrong criteria, such as:
- Sorting by
minimum
alone (ascending or descending) - Sorting by
actual
alone - Sorting by
minimum - actual
instead ofactual - minimum
Why it fails:
Consider tasks [[10, 10], [1, 20]]
:
- If sorted by minimum ascending:
[[10, 10], [1, 20]]
โ requires 30 initial energy - If sorted by
actual - minimum
:[[10, 10], [1, 20]]
โ requires 21 initial energy (optimal)
Sorting by the wrong criteria leads to suboptimal task ordering and higher initial energy requirements.
Solution:
Always sort by actual - minimum
in ascending order (or equivalently, minimum - actual
in descending order):
sorted_tasks = sorted(tasks, key=lambda task: task[0] - task[1])
2. Not Understanding the Energy Update Logic
The Pitfall: Incorrectly updating the current energy when insufficient, such as:
# Wrong approach 1: Adding the minimum required instead of the deficit
if cur < m:
ans += m # Wrong! This adds the full minimum, not the deficit
cur = m
# Wrong approach 2: Setting ans to maximum seen so far
if cur < m:
ans = max(ans, m) # Wrong! This doesn't accumulate properly
Why it fails: The algorithm needs to track the cumulative energy additions, not individual minimums. Each time we're short on energy, we need to add exactly the deficit to our initial energy pool.
Solution: Add only the deficit (shortage) to the initial energy:
if cur < m: ans += m - cur # Add only what's missing cur = m # Update current to minimum required
3. Confusing Initial Energy vs Current Energy
The Pitfall:
Mixing up what ans
(initial energy) and cur
(current energy) represent:
# Wrong: Thinking ans represents current energy at any point for a, m in sorted_tasks: if ans < m: # Wrong! Should check 'cur' not 'ans' ans += m - ans
Why it fails:
ans
accumulates the total initial energy we need to start withcur
tracks our energy level as we progress through tasks- Checking
ans < m
doesn't tell us if we can currently start a task
Solution: Keep the roles clear:
- Use
cur
to track current energy state - Use
ans
only to accumulate the initial energy requirement - Always check
cur < minimum
to determine if we can start a task
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Donโt Miss This!