390. Elimination Game
Problem Description
You start with a list containing all integers from 1 to n in strictly increasing order: [1, 2, 3, ..., n]
.
You need to repeatedly eliminate numbers from this list following a specific pattern until only one number remains:
-
First elimination (left to right): Start from the leftmost number, remove it, then skip the next number, remove the following one, skip the next, and so on until you reach the end of the list.
-
Second elimination (right to left): Now work from the rightmost number of the remaining list, remove it, skip the previous number, remove the one before that, skip the previous, and continue this pattern until you reach the beginning.
-
Continue alternating: Keep alternating between left-to-right and right-to-left eliminations, always removing every other number starting from the appropriate end.
-
Stop when one remains: Continue this process until only a single number is left in the list.
For example, if n = 9
:
- Initial:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
- After 1st elimination (L→R):
[2, 4, 6, 8]
(removed 1, 3, 5, 7, 9) - After 2nd elimination (R→L):
[2, 6]
(removed 8, 4) - After 3rd elimination (L→R):
[6]
(removed 2) - Result:
6
Given an integer n
, return the last remaining number after applying this elimination process.
Intuition
Instead of actually removing elements from a list, we can track the first and last remaining elements after each elimination round. The key insight is that we don't need to maintain the entire list - we only need to know the boundaries and the pattern of elimination.
After each elimination round:
- The number of remaining elements is halved (since we remove every other element)
- The distance between consecutive remaining elements doubles
- The first and last elements change based on the elimination direction and whether we have an odd or even count
Let's think about what happens to the boundaries:
- Left-to-right elimination: The first element always gets removed, so the new first element moves forward by the current step size. The last element only changes if we have an odd count (since we'd remove it in that case).
- Right-to-left elimination: The last element always gets removed, so it moves backward by the step size. The first element only changes if we have an odd count.
We can track:
a1
: The first element in the current remaining sequencean
: The last element in the current remaining sequencestep
: The distance between consecutive elements (doubles after each round)cnt
: The count of remaining elements (halves after each round)i
: Round counter to determine the direction (even = left-to-right, odd = right-to-left)
The algorithm updates these values after each elimination:
- When going left-to-right (
i
is even): Updatea1
by addingstep
, and if count is odd, also updatean
- When going right-to-left (
i
is odd): Updatean
by subtractingstep
, and if count is odd, also updatea1
We continue until only one element remains (cnt == 1
), at which point a1
and an
converge to the same value - our answer.
Solution Approach
The solution implements the boundary tracking approach we discussed in the intuition. Here's how the algorithm works:
Initialize variables:
a1 = 1
: First element of the current sequencean = n
: Last element of the current sequencei = 0
: Round counter (even means left-to-right, odd means right-to-left)step = 1
: Distance between consecutive elements in the current sequencecnt = n
: Number of elements remaining
Main loop (while cnt > 1
):
For each elimination round, we update the boundaries based on the direction:
- Right-to-left elimination (
i % 2 == 1
):- The last element is always removed:
an -= step
- If the count is odd, the first element also changes:
if cnt % 2: a1 += step
- The last element is always removed:
- Left-to-right elimination (
i % 2 == 0
):- The first element is always removed:
a1 += step
- If the count is odd, the last element also changes:
if cnt % 2: an -= step
- The first element is always removed:
After updating boundaries:
- Halve the count:
cnt >>= 1
(equivalent tocnt = cnt // 2
) - Double the step size:
step <<= 1
(equivalent tostep = step * 2
) - Increment round counter:
i += 1
Return the result:
When the loop ends (cnt == 1
), both a1
and an
point to the same value - the last remaining number. The solution returns a1
.
Example walkthrough with n = 9
:
- Initial:
a1=1, an=9, step=1, cnt=9
- Round 0 (L→R):
a1=2, an=8, step=2, cnt=4
- Round 1 (R→L):
a1=2, an=6, step=4, cnt=2
- Round 2 (L→R):
a1=6, an=6, step=8, cnt=1
- Return:
6
The time complexity is O(log n)
since we halve the count in each iteration, and space complexity is O(1)
as we only use a constant amount of variables.
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 the solution with n = 7
to see how the boundary tracking approach works:
Initial Setup:
- List:
[1, 2, 3, 4, 5, 6, 7]
- Variables:
a1=1, an=7, step=1, cnt=7, i=0
Round 0 (Left-to-Right, i=0):
- We're eliminating from the left, removing every other element starting with the first
- Elements removed: 1, 3, 5, 7
- Remaining:
[2, 4, 6]
- Updates:
a1 = 1 + 1 = 2
(first element always moves forward by step)- Since
cnt=7
is odd:an = 7 - 1 = 6
(last element also changes) cnt = 7 >> 1 = 3
(halve the count)step = 1 << 1 = 2
(double the step)i = 1
Round 1 (Right-to-Left, i=1):
- We're eliminating from the right, removing every other element starting with the last
- Elements removed: 6, 2
- Remaining:
[4]
- Updates:
an = 6 - 2 = 4
(last element always moves backward by step)- Since
cnt=3
is odd:a1 = 2 + 2 = 4
(first element also changes) cnt = 3 >> 1 = 1
(halve the count)step = 2 << 1 = 4
(double the step)i = 2
Result:
cnt = 1
, so we exit the loop- Both
a1
andan
equal4
- Return
4
The key insight is that we never actually maintain the list - we just track where the boundaries are after each elimination. The step
variable tells us the spacing between remaining elements, and we update boundaries based on the elimination direction and whether we have an odd count of elements.
Solution Implementation
1class Solution:
2 def lastRemaining(self, n: int) -> int:
3 # Initialize the first and last elements of the current sequence
4 first_element = 1
5 last_element = n
6
7 # Track iteration count (0-indexed), step size between elements, and remaining count
8 iteration = 0
9 step_size = 1
10 remaining_count = n
11
12 # Continue until only one element remains
13 while remaining_count > 1:
14 if iteration % 2 == 1:
15 # Odd iteration: removing from right to left
16 # Last element always gets removed when going right to left
17 last_element -= step_size
18
19 # First element only changes if we have odd number of elements
20 if remaining_count % 2 == 1:
21 first_element += step_size
22 else:
23 # Even iteration: removing from left to right
24 # First element always gets removed when going left to right
25 first_element += step_size
26
27 # Last element only changes if we have odd number of elements
28 if remaining_count % 2 == 1:
29 last_element -= step_size
30
31 # After each elimination round:
32 # - Half the elements are removed
33 # - Double the step size between remaining elements
34 remaining_count //= 2
35 step_size *= 2
36 iteration += 1
37
38 # When only one element remains, first_element points to it
39 return first_element
40
1class Solution {
2 public int lastRemaining(int n) {
3 // Track the first and last elements in the remaining sequence
4 int firstElement = 1;
5 int lastElement = n;
6
7 // Step size between consecutive elements in the current sequence
8 int stepSize = 1;
9
10 // Keep track of remaining count and current round
11 int remainingCount = n;
12 int round = 0;
13
14 // Continue elimination until only one element remains
15 while (remainingCount > 1) {
16 if (round % 2 == 0) {
17 // Even round: eliminate from left to right
18 // First element always moves forward
19 firstElement += stepSize;
20
21 // Last element moves backward only if count is odd
22 if (remainingCount % 2 == 1) {
23 lastElement -= stepSize;
24 }
25 } else {
26 // Odd round: eliminate from right to left
27 // Last element always moves backward
28 lastElement -= stepSize;
29
30 // First element moves forward only if count is odd
31 if (remainingCount % 2 == 1) {
32 firstElement += stepSize;
33 }
34 }
35
36 // Update for next round
37 remainingCount /= 2; // Half of the elements remain after each round
38 stepSize *= 2; // Double the step size between remaining elements
39 round++;
40 }
41
42 // When only one element remains, first and last converge to the same value
43 return firstElement;
44 }
45}
46
1class Solution {
2public:
3 int lastRemaining(int n) {
4 // Track the first and last elements of the remaining sequence
5 int firstElement = 1;
6 int lastElement = n;
7
8 // Step size between consecutive elements in the current sequence
9 int stepSize = 1;
10
11 // Iteration counter (even = left-to-right, odd = right-to-left)
12 int iteration = 0;
13
14 // Count of remaining elements
15 int remainingCount = n;
16
17 // Continue until only one element remains
18 while (remainingCount > 1) {
19 if (iteration % 2 == 0) {
20 // Left-to-right elimination
21 // Always update the first element
22 firstElement += stepSize;
23
24 // Update last element only if count is odd
25 // (the last element gets eliminated when count is odd)
26 if (remainingCount % 2 == 1) {
27 lastElement -= stepSize;
28 }
29 } else {
30 // Right-to-left elimination
31 // Always update the last element
32 lastElement -= stepSize;
33
34 // Update first element only if count is odd
35 // (the first element gets eliminated when count is odd)
36 if (remainingCount % 2 == 1) {
37 firstElement += stepSize;
38 }
39 }
40
41 // Prepare for next iteration
42 remainingCount /= 2; // Half the elements remain after each pass
43 stepSize *= 2; // Double the step size between elements
44 iteration++; // Move to next iteration
45 }
46
47 // When only one element remains, firstElement equals lastElement
48 return firstElement;
49 }
50};
51
1function lastRemaining(n: number): number {
2 // Track the first and last elements of the remaining sequence
3 let firstElement: number = 1;
4 let lastElement: number = n;
5
6 // Step size between consecutive elements in the current sequence
7 let stepSize: number = 1;
8
9 // Iteration counter (even = left-to-right, odd = right-to-left)
10 let iteration: number = 0;
11
12 // Count of remaining elements
13 let remainingCount: number = n;
14
15 // Continue until only one element remains
16 while (remainingCount > 1) {
17 if (iteration % 2 === 0) {
18 // Left-to-right elimination
19 // Always update the first element
20 firstElement += stepSize;
21
22 // Update last element only if count is odd
23 // (the last element gets eliminated when count is odd)
24 if (remainingCount % 2 === 1) {
25 lastElement -= stepSize;
26 }
27 } else {
28 // Right-to-left elimination
29 // Always update the last element
30 lastElement -= stepSize;
31
32 // Update first element only if count is odd
33 // (the first element gets eliminated when count is odd)
34 if (remainingCount % 2 === 1) {
35 firstElement += stepSize;
36 }
37 }
38
39 // Prepare for next iteration
40 remainingCount = Math.floor(remainingCount / 2); // Half the elements remain after each pass
41 stepSize *= 2; // Double the step size between elements
42 iteration++; // Move to next iteration
43 }
44
45 // When only one element remains, firstElement equals lastElement
46 return firstElement;
47}
48
Time and Space Complexity
Time Complexity: O(log n)
The algorithm uses a while loop that continues as long as cnt > 1
. In each iteration:
cnt
is halved (cnt >>= 1
), which meanscnt
is divided by 2- Starting from
n
, the value is repeatedly halved until it reaches 1 - This results in approximately
log₂(n)
iterations
Each iteration performs only constant time operations (arithmetic operations, comparisons, and bit shifts), so the overall time complexity is O(log n)
.
Space Complexity: O(1)
The algorithm uses only a fixed number of variables:
a1
andan
to track the first and last elementsi
as a counter for iterationsstep
to track the step sizecnt
to track the remaining count
No additional data structures are used, and the space usage doesn't depend on the input size n
. Therefore, the space complexity is constant, O(1)
.
Common Pitfalls
1. Incorrect Boundary Update Logic
The most common mistake is incorrectly determining when to update the first or last element based on the direction and count parity.
Pitfall Example:
# INCORRECT: Updating wrong boundary or wrong condition if iteration % 2 == 0: # Left to right first_element += step_size last_element -= step_size # Wrong! Should only update if odd count
Why it's wrong: When eliminating from left to right with an even count, the last element doesn't change. For example, [1,2,3,4]
→ [2,4]
. The last element remains at position 4.
Correct Approach:
if iteration % 2 == 0: # Left to right first_element += step_size if remaining_count % 2 == 1: # Only update last if odd count last_element -= step_size
2. Off-by-One Error in Step Size Update
Another pitfall is updating the step size at the wrong time or by the wrong amount.
Pitfall Example:
# INCORRECT: Updating step before boundary updates while remaining_count > 1: step_size *= 2 # Wrong timing! if iteration % 2 == 1: last_element -= step_size # Now using wrong step size # ...
Correct Approach: Always update boundaries first using the current step size, then update the step size for the next iteration:
while remaining_count > 1: # First: Update boundaries with current step if iteration % 2 == 1: last_element -= step_size # ... # Then: Update step for next iteration step_size *= 2 remaining_count //= 2 iteration += 1
3. Confusion Between Direction and Iteration Number
It's easy to mix up which iteration number corresponds to which direction.
Pitfall Example:
# INCORRECT: Confusing iteration 0 as right-to-left if iteration % 2 == 0: # Thinking this is right-to-left last_element -= step_size # Wrong direction logic
Remember:
- Iteration 0 (even) = Left to Right (removes first element always)
- Iteration 1 (odd) = Right to Left (removes last element always)
Debugging Tip: Add comments or use descriptive variable names:
is_left_to_right = (iteration % 2 == 0) if is_left_to_right: first_element += step_size # ...
4. Not Handling Edge Cases
Failing to consider when n = 1
or very small values.
Solution: The algorithm naturally handles n = 1
since the while loop condition remaining_count > 1
prevents any iterations, correctly returning 1. However, always verify edge cases during implementation.
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
Recommended Readings
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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!