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 (
iis even): Updatea1by addingstep, and if count is odd, also updatean - When going right-to-left (
iis odd): Updateanby 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 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 401class 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} 461class 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}; 511function 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} 48Solution 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 5-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=7is 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=3is 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
a1andanequal4 - 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.
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:
cntis halved (cnt >>= 1), which meanscntis 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:
a1andanto track the first and last elementsias a counter for iterationsstepto track the step sizecntto 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.
What does the following code do?
1def f(arr1, arr2): 2 i, j = 0, 0 3 new_arr = [] 4 while i < len(arr1) and j < len(arr2): 5 if arr1[i] < arr2[j]: 6 new_arr.append(arr1[i]) 7 i += 1 8 else: 9 new_arr.append(arr2[j]) 10 j += 1 11 new_arr.extend(arr1[i:]) 12 new_arr.extend(arr2[j:]) 13 return new_arr 141public static List<Integer> f(int[] arr1, int[] arr2) { 2 int i = 0, j = 0; 3 List<Integer> newArr = new ArrayList<>(); 4 5 while (i < arr1.length && j < arr2.length) { 6 if (arr1[i] < arr2[j]) { 7 newArr.add(arr1[i]); 8 i++; 9 } else { 10 newArr.add(arr2[j]); 11 j++; 12 } 13 } 14 15 while (i < arr1.length) { 16 newArr.add(arr1[i]); 17 i++; 18 } 19 20 while (j < arr2.length) { 21 newArr.add(arr2[j]); 22 j++; 23 } 24 25 return newArr; 26} 271function f(arr1, arr2) { 2 let i = 0, j = 0; 3 let newArr = []; 4 5 while (i < arr1.length && j < arr2.length) { 6 if (arr1[i] < arr2[j]) { 7 newArr.push(arr1[i]); 8 i++; 9 } else { 10 newArr.push(arr2[j]); 11 j++; 12 } 13 } 14 15 while (i < arr1.length) { 16 newArr.push(arr1[i]); 17 i++; 18 } 19 20 while (j < arr2.length) { 21 newArr.push(arr2[j]); 22 j++; 23 } 24 25 return newArr; 26} 27Recommended Readings
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
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!