Start learning free

LESSON 319 OF 415 · HEAP

Kill Monsters in Swift: Greedy Potions with a Max Heap

Practice O(n log n) Read the full explanation

You are a hero in a game, initially having HP health points and K magic potions.

You have to fight N monsters in the order they are given, knowing the damage each monster deals to you.

damage[i] represents the damage the monster deals to you when you fight them. If the damage is negative, it means your HP increases by that amount.

If you use a magic potion on a monster, it will kill the monster without you fighting or losing health points.

If your HP drops below zero, you lose the fight. If it drops to exactly zero HP, you still kill that monster, but you are left with no health, so the fight ends there. Therefore you can't win against a monster that has damage[i] > HP.

Find the maximum number of monsters you can kill, considering you are playing optimally.

Example

Input: K = 2, HP = 10,
       damage = [-3, 2, 3, -2, 8, 8, 6, 4, 3, 3]
Output: 8
Explanation: You kill the first 8 monsters by using the two potions on the 5th and 6th monsters
STEP 1 OF 10
    Ctrl + Enter to check

    SWIFT · HEAP

    Kill Monsters in Swift, explained

    Problem overview

    The method killMonsters receives the monsters' damage values in fight order, damage, the number of magic potions K and the hero's starting health HP. Fighting a monster subtracts its damage from the hero's health, and a negative damage heals the hero instead. A potion kills one monster instantly, with no fight and no loss of health. The hero dies when health reaches zero or less. The method returns the largest number of monsters the hero can kill, assuming the potions are used as well as possible.

    In the example, K = 2, HP = 10 and damage = [-3, 2, 3, -2, 8, 8, 6, 4, 3, 3]. Health goes 13, 11, 8, 10 over the first four monsters. Then the two 8s and the 6 would cost 22 health, but two potions can cancel the two biggest hits. The 4 brings health to exactly 0, so the hero kills 8 monsters in total and goes no further.

    Approach: decide about potions afterwards

    The difficulty is choosing where to spend the potions. A potion on the first big monster may be a waste if a bigger one comes later. The useful trick is to postpone the decision: fight every monster as if no potion existed, and only when health runs out look back and apply a potion to whichever monster already fought hurt the most. Applying it means giving that monster's damage back to the hero, as if the monster had been killed by the potion instead of the fight.

    Taking back the largest damage so far is optimal, because it restores the most health with a single potion. To find that largest damage quickly, keep the damage of every monster fought so far in a max heap. Monsters with zero or negative damage never go in: they cost nothing, so they never need a potion.

    The loop stops at the first monster the hero can't survive. The number of monsters killed is how far it got.

    Swift has no heap in its standard library, so the lesson provides a MaxHeap struct with insert, extract and isEmpty.

    Step by step

    1. Create the heap. var kills = MaxHeap(). It is a var because inserting and extracting change it.
    2. Loop with an index. for i in 0..<damage.count. The index matters because the answer is a number of monsters.
    3. Skip harmless monsters. If damage[i] <= 0, add -damage[i] to HP and continue. Parameters in Swift are constants, so the starter code first copies K and HP into variables with the same names.
    4. Fight. Subtract damage[i] from HP.
    5. Remember the monster. Insert damage[i] into kills, so a potion can still be applied to it later.
    6. Use potions while needed. Loop while HP <= 0 && K > 0 && !kills.isEmpty: the hero is out of health and still has a potion and a fought monster to use it on.
    7. Apply one potion. Inside, add kills.extract(), the largest damage, back to HP and do K -= 1. Swift has no -- operator.
    8. Fail case. After the loop, if HP < 0, return i. Even with potions the hero can't survive monster i, so only the i monsters before it count, since indices start at 0.
    9. Zero health. If HP == 0, return i + 1. Monster i is killed, but the hero has no health left and no potion to continue with.
    10. Survived everyone. After the for loop, return damage.count.

    The solution in Swift

    class Solution {
        func killMonsters(_ damage: [Int], _ K: Int, _ HP: Int) -> Int {
            var K = K
            var HP = HP
            var kills = MaxHeap()
            for i in 0..<damage.count {
                if damage[i] <= 0 {
                    HP += -damage[i]
                    continue
                }
                HP -= damage[i]
                kills.insert(damage[i])
                while HP <= 0 && K > 0 && !kills.isEmpty {
                    HP += kills.extract()
                    K -= 1
                }
                if HP < 0 {
                    return i
                }
                if HP == 0 {
                    return i + 1
                }
            }
            return damage.count
        }
    }
    

    The Swift details worth a closer look:

    • Shadowing a parameter. var K = K declares a new local variable with the same name as the constant parameter, initialised from it. This is the usual Swift idiom for a parameter you want to change.
    • continue. It skips the rest of the loop body and moves to the next index.
    • isEmpty is a property, so it is written without parentheses.
    • Returning from inside a loop. Both early return statements end the method at once, which is why no code after the loop has to repeat the checks.
    • Overflow. Swift's Int is 64 bits and an overflowing addition crashes the program, so very large damage values would need care. Ordinary values are far from the limit.

    Complexity

    Time: O(n log n). Every monster is inserted into the heap at most once, and every extraction removes one entry for good, so there are at most n insertions and n extractions, each O(log n).

    Space: O(n). The heap can hold the damage of every monster fought.

    Edge cases and common mistakes

    • No monsters. The method returns 0, which equals damage.count.
    • No potions. With K = 0, [4, 4] and HP = 5, the second fight leaves health at -3, so the method returns 1.
    • Health of exactly zero. [5, 1] with K = 0 and HP = 5 returns 1: the first monster takes the hero to 0, and that monster counts.
    • A potion saves the hero. [6] with K = 1 and HP = 5 returns 1.
    • Not enough potions. [6, 7] with K = 1 and HP = 5 returns 1: the potion handles the 6, and the 7 is too strong.
    • Healing monsters. A zero or negative damage never enters the heap. Putting a negative value in would make a "potion" take health away.
    • Spending a potion at once. Using a potion on the current monster when health runs out can be wrong, because an earlier monster may have hurt more. The heap finds the biggest one.

    Testing

    • damage = [-3, 2, 3, -2, 8, 8, 6, 4, 3, 3], K = 2, HP = 10 gives 8.
    • damage = [], K = 1, HP = 5 gives 0.
    • damage = [3, 3, 3], K = 0, HP = 7 gives 2.
    • damage = [3, 3, 3], K = 1, HP = 7 gives 3.
    • damage = [6, 7], K = 1, HP = 5 gives 1.
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free