Facebook Pixel

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:

  1. 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.

  2. 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.

  3. Continue alternating: Keep alternating between left-to-right and right-to-left eliminations, always removing every other number starting from the appropriate end.

  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 sequence
  • an: The last element in the current remaining sequence
  • step: 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): Update a1 by adding step, and if count is odd, also update an
  • When going right-to-left (i is odd): Update an by subtracting step, and if count is odd, also update a1

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 sequence
  • an = n: Last element of the current sequence
  • i = 0: Round counter (even means left-to-right, odd means right-to-left)
  • step = 1: Distance between consecutive elements in the current sequence
  • cnt = n: Number of elements remaining

Main loop (while cnt > 1):

For each elimination round, we update the boundaries based on the direction:

  1. 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
  2. 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

After updating boundaries:

  • Halve the count: cnt >>= 1 (equivalent to cnt = cnt // 2)
  • Double the step size: step <<= 1 (equivalent to step = 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 Evaluator

Example 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 and an equal 4
  • 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 means cnt 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 and an to track the first and last elements
  • i as a counter for iterations
  • step to track the step size
  • cnt 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More