Problem overview
A hero starts with HP health points and K magic potions. The method killMonsters receives a vector<int> called damage, K and HP. The hero fights the monsters in the given order. Monster i takes damage[i] health points away, and a negative value gives health back instead. A potion kills one monster without a fight, so that monster costs nothing. The method returns the largest number of monsters the hero can kill in a row.
Take K = 2, HP = 10 and damage = [-3, 2, 3, -2, 8, 8, 6, 4, 3, 3]. The answer is 8. With potions on the two monsters that deal 8, the hero's HP goes 13, 11, 8, 10, then stays at 10 over both skipped monsters, then 4 after the monster that deals 6, and finally 0 after the one that deals 4. Reaching exactly 0 still counts the last monster as killed, but the hero cannot fight on, so the count stops at 8.
Approach: spend a potion only when you are forced to
The question is where to use the potions. The brute-force answer tries every way of choosing K monsters out of N, which is far too slow. A greedy rule is enough.
Using a potion on a monster is the same as never taking its damage. So walk through the monsters in order and fight each one. If HP drops to 0 or below, the hero would have died, which means a potion was needed somewhere among the monsters fought so far. The best place for it is the monster that hurt the most, because cancelling the largest damage gives back the most HP. Doing it afterwards is allowed: if the potion had been used on that monster at the time, every HP value in between would only have been higher, so the hero would have been safe in the meantime too.
That rule needs a way to find the largest damage among the monsters fought so far, again and again, while new monsters are added. That is exactly what a max-heap is for. In C++ the priority_queue<int> is a max-heap by default: top() returns the biggest element, push() adds one and pop() removes the biggest, each in logarithmic time.
Monsters with damage 0 or less never need a potion, because they cost nothing or even help. They are applied to HP directly and never go into the heap.
The example, step by step
| Monster | damage | HP after the fight | Potion | HP and K after |
|---|---|---|---|---|
| 0 | -3 | 13 | - | 13, K = 2 |
| 1 | 2 | 11 | - | 11, K = 2 |
| 2 | 3 | 8 | - | 8, K = 2 |
| 3 | -2 | 10 | - | 10, K = 2 |
| 4 | 8 | 2 | - | 2, K = 2 |
| 5 | 8 | -6 | on a monster with 8 | 2, K = 1 |
| 6 | 6 | -4 | on a monster with 8 | 4, K = 0 |
| 7 | 4 | 0 | none left | 0, K = 0 |
At monster 7 the hero has exactly 0 HP and no potions, so the method returns 8, the number of monsters up to and including monster 7.
Step by step
- Create the max-heap
kills, an emptypriority_queue<int>. It remembers the damage of every monster that was fought. - Loop over the monsters with an
intindexi. The index is needed because the answer is a count of monsters. - Handle healing monsters. If
damage[i] <= 0, add-damage[i]toHPandcontinue. Subtracting a negative damage would raise HP too, but writing it as an addition makes the intent clear. - Fight the monster. Subtract
damage[i]fromHPand pushdamage[i]into the heap, so a potion can still be applied to this monster later. - Rescue the hero with potions. While
HP <= 0 && K > 0 && !kills.empty(), take the largest damage withkills.top(), add it back toHP, remove it withkills.pop()and decrementK. The order matters, becausepop()returns nothing in C++, so the value has to be read before. - Decide whether the hero survived. If
HP < 0even after the potions, monstericannot be beaten, so the hero killed theimonsters before it. Returni. - Handle exactly zero. The potion loop only stops at
HP == 0when no potion is left. The hero killed monsteri, but has no health left, so returni + 1. - Return
damage.size()after the loop: the hero survived every monster.
The solution in C++
class Solution {
public:
int killMonsters(vector<int>& damage, int K, int HP) {
priority_queue<int> kills;
for (int i = 0; i < damage.size(); i++) {
if (damage[i] <= 0) {
HP += -damage[i];
continue;
}
HP -= damage[i];
kills.push(damage[i]);
while (HP <= 0 && K > 0 && !kills.empty()) {
HP += kills.top();
kills.pop();
K--;
}
if (HP < 0) {
return i;
}
if (HP == 0) {
return i + 1;
}
}
return damage.size();
}
};
The C++ details worth a closer look:
- Parameters by value and by reference.
damageis a reference, so the vector is not copied.KandHPare plainintparameters, copies local to the call. That is why the method can freely decrementKand changeHPwithout affecting the caller. - The heap is a max-heap by default. A min-heap would need
priority_queue<int, vector<int>, greater<int>>. Here the biggest damage is wanted, so the default is right. top()beforepop().top()returns a reference to the largest element, andpop()returnsvoid. Callingtop()on an empty queue is undefined behaviour, which is what the!kills.empty()check in the loop condition prevents.damage.size()is unsigned.i < damage.size()compares anintwith asize_t, which is fine here becauseistarts at 0. The finalreturn damage.size()converts back tointimplicitly.- Overflow. HP and the damage values are plain
int. If the inputs could be large enough for the running total to pass about two billion, signed overflow would be undefined behaviour, andlong longwould be the safer type.
Complexity
Time: O(N log N), where N is the number of monsters. Each monster is pushed at most once and popped at most once, and each heap operation costs O(log N), since the heap can hold up to N damages. The potion loop runs at most K times in total, never more than N.
Space: O(N), for the heap that may hold the damage of every fought monster.
Edge cases and common mistakes
- No monsters. The loop does not run and the method returns 0.
- No potions. With
K = 0the hero fights until HP runs out. Fordamage = [3, 3, 3], HP 10 gives 3, and HP 9 also gives 3, because ending at exactly 0 counts the last monster. Withdamage = [5], HP 4 gives 0. - Healing monsters. They must not go into the heap. A potion spent on a healing monster would only cancel the health it gives.
- Zero damage. A monster with damage 0 is treated as a healing monster that heals nothing, and the code skips it correctly.
- Using potions too early. Spending a potion on the first big monster you meet is not optimal. The potion should go to the largest damage seen so far, and only when HP requires it.
- Returning
iinstead ofi + 1. The two returns differ on purpose: negative HP means monsteriwas not killed, while exactly 0 means it was. - Reading
top()afterpop(). The element is gone by then, and you would add the wrong damage back.
Testing
damage = [-3, 2, 3, -2, 8, 8, 6, 4, 3, 3],K = 2,HP = 10gives8.damage = [],K = 1,HP = 5gives0.damage = [5],K = 0,HP = 6gives1, and withHP = 4gives0.damage = [3, 3, 3],K = 1,HP = 5gives2.damage = [5],K = 1,HP = 3gives1.
