2214. Minimum Health to Beat Game π
Problem Description
You're playing a game with n
levels (numbered from 0
to n - 1
). Each level causes you to lose a certain amount of health, given in an array called damage
, where damage[i]
represents the health you lose when completing level i
.
You have a special armor ability that you can use at most once during the entire game. When you use this armor on any level, it will protect you from up to armor
points of damage on that specific level. For example, if a level would deal 10 damage and your armor value is 7, you would only take 3 damage. If your armor value is 15, you would take 0 damage from that level.
The game has the following rules:
- You must complete the levels in order (from level 0 to level n-1)
- Your health must always be greater than 0 (not equal to 0) to continue playing
- You can choose which level to use your armor on (or not use it at all)
Your task is to find the minimum starting health needed to successfully complete all levels of the game.
For example, if damage = [2, 7, 4, 3]
and armor = 4
, you might choose to use your armor on level 1 (which deals 7 damage), reducing it to 3 damage. So you'd take: 2 + 3 + 4 + 3 = 12 total damage, meaning you need at least 13 health to survive the game.
Intuition
Since we can use the armor ability only once throughout the entire game, we want to maximize its benefit. The key insight is: where should we use the armor to minimize the total damage taken?
Think about it this way - the armor blocks up to armor
points of damage on whichever level we choose. To get the maximum benefit, we should use it on the level that deals the most damage. Why? Because that's where we can potentially block the most damage.
Consider this: if we have levels dealing [2, 7, 4, 3]
damage and armor = 4
:
- Using armor on level 0: blocks 2 damage (the full 2 damage of that level)
- Using armor on level 1: blocks 4 damage (4 out of 7 damage)
- Using armor on level 2: blocks 4 damage (the full 4 damage of that level)
- Using armor on level 3: blocks 3 damage (the full 3 damage of that level)
The maximum damage we can avoid is min(max(damage), armor)
. This is because:
- We target the level with
max(damage)
- But we can only block up to
armor
amount of damage - So the actual damage blocked is the minimum of these two values
Once we know the maximum damage we can avoid, the total damage we'll take is:
total_damage = sum(damage) - min(max(damage), armor)
Since our health must be greater than 0 after taking all this damage, we need at least:
minimum_health = total_damage + 1
This greedy approach of using armor on the highest damage level guarantees we minimize the total damage taken, and therefore minimize the starting health required.
Learn more about Greedy patterns.
Solution Approach
The implementation follows a greedy strategy where we use the armor on the level with maximum damage to minimize the total damage taken.
Here's the step-by-step breakdown:
-
Calculate total damage: First, we compute
sum(damage)
to get the total damage from all levels if we don't use armor at all. -
Find maximum damage level: We identify
max(damage)
to find the level where we should use our armor for maximum benefit. -
Determine armor effectiveness: We calculate
min(max(damage), armor)
to find how much damage we can actually block:- If
max(damage) β€ armor
, we can block all damage from that level - If
max(damage) > armor
, we can only blockarmor
amount of damage
- If
-
Calculate minimum health: The formula becomes:
minimum_health = sum(damage) - min(max(damage), armor) + 1
Breaking this down:
sum(damage)
is the total damage we'd take without armor- We subtract
min(max(damage), armor)
which is the damage avoided by using armor optimally - We add
1
because our health must be greater than 0 (not equal to 0) after taking all damage
Example walkthrough:
If damage = [2, 7, 4, 3]
and armor = 4
:
- Total damage without armor:
2 + 7 + 4 + 3 = 16
- Maximum damage level:
max(damage) = 7
- Damage blocked by armor:
min(7, 4) = 4
- Total damage after using armor:
16 - 4 = 12
- Minimum health needed:
12 + 1 = 13
The solution is implemented in a single line because the greedy approach is straightforward - we don't need to track states or use complex data structures. The time complexity is O(n)
for finding the sum and maximum, and space complexity is O(1)
.
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 small example with damage = [3, 5, 2]
and armor = 4
.
Step 1: Calculate total damage without armor
- Sum of all damage:
3 + 5 + 2 = 10
- This is how much damage we'd take if we don't use armor at all
Step 2: Find the maximum damage level
- Looking at each level: 3, 5, 2
- Maximum damage is
5
(at level 1) - This is where we should use our armor for maximum benefit
Step 3: Calculate how much damage the armor blocks
- The armor can block up to 4 points of damage
- Level 1 deals 5 damage
- Damage blocked =
min(5, 4) = 4
- So level 1 will now deal
5 - 4 = 1
damage instead of 5
Step 4: Calculate total damage after using armor
- Level 0: 3 damage (no armor)
- Level 1: 1 damage (armor used, blocked 4 out of 5)
- Level 2: 2 damage (no armor)
- Total:
3 + 1 + 2 = 6
damage
Step 5: Determine minimum starting health
- We take 6 total damage
- Our health must be > 0 after all levels
- Minimum health =
6 + 1 = 7
Verification: Starting with 7 health:
- After level 0:
7 - 3 = 4
health (> 0 β) - After level 1:
4 - 1 = 3
health (> 0 β) - After level 2:
3 - 2 = 1
health (> 0 β)
All levels completed successfully with health > 0!
Solution Implementation
1class Solution:
2 def minimumHealth(self, damage: List[int], armor: int) -> int:
3 """
4 Calculate the minimum health needed to survive all damage attacks.
5
6 The armor can be used once to block up to 'armor' amount of damage from a single attack.
7 Strategy: Use armor on the attack that deals the most damage (up to armor's capacity).
8
9 Args:
10 damage: List of integers representing damage from each attack
11 armor: Integer representing the maximum damage the armor can block (used once)
12
13 Returns:
14 Minimum health points needed to survive all attacks
15 """
16 # Calculate total damage from all attacks
17 total_damage = sum(damage)
18
19 # Find the maximum damage attack in the list
20 max_damage_attack = max(damage)
21
22 # Armor blocks at most 'armor' damage from the largest attack
23 # If the largest attack is less than armor capacity, it blocks the entire attack
24 damage_blocked = min(max_damage_attack, armor)
25
26 # Minimum health = (total damage - damage blocked by armor) + 1
27 # We need +1 because we must have at least 1 health remaining after all attacks
28 minimum_health_needed = total_damage - damage_blocked + 1
29
30 return minimum_health_needed
31
1class Solution {
2 public long minimumHealth(int[] damage, int armor) {
3 // Calculate total damage and find the maximum damage value
4 long totalDamage = 0;
5 int maxDamage = damage[0];
6
7 // Iterate through all damage values
8 for (int damageValue : damage) {
9 // Add current damage to total
10 totalDamage += damageValue;
11 // Update maximum damage if current is greater
12 maxDamage = Math.max(maxDamage, damageValue);
13 }
14
15 // Calculate minimum health needed:
16 // Total damage - armor reduction (limited by max single damage) + 1
17 // Armor can only block up to its value or the maximum single damage, whichever is smaller
18 long armorReduction = Math.min(maxDamage, armor);
19 long minimumHealthNeeded = totalDamage - armorReduction + 1;
20
21 return minimumHealthNeeded;
22 }
23}
24
1class Solution {
2public:
3 long long minimumHealth(vector<int>& damage, int armor) {
4 // Calculate the total damage sum
5 long long totalDamage = 0;
6
7 // Track the maximum damage value (best candidate for armor usage)
8 int maxDamage = damage[0];
9
10 // Iterate through all damage values
11 for (int& damageValue : damage) {
12 // Accumulate total damage
13 totalDamage += damageValue;
14
15 // Update maximum damage encountered
16 maxDamage = max(maxDamage, damageValue);
17 }
18
19 // Calculate minimum health needed:
20 // Total damage - armor reduction (applied to the largest damage) + 1
21 // Armor can only reduce up to its value, so we take min(maxDamage, armor)
22 return totalDamage - min(maxDamage, armor) + 1;
23 }
24};
25
1/**
2 * Calculates the minimum health required to survive all damage attacks
3 * @param damage - Array of damage values from each attack
4 * @param armor - Armor value that can block damage from one attack
5 * @returns The minimum health points needed to survive all attacks
6 */
7function minimumHealth(damage: number[], armor: number): number {
8 // Total sum of all damage values
9 let totalDamage: number = 0;
10
11 // Maximum damage value among all attacks
12 let maxDamage: number = 0;
13
14 // Iterate through all damage values
15 for (const damageValue of damage) {
16 // Update maximum damage if current value is larger
17 maxDamage = Math.max(maxDamage, damageValue);
18
19 // Accumulate total damage
20 totalDamage += damageValue;
21 }
22
23 // Calculate minimum health needed:
24 // Total damage minus the damage blocked by armor (up to the maximum single damage)
25 // Plus 1 to ensure survival (health must be > 0)
26 return totalDamage - Math.min(maxDamage, armor) + 1;
27}
28
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array damage
. This is because:
sum(damage)
iterates through alln
elements once, takingO(n)
timemax(damage)
also iterates through alln
elements to find the maximum value, takingO(n)
timemin(max(damage), armor)
is a constant time operationO(1)
- The arithmetic operations (subtraction and addition) are
O(1)
Since these operations are performed sequentially, the overall time complexity is O(n) + O(n) + O(1) = O(n)
.
The space complexity is O(1)
. The algorithm only uses a constant amount of extra space for:
- Storing the sum result
- Storing the maximum value from the damage array
- Storing the minimum between max damage and armor
- Performing the final calculation
No additional data structures that scale with input size are created, so the space complexity remains constant.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Health Requirement
The Problem: A common mistake is thinking that having exactly 0 health after all damage is acceptable. Many developers incorrectly calculate the minimum health as just sum(damage) - min(max(damage), armor)
without adding 1.
Why it happens: The problem statement says "health must always be greater than 0", but it's easy to misread this as "greater than or equal to 0" or forget this constraint when implementing.
Example of incorrect code:
def minimumHealth(self, damage: List[int], armor: int) -> int:
return sum(damage) - min(max(damage), armor) # WRONG: allows health to reach 0
Correct approach: Always add 1 to ensure health remains positive:
def minimumHealth(self, damage: List[int], armor: int) -> int:
return sum(damage) - min(max(damage), armor) + 1 # Ensures health > 0
Pitfall 2: Not Handling Edge Cases
The Problem: Failing to consider edge cases like empty damage arrays or when armor value is 0.
Why it happens: The greedy solution seems straightforward, but edge cases can cause runtime errors with functions like max()
on empty lists.
Example scenarios:
damage = []
would causemax([])
to throw a ValueErrorarmor = 0
means no protection (though the formula handles this correctly)
Solution: Add validation for edge cases:
def minimumHealth(self, damage: List[int], armor: int) -> int:
if not damage: # Empty damage array
return 1 # Need at least 1 health to "survive"
return sum(damage) - min(max(damage), armor) + 1
Pitfall 3: Overthinking the Problem
The Problem: Attempting to use dynamic programming or trying to determine if using armor on a different level (not the maximum) could somehow be better.
Why it happens: The problem seems like it might require exploring different choices, leading to unnecessary complexity.
Incorrect approach example:
def minimumHealth(self, damage: List[int], armor: int) -> int:
# Unnecessary DP approach trying all possibilities
min_health = float('inf')
for i in range(len(damage)):
# Try using armor on level i
total = sum(damage) - min(damage[i], armor)
min_health = min(min_health, total + 1)
return min_health
Why the greedy approach works: Using armor on the maximum damage level always yields the optimal result because:
- We want to minimize total damage taken
- Armor reduces damage by
min(damage[i], armor)
for whichever level we choose - To maximize reduction, we should choose the level where
min(damage[i], armor)
is largest - This is always the level with maximum damage (within armor's capacity)
Is the following code DFS or BFS?
void search(Node root) { if (!root) return; visit(root); root.visited = true; for (Node node in root.adjacent) { if (!node.visited) { search(node); } } }
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
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
Want a Structured Path to Master System Design Too? Donβt Miss This!