1921. Eliminate Maximum Number of Monsters
Problem Description
You're defending a city from n
monsters in a video game. Each monster starts at a certain distance from the city and walks toward it at a constant speed.
Given:
dist[i]
: the initial distance (in kilometers) of thei-th
monster from the cityspeed[i]
: the speed (in kilometers per minute) of thei-th
monster
You have a weapon that can eliminate one monster at a time, but it takes exactly one minute to charge between shots. The weapon starts fully charged at the beginning of the game.
Important rules:
- You can only eliminate one monster per minute
- The weapon needs one full minute to recharge after each use
- You lose if any monster reaches the city
- If a monster arrives at the exact moment your weapon becomes ready, you still lose (the monster reaches the city before you can shoot)
Your goal is to find the maximum number of monsters you can eliminate before losing the game. If you can eliminate all n
monsters before any reach the city, return n
.
The strategy involves calculating when each monster would reach the city and determining how many you can eliminate in time. For each monster, you need to calculate the latest moment you can eliminate it (just before it reaches the city), then greedily eliminate monsters in the order that gives you the most eliminations possible.
The key insight is that the latest safe elimination time for monster i
is โ(dist[i] - 1) / speed[i]โ
minutes (using floor division to get whole minutes). By sorting these times and checking if you have enough time to eliminate each monster in sequence, you can determine the maximum number of eliminations possible.
Intuition
Think about what happens in this game: you can shoot exactly one monster per minute, starting at minute 0. So at minute 0 you shoot the first monster, at minute 1 you shoot the second monster, and so on.
The critical question is: which monsters should we shoot and in what order?
First, let's figure out when each monster becomes a threat. A monster at distance dist[i]
moving at speed speed[i]
will reach the city at time dist[i] / speed[i]
. But we need to eliminate it before it reaches the city. Since we're dealing with discrete minutes, the latest minute we can eliminate this monster is โ(dist[i] - 1) / speed[i]โ
.
Why (dist[i] - 1)
? Because if a monster is at distance 5 and speed 1, it reaches at minute 5. We need to shoot it by minute 4 at the latest. Using (5-1)/1 = 4
gives us this deadline correctly.
Now comes the key insight: we should eliminate monsters in order of their deadlines. Why? Because:
- At minute 0, we must shoot some monster
- At minute 1, we must shoot another monster
- And so on...
If we have the deadlines sorted, we can check:
- Can we shoot monster 0 at minute 0? (Is its deadline >= 0?)
- Can we shoot monster 1 at minute 1? (Is its deadline >= 1?)
- Can we shoot monster 2 at minute 2? (Is its deadline >= 2?)
The greedy approach works because once we've sorted by deadlines, shooting the monsters with earlier deadlines first ensures we don't miss any opportunity. If a monster's deadline is less than the minute we'd shoot it (its index in the sorted order), then we can't eliminate it in time, and that's where we stop.
This transforms the problem from "which monsters to shoot and when" into a simple check: after sorting by deadlines, does each monster's deadline allow us to shoot it at its position in the queue?
Solution Approach
Following our intuition, let's implement the solution step by step:
Step 1: Calculate elimination deadlines
For each monster, we calculate the latest minute at which we can eliminate it:
times = sorted((d - 1) // s for d, s in zip(dist, speed))
This line does several things:
zip(dist, speed)
pairs up each monster's distance with its speed- For each pair
(d, s)
, we calculate(d - 1) // s
using integer division - The
(d - 1)
ensures we get the last full minute before the monster arrives - We sort these deadlines in ascending order
Step 2: Check if we can eliminate monsters in order
for i, t in enumerate(times):
if t < i:
return i
return len(times)
This greedy validation works as follows:
- We iterate through the sorted deadlines with their indices
- At minute
i
, we attempt to eliminate thei-th
monster (0-indexed) - If
times[i] < i
, it means the monster's deadline is before minutei
- The monster would reach the city before we can shoot it
- We return
i
as the maximum number of monsters we can eliminate
- If we successfully check all monsters without returning early, it means we can eliminate all
n
monsters
Example walkthrough:
Let's say dist = [3, 5, 7]
and speed = [1, 1, 2]
:
- Monster 0: deadline =
(3-1)//1 = 2
- Monster 1: deadline =
(5-1)//1 = 4
- Monster 2: deadline =
(7-1)//2 = 3
After sorting: times = [2, 3, 4]
Checking:
- Minute 0: Can we shoot monster with deadline 2? Yes (2 >= 0) โ
- Minute 1: Can we shoot monster with deadline 3? Yes (3 >= 1) โ
- Minute 2: Can we shoot monster with deadline 4? Yes (4 >= 2) โ
Result: We can eliminate all 3 monsters.
The algorithm has a time complexity of O(n log n)
due to sorting, and space complexity of O(n)
for storing the deadlines.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the solution works:
Example: dist = [4, 2, 8]
, speed = [2, 1, 2]
Step 1: Calculate elimination deadlines for each monster
-
Monster 0: distance = 4, speed = 2
- Arrival time: 4/2 = 2 minutes
- Latest elimination time: (4-1)//2 = 3//2 = 1 minute
-
Monster 1: distance = 2, speed = 1
- Arrival time: 2/1 = 2 minutes
- Latest elimination time: (2-1)//1 = 1//1 = 1 minute
-
Monster 2: distance = 8, speed = 2
- Arrival time: 8/2 = 4 minutes
- Latest elimination time: (8-1)//2 = 7//2 = 3 minutes
Step 2: Sort the deadlines
Deadlines: [1, 1, 3] โ Already sorted in this case
Step 3: Check if we can eliminate each monster at its scheduled minute
-
Minute 0: Try to eliminate first monster (deadline = 1)
- Can we shoot it at minute 0? Check: 1 >= 0? โ Yes
-
Minute 1: Try to eliminate second monster (deadline = 1)
- Can we shoot it at minute 1? Check: 1 >= 1? โ Yes
-
Minute 2: Try to eliminate third monster (deadline = 3)
- Can we shoot it at minute 2? Check: 3 >= 2? โ Yes
Result: We can eliminate all 3 monsters!
Counter-example to show when we can't eliminate all:
dist = [1, 1, 3]
, speed = [1, 1, 1]
- Monster 0: (1-1)//1 = 0 (must shoot at minute 0)
- Monster 1: (1-1)//1 = 0 (must shoot at minute 0)
- Monster 2: (3-1)//1 = 2 (must shoot by minute 2)
Sorted deadlines: [0, 0, 2]
Checking:
- Minute 0: deadline 0 >= 0? โ Yes
- Minute 1: deadline 0 >= 1? โ No! (0 < 1)
We can only eliminate 1 monster before losing the game.
Solution Implementation
1class Solution:
2 def eliminateMaximum(self, dist: List[int], speed: List[int]) -> int:
3 # Calculate the time (in minutes) each monster takes to reach the city
4 # Using (distance - 1) // speed to get the last full minute before arrival
5 arrival_times = sorted((distance - 1) // monster_speed
6 for distance, monster_speed in zip(dist, speed))
7
8 # Check if we can eliminate all monsters before they reach the city
9 for minute, arrival_time in enumerate(arrival_times):
10 # At minute i, we've eliminated i monsters (0-indexed)
11 # If a monster arrives before we can eliminate it, return count
12 if arrival_time < minute:
13 return minute
14
15 # If we can eliminate all monsters, return the total count
16 return len(arrival_times)
17
1class Solution {
2 public int eliminateMaximum(int[] dist, int[] speed) {
3 int n = dist.length;
4
5 // Calculate arrival time for each monster (in minutes)
6 // Using ceiling division: (dist[i] - 1) / speed[i] gives us the last full minute before arrival
7 int[] arrivalTimes = new int[n];
8 for (int i = 0; i < n; i++) {
9 arrivalTimes[i] = (dist[i] - 1) / speed[i];
10 }
11
12 // Sort monsters by their arrival time (earliest first)
13 Arrays.sort(arrivalTimes);
14
15 // Check if we can eliminate all monsters before they arrive
16 // At minute i, we can eliminate the (i+1)th monster
17 for (int i = 0; i < n; i++) {
18 // If monster arrives before or at minute i, but we can only shoot it at minute i
19 // This means the monster reaches us before we can eliminate it
20 if (arrivalTimes[i] < i) {
21 return i; // Return the number of monsters eliminated before losing
22 }
23 }
24
25 // All monsters can be eliminated successfully
26 return n;
27 }
28}
29
1class Solution {
2public:
3 int eliminateMaximum(vector<int>& dist, vector<int>& speed) {
4 // Get the number of monsters
5 int n = dist.size();
6
7 // Calculate arrival time for each monster (in minutes)
8 // Using (distance - 1) / speed to get the last minute before arrival
9 vector<int> arrivalTimes;
10 for (int i = 0; i < n; ++i) {
11 // Calculate when each monster will arrive (rounded down)
12 // The monster arrives when it covers the distance
13 // We use (dist[i] - 1) / speed[i] to get the last full minute before arrival
14 arrivalTimes.push_back((dist[i] - 1) / speed[i]);
15 }
16
17 // Sort monsters by their arrival time (earliest first)
18 sort(arrivalTimes.begin(), arrivalTimes.end());
19
20 // Check if we can eliminate all monsters before they arrive
21 // At minute i, we can eliminate the (i+1)th monster
22 for (int i = 0; i < n; ++i) {
23 // If monster arrives before we can eliminate it, return count
24 if (arrivalTimes[i] < i) {
25 return i;
26 }
27 }
28
29 // All monsters can be eliminated
30 return n;
31 }
32};
33
1/**
2 * Calculates the maximum number of monsters that can be eliminated before any reaches the city.
3 * Each monster can be eliminated with one shot per minute starting from minute 0.
4 *
5 * @param dist - Array of initial distances of monsters from the city
6 * @param speed - Array of speeds at which monsters approach the city
7 * @returns The maximum number of monsters that can be eliminated
8 */
9function eliminateMaximum(dist: number[], speed: number[]): number {
10 const monsterCount: number = dist.length;
11
12 // Calculate the time (in minutes) when each monster reaches the city
13 // Using Math.floor((dist[i] - 1) / speed[i]) to get the last full minute before arrival
14 const arrivalTimes: number[] = Array(monsterCount).fill(0);
15
16 for (let i = 0; i < monsterCount; ++i) {
17 // Calculate the last minute we can eliminate this monster
18 // Subtracting 1 from distance ensures we get the last safe minute
19 arrivalTimes[i] = Math.floor((dist[i] - 1) / speed[i]);
20 }
21
22 // Sort arrival times in ascending order to prioritize monsters arriving sooner
23 arrivalTimes.sort((a, b) => a - b);
24
25 // Check if we can eliminate each monster in time
26 for (let minute = 0; minute < monsterCount; ++minute) {
27 // If a monster arrives before we can eliminate it (at minute i), return count
28 if (arrivalTimes[minute] < minute) {
29 return minute;
30 }
31 }
32
33 // All monsters can be eliminated
34 return monsterCount;
35}
36
Time and Space Complexity
Time Complexity: O(n ร log n)
The time complexity is dominated by the sorting operation. Breaking down the algorithm:
- Creating the
times
list using list comprehension withzip
:O(n)
, wheren
is the length of the input arrays - Sorting the
times
list:O(n ร log n)
- Iterating through the sorted list with enumerate:
O(n)
The overall time complexity is O(n) + O(n ร log n) + O(n) = O(n ร log n)
.
Space Complexity: O(n)
The space complexity analysis:
- The
times
list storesn
elements (arrival times for each monster):O(n)
- The sorting operation may use additional space depending on the implementation (Python's Timsort uses up to
O(n)
auxiliary space in worst case) - The enumerate iterator and loop variables use constant space:
O(1)
Therefore, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Arrival Time Calculation
The Pitfall:
A common mistake is calculating the arrival time as simply dist[i] / speed[i]
or dist[i] // speed[i]
. This fails to account for the edge case where a monster arrives at the exact moment your weapon becomes ready.
Why it's wrong:
- If a monster arrives at exactly minute 3.0, and your weapon becomes ready at minute 3.0, the monster reaches the city before you can shoot
- Using
dist[i] // speed[i]
would suggest you can eliminate it at minute 3, but that's too late
The Solution:
Use (dist[i] - 1) // speed[i]
to get the last safe minute to eliminate the monster. This ensures you shoot before the monster arrives.
# Wrong
arrival_times = sorted(dist[i] // speed[i] for i in range(len(dist)))
# Correct
arrival_times = sorted((dist[i] - 1) // speed[i] for i in range(len(dist)))
2. Not Sorting the Arrival Times
The Pitfall: Processing monsters in their original order instead of by urgency (arrival time).
Why it's wrong: You might waste early shots on monsters that arrive later, missing the opportunity to eliminate more urgent threats.
Example:
- Monster A: arrives at minute 5
- Monster B: arrives at minute 1
- If you shoot A first (minute 0), B reaches the city at minute 1 before you can shoot again
The Solution: Always sort monsters by their arrival deadlines to prioritize the most urgent threats first.
3. Off-by-One Error in Elimination Check
The Pitfall:
Using <=
instead of <
when checking if we can eliminate a monster in time.
# Wrong - this would allow shooting after the monster arrives if arrival_time <= minute: return minute # Correct - we can only shoot if the monster hasn't arrived yet if arrival_time < minute: return minute
Why it matters:
At minute i
, we're about to shoot our (i+1)
-th monster. If the monster's deadline equals i
, it means the monster arrives exactly when we're trying to shoot, which is too late.
4. Using Floating-Point Division
The Pitfall:
Using regular division /
instead of integer division //
can lead to floating-point precision issues.
# Potentially problematic with floating-point arithmetic
arrival_time = int((dist[i] - 1) / speed[i])
# Better - uses integer division directly
arrival_time = (dist[i] - 1) // speed[i]
The Solution:
Stick to integer division //
to avoid any floating-point precision errors and ensure consistent results.
Which of the following uses divide and conquer strategy?
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Donโt Miss This!