LeetCode 1665 - Minimum Initial Energy to Finish Tasks
The problem gives us a list of tasks, where each task is represented as [actual, minimum]. For every task: - minimum is the amount of energy we must currently have before we are allowed to start the task.
Difficulty: 🔴 Hard
Topics: Array, Greedy, Sorting
Solution
Problem Understanding
The problem gives us a list of tasks, where each task is represented as [actual, minimum].
For every task:
minimumis the amount of energy we must currently have before we are allowed to start the task.actualis the amount of energy that gets consumed after finishing the task.
Suppose a task is [4, 10]. We need at least 10 energy to begin it. After completing it, our energy decreases by 4.
The important detail is that tasks can be completed in any order. Our goal is to determine the smallest possible initial energy such that there exists some ordering of tasks that allows us to complete all of them.
This is fundamentally an ordering optimization problem. Different task orders require different starting energy levels. We want the order that minimizes the required initial energy.
The constraints are large:
- Up to
10^5tasks - Each value can be up to
10^4
Because of this, exponential or factorial solutions are impossible. We need something around O(n log n).
Another important observation is that every task satisfies:
actual <= minimum
This guarantees that if we can start a task, we will never immediately go negative after finishing it.
Several edge cases are important:
-
Tasks where
actual == minimum -
These are strict tasks because they leave no safety margin.
-
Tasks with very large gaps between
minimumandactual -
These tasks are difficult to start but relatively cheap to execute.
-
Single task inputs
-
The answer is simply the task's
minimum. -
Tasks with identical values
-
Sorting logic must still behave correctly.
Approaches
Brute Force Approach
The brute force solution tries every possible ordering of tasks.
For each permutation:
- Simulate completing tasks in that order.
- Compute the minimum initial energy needed for that ordering.
- Take the minimum across all permutations.
To compute the required initial energy for one ordering, we process tasks sequentially and ensure that before each task our energy is at least its minimum.
This approach is correct because it explores every possible ordering, so it cannot miss the optimal one.
However, it is far too slow.
If there are n tasks, there are:
n!
possible permutations.
Even for n = 15, this becomes infeasible. Since the real constraint is 10^5, brute force is completely impossible.
Key Insight for the Optimal Solution
The core challenge is determining the best order.
Consider two tasks:
A = [a1, m1]
B = [a2, m2]
Suppose we perform A before B.
To start A, we need at least m1.
After finishing A, our energy decreases by a1, so before starting B we need:
initial_energy - a1 >= m2
which means:
initial_energy >= a1 + m2
Therefore, if A comes before B, required energy is:
max(m1, a1 + m2)
Similarly, if B comes before A, required energy is:
max(m2, a2 + m1)
We should choose the order that produces the smaller requirement.
After simplifying the comparison, we obtain the greedy rule:
Sort tasks by:
(minimum - actual) descending
Why does this work?
The quantity:
minimum - actual
represents how demanding a task is relative to its cost.
-
Large difference:
-
Hard to start
-
Cheap to finish
-
Small difference:
-
Easier to start relative to energy spent
Tasks with larger gaps should be done earlier while energy is still high.
Once tasks are sorted this way, we can greedily simulate execution and compute the minimum required initial energy.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n! · n) | O(n) | Tries every possible ordering |
| Optimal | O(n log n) | O(1) or O(log n) | Greedy sorting by (minimum - actual) |
Algorithm Walkthrough
Step 1: Sort the Tasks
Sort all tasks in descending order of:
minimum - actual
This prioritizes tasks that require a large starting buffer.
For example:
[1,7] -> 6
[5,11] -> 6
[2,4] -> 2
Tasks with larger values come first.
Step 2: Initialize Tracking Variables
We maintain:
-
current_energy -
Simulated remaining energy
-
required_energy -
Minimum initial energy needed so far
Initially both are 0.
Step 3: Process Tasks in Sorted Order
For every task [actual, minimum]:
If current energy is less than minimum, we must increase our initial energy.
The extra amount needed is:
minimum - current_energy
We add this amount to both:
required_energycurrent_energy
Now we can safely perform the task.
Step 4: Spend the Task Energy
After completing the task:
current_energy -= actual
Continue until all tasks are processed.
Step 5: Return the Result
At the end:
required_energy
is the smallest initial energy that makes the entire schedule feasible.
Why it works
The greedy ordering ensures that tasks with the largest startup overhead are handled first, before energy has been depleted by previous tasks.
The exchange argument proves correctness. If two adjacent tasks are not ordered by descending (minimum - actual), swapping them cannot increase the required initial energy. Repeatedly applying this swap rule transforms any optimal ordering into the greedy ordering, proving that the greedy arrangement is optimal.
Python Solution
from typing import List
class Solution:
def minimumEffort(self, tasks: List[List[int]]) -> int:
tasks.sort(key=lambda task: task[1] - task[0], reverse=True)
required_energy = 0
current_energy = 0
for actual, minimum in tasks:
if current_energy < minimum:
extra = minimum - current_energy
required_energy += extra
current_energy += extra
current_energy -= actual
return required_energy
The implementation begins by sorting tasks according to the greedy rule. The lambda function computes:
task[1] - task[0]
which corresponds to:
minimum - actual
in descending order.
The variable required_energy stores the total initial energy we have had to add so far. The variable current_energy simulates how much energy remains during execution.
For every task, we first check whether we have enough energy to begin it. If not, we increase both variables by exactly the missing amount.
After that, we subtract the task's actual energy cost.
Because we only ever add the minimum extra energy necessary, the final result is optimal.
Go Solution
package main
import "sort"
func minimumEffort(tasks [][]int) int {
sort.Slice(tasks, func(i, j int) bool {
return (tasks[i][1] - tasks[i][0]) > (tasks[j][1] - tasks[j][0])
})
requiredEnergy := 0
currentEnergy := 0
for _, task := range tasks {
actual := task[0]
minimum := task[1]
if currentEnergy < minimum {
extra := minimum - currentEnergy
requiredEnergy += extra
currentEnergy += extra
}
currentEnergy -= actual
}
return requiredEnergy
}
The Go implementation follows the same logic as the Python version.
The main difference is sorting syntax. Go uses sort.Slice with a custom comparator.
Go integers are sufficient because the maximum possible answer is well within 32 bit integer range:
10^5 * 10^4 = 10^9
Slices are used directly since each task is represented as a []int.
Worked Examples
Example 1
Input:
[[1,2],[2,4],[4,8]]
Step 1: Sort
Compute differences:
| Task | minimum - actual |
|---|---|
| [1,2] | 1 |
| [2,4] | 2 |
| [4,8] | 4 |
Sorted order:
[[4,8],[2,4],[1,2]]
Step 2: Simulation
| Task | Current Energy Before | Minimum Needed | Extra Added | Current Energy After |
|---|---|---|---|---|
| [4,8] | 0 | 8 | 8 | 4 |
| [2,4] | 4 | 4 | 0 | 2 |
| [1,2] | 2 | 2 | 0 | 1 |
Final answer:
8
Example 2
Input:
[[1,3],[2,4],[10,11],[10,12],[8,9]]
Step 1: Sort
| Task | Difference |
|---|---|
| [1,3] | 2 |
| [2,4] | 2 |
| [10,11] | 1 |
| [10,12] | 2 |
| [8,9] | 1 |
One valid sorted order:
[[1,3],[2,4],[10,12],[10,11],[8,9]]
Step 2: Simulation
| Task | Energy Before | Minimum | Extra Added | Energy After |
|---|---|---|---|---|
| [1,3] | 0 | 3 | 3 | 2 |
| [2,4] | 2 | 4 | 2 | 2 |
| [10,12] | 2 | 12 | 10 | 2 |
| [10,11] | 2 | 11 | 9 | 1 |
| [8,9] | 1 | 9 | 8 | 1 |
Total added:
3 + 2 + 10 + 9 + 8 = 32
Final answer:
32
Example 3
Input:
[[1,7],[2,8],[3,9],[4,10],[5,11],[6,12]]
All tasks have:
minimum - actual = 6
So any order works.
Simulation
| Task | Energy Before | Minimum | Extra Added | Energy After |
|---|---|---|---|---|
| [1,7] | 0 | 7 | 7 | 6 |
| [2,8] | 6 | 8 | 2 | 6 |
| [3,9] | 6 | 9 | 3 | 6 |
| [4,10] | 6 | 10 | 4 | 6 |
| [5,11] | 6 | 11 | 5 | 6 |
| [6,12] | 6 | 12 | 6 | 6 |
Total:
7 + 2 + 3 + 4 + 5 + 6 = 27
Final answer:
27
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Sorting dominates the runtime |
| Space | O(1) or O(log n) | Depends on sorting implementation |
The simulation loop itself is linear. The only expensive operation is sorting the tasks according to the greedy priority rule.
Python's Timsort uses logarithmic auxiliary stack space in practice. Go's sort implementation also uses logarithmic recursion depth.
Test Cases
from typing import List
class Solution:
def minimumEffort(self, tasks: List[List[int]]) -> int:
tasks.sort(key=lambda task: task[1] - task[0], reverse=True)
required_energy = 0
current_energy = 0
for actual, minimum in tasks:
if current_energy < minimum:
extra = minimum - current_energy
required_energy += extra
current_energy += extra
current_energy -= actual
return required_energy
sol = Solution()
assert sol.minimumEffort([[1,2],[2,4],[4,8]]) == 8
# Basic example from statement
assert sol.minimumEffort([[1,3],[2,4],[10,11],[10,12],[8,9]]) == 32
# Mixed task sizes and requirements
assert sol.minimumEffort([[1,7],[2,8],[3,9],[4,10],[5,11],[6,12]]) == 27
# All tasks have identical difference
assert sol.minimumEffort([[5,5]]) == 5
# Single task where actual equals minimum
assert sol.minimumEffort([[1,10]]) == 10
# Single task with large startup requirement
assert sol.minimumEffort([[1,2],[1,2],[1,2]]) == 4
# Repeated identical tasks
assert sol.minimumEffort([[2,2],[2,2],[2,2]]) == 6
# Tasks that completely consume current energy
assert sol.minimumEffort([[1,10000]]) == 10000
# Maximum minimum value
assert sol.minimumEffort([[3,5],[2,2],[1,10]]) == 10
# Large difference task should go first
assert sol.minimumEffort([[5,9],[4,8],[3,7],[2,6]]) == 14
# Descending structure
assert sol.minimumEffort([[1,2]]) == 2
# Smallest nontrivial input
| Test | Why |
|---|---|
[[1,2],[2,4],[4,8]] |
Validates standard greedy ordering |
[[1,3],[2,4],[10,11],[10,12],[8,9]] |
Mixed constraints and costs |
[[1,7],[2,8],[3,9],[4,10],[5,11],[6,12]] |
Equal differences across all tasks |
[[5,5]] |
Single exact requirement task |
[[1,10]] |
Large minimum requirement |
[[1,2],[1,2],[1,2]] |
Duplicate tasks |
[[2,2],[2,2],[2,2]] |
Energy fully depleted each step |
[[1,10000]] |
Constraint boundary |
[[3,5],[2,2],[1,10]] |
Validates sorting importance |
[[5,9],[4,8],[3,7],[2,6]] |
Multiple equal gaps |
[[1,2]] |
Minimal valid input |
Edge Cases
One important edge case is when there is only a single task. In that scenario, the answer is simply the task's minimum required energy. A buggy implementation might incorrectly add the actual energy cost again or mishandle initialization. The provided implementation correctly checks whether current energy is below the minimum and adds only the required amount.
Another important case occurs when several tasks have identical values of:
minimum - actual
The greedy proof guarantees that any relative ordering among such tasks is valid. Some implementations mistakenly rely on a secondary sorting rule that is unnecessary and can complicate reasoning. This solution only sorts by the required greedy criterion, which is sufficient.
A third critical edge case involves tasks where:
actual == minimum
These tasks are strict because after completing them, energy drops significantly with no extra buffer. A buggy implementation may allow the task to start with insufficient energy due to incorrect comparison operators. This implementation uses:
if current_energy < minimum:
which correctly enforces the exact minimum threshold.
Another subtle case is when a task has an extremely large startup requirement but a tiny execution cost, such as:
[1, 10000]
If this task is delayed until later, previously consumed energy can force a much larger initial requirement. Sorting by descending (minimum - actual) correctly prioritizes these tasks early, preventing unnecessary energy inflation later in the schedule.