Start learning free

LESSON 294 OF 348 · HEAP

Kill Monsters in Kotlin: Max Heap and Magic Potions

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 you reach zero HP, you die. Therefore you can't win agains 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 6th and 7th monsters
STEP 1 OF 10
    Ctrl + Enter to check

    KOTLIN · HEAP

    Kill Monsters in Kotlin, explained

    Problem overview

    A hero starts with HP health points and K magic potions and fights monsters in the given order. The array damage holds the damage each monster deals. A negative value means the monster heals you by that amount. A potion kills a monster outright, so the hero loses nothing from it. The function returns the largest number of monsters the hero can kill, with the potions used as well as possible.

    For K = 2, HP = 10 and damage = [-3, 2, 3, -2, 8, 8, 6, 4, 3, 3] the answer is 8. The hero can reach 0 HP exactly after the eighth monster, for example by using the two potions on the two monsters that deal 8 damage. The ninth monster is out of reach, so the count is 8.

    Approach

    You don't know in advance which monsters deserve a potion. The trick is to decide late:

    • Fight each monster as if no potion existed and subtract its damage from HP.
    • Remember every damage in a max heap, so the biggest hit so far is always on top.
    • If HP drops to 0 or below, you go back in time: use a potion on the monster that hurt you most, which gives its damage back.

    Spending a potion on the biggest hit among the monsters fought so far is the best use of it, because it restores the most HP. If even that does not bring HP above 0, then no choice of potions would, and the hero cannot kill this monster. Monsters that heal the hero never need a potion and never go into the heap.

    Each monster enters and leaves the heap at most once, so the cost is O(n log n).

    Step by step

    1. The heap. kills is a PriorityQueue<Int> built with compareByDescending { it }. The queue is a min heap by default, and the comparator reverses it so that the largest damage is on top.
    2. The loop. A for loop over damage.indices gives the index i. The index is needed because the answer counts monsters.
    3. Healing monsters. If damage[i] <= 0, add -damage[i] to hp and continue. The code copies the parameters into var k = K and var hp = HP, because Kotlin function parameters are read-only.
    4. Fight. Subtract damage[i] from hp.
    5. Remember the hit. Add damage[i] to kills, so a potion can still be used on this monster later.
    6. Use potions while needed. The while loop runs as long as hp <= 0, a potion is left (k > 0) and the heap is not empty.
    7. One potion. Inside the loop, kills.poll() removes the largest damage so far, that damage is added back to hp, and k-- uses up the potion.
    8. Dead. After the while, if hp < 0, return i: the hero can't survive monster i, so only the i monsters before it count.
    9. Zero HP. If hp == 0, return i + 1. Monster i is killed, but the hero has no HP left, so the fight stops there. The potion loop ends at exactly 0 only when no potion or no earlier monster is left to use.
    10. Survived. After the loop, return damage.size.

    The solution in Kotlin

    class Solution {
        fun killMonsters(damage: IntArray, K: Int, HP: Int): Int {
            var k = K
            var hp = HP
            val kills = PriorityQueue<Int>(compareByDescending { it })
            for (i in damage.indices) {
                if (damage[i] <= 0) {
                    hp += -damage[i]
                    continue
                }
                hp -= damage[i]
                kills.add(damage[i])
                while (hp <= 0 && k > 0 && kills.isNotEmpty()) {
                    hp += kills.poll()
                    k--
                }
                if (hp < 0) {
                    return i
                }
                if (hp == 0) {
                    return i + 1
                }
            }
            return damage.size
        }
    }
    

    Kotlin details worth knowing:

    • Parameters are vals and can't be reassigned. That's why the code copies K and HP into the vars k and hp. Assigning to K would not compile.
    • PriorityQueue comes from java.util; a standalone file needs import java.util.PriorityQueue.
    • kills.poll() returns a platform type, so hp += kills.poll() compiles. The isNotEmpty() check in the loop condition guarantees that the queue has a value to give.
    • && short-circuits, so the conditions are checked left to right and stop at the first false one.
    • continue skips the rest of that pass of the for loop, like in Java.

    Following the example

    Start with hp = 10. The first monster (-3) heals, so HP is 13. Monsters with damage 2 and 3 bring it to 8, and the monster with -2 heals it to 10. The 8 brings it to 2 and the next 8 to -6. The potion loop pops the 8, giving HP 2 and one potion left. Then comes 6: HP -4. The loop pops the other 8, giving HP 4 and no potions left. Monster 7 (damage 4) brings HP to 0, so the function returns i + 1, which is 8.

    Complexity

    Time: O(n log n). Every monster with positive damage is added to the heap once, and each is polled at most once. Both operations cost O(log n). Because each potion removes one heap entry, the inner loop runs at most K times in total.

    Space: O(n) for the heap in the worst case.

    Edge cases and common mistakes

    • No potions. With K = 0 the heap is never polled, and the hero dies at the first monster that brings HP to 0 or below.
    • Monsters that heal. A damage of 0 or less raises HP and is skipped by the heap. A potion on a healing monster would waste it.
    • Dying at exactly 0. The hero reaching 0 HP is dead, but the monster that did it still counts as killed. That is why the code returns i + 1 here and i when HP is negative.
    • Using a min heap. That would spend potions on the smallest hit and recover the least HP.
    • Using the original parameters. Updating HP in place doesn't compile in Kotlin, and changing k and hp inside the loop is what the algorithm needs.
    Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free