Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Calculate total damage: First, we compute sum(damage) to get the total damage from all levels if we don't use armor at all.

  2. Find maximum damage level: We identify max(damage) to find the level where we should use our armor for maximum benefit.

  3. 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 block armor amount of damage
  4. 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 Evaluator

Example 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 all n elements once, taking O(n) time
  • max(damage) also iterates through all n elements to find the maximum value, taking O(n) time
  • min(max(damage), armor) is a constant time operation O(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 cause max([]) to throw a ValueError
  • armor = 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)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More