2293. Min Max Game
Problem Description
You are given an integer array nums
whose length is a power of 2 (like 1, 2, 4, 8, 16, etc.). The array is 0-indexed.
You need to repeatedly apply a specific algorithm to transform the array until only one number remains:
The Algorithm Process:
-
Check the current length
n
ofnums
. Ifn == 1
, stop - you've found your answer. Otherwise, continue to step 2. -
Create a new array
newNums
with half the length (n/2
). -
Fill the new array according to these rules:
- For even indices
i
(0, 2, 4, ...): SetnewNums[i] = min(nums[2*i], nums[2*i + 1])
- For odd indices
i
(1, 3, 5, ...): SetnewNums[i] = max(nums[2*i], nums[2*i + 1])
- For even indices
-
Replace
nums
withnewNums
. -
Go back to step 1 and repeat the entire process.
Example walkthrough:
If nums = [1, 3, 5, 2, 4, 8, 2, 2]
(length = 8):
-
Round 1: Create array of length 4
newNums[0]
(even):min(1, 3) = 1
newNums[1]
(odd):max(5, 2) = 5
newNums[2]
(even):min(4, 8) = 4
newNums[3]
(odd):max(2, 2) = 2
- Result:
[1, 5, 4, 2]
-
Round 2: Create array of length 2
newNums[0]
(even):min(1, 5) = 1
newNums[1]
(odd):max(4, 2) = 4
- Result:
[1, 4]
-
Round 3: Create array of length 1
newNums[0]
(even):min(1, 4) = 1
- Result:
[1]
The final answer is 1
.
The task is to return the last remaining number after the algorithm completes.
Intuition
The problem asks us to simulate a specific reduction process, so the most straightforward approach is to directly implement what the problem describes - a simulation.
The key insight is recognizing that we're essentially building a tournament-like structure where pairs of elements compete, and the winner (either min or max based on position) advances to the next round. The array shrinks by half in each iteration until we're left with a single element.
Looking at the pattern:
- We take consecutive pairs from the current array:
(nums[0], nums[1])
,(nums[2], nums[3])
, etc. - For the first pair, we take the minimum; for the second pair, we take the maximum; then minimum again, and so on
- This creates a new array of half the size
- We repeat until only one element remains
Since we're always reducing the array size by half and only need the values from the previous iteration to compute the current one, we can optimize space by reusing the original array. Instead of creating a new array each time, we can overwrite the first half of the current array with the new values.
The implementation becomes elegant with bit operations:
n >>= 1
is equivalent ton = n // 2
(halving the size)i << 1
is equivalent toi * 2
(getting the first element of pair i)i << 1 | 1
is equivalent toi * 2 + 1
(getting the second element of pair i)
The alternating min/max pattern is naturally handled by checking if the index i
is even or odd using i % 2 == 0
.
Solution Approach
The solution implements a direct simulation of the algorithm described in the problem. Instead of creating new arrays in each iteration, we optimize by reusing the original array and overwriting values in-place.
Implementation Steps:
-
Initialize the length: Start with
n = len(nums)
, which represents the current working size of the array. -
Main loop: Continue the process while
n > 1
:- Halve the working size: Use
n >>= 1
(bit shift right by 1) to dividen
by 2 - Process pairs: For each index
i
from 0 ton-1
:- Extract the pair of elements:
a = nums[i << 1]
(equivalent tonums[2*i]
)b = nums[i << 1 | 1]
(equivalent tonums[2*i + 1]
)
- Apply the min/max rule:
- If
i
is even:nums[i] = min(a, b)
- If
i
is odd:nums[i] = max(a, b)
- If
- Extract the pair of elements:
- The result is stored directly in the first half of the array
- Halve the working size: Use
-
Return the result: After the loop completes (when
n == 1
), returnnums[0]
which contains the final answer.
Key Optimizations:
- Space optimization: No additional arrays are created. We reuse the original array by storing new values in the first half.
- Bit operations:
n >>= 1
for division by 2i << 1
for multiplication by 2i << 1 | 1
for2*i + 1
These bit operations are faster than regular arithmetic operations.
Example trace with nums = [70, 38, 21, 22]
:
- Initial:
n = 4
,nums = [70, 38, 21, 22]
- Iteration 1:
n = 2
i = 0
(even):nums[0] = min(70, 38) = 38
i = 1
(odd):nums[1] = max(21, 22) = 22
- Array becomes:
[38, 22, 21, 22]
(only first 2 elements matter)
- Iteration 2:
n = 1
i = 0
(even):nums[0] = min(38, 22) = 22
- Array becomes:
[22, ...]
(only first element matters)
- Return
nums[0] = 22
The time complexity is O(n)
where n
is the length of the input array, as we process each element once in each round, and there are log n
rounds total. The space complexity is O(1)
as we modify the array in-place.
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 trace through the algorithm with nums = [9, 3, 2, 6]
(length = 4):
Initial State:
n = 4
nums = [9, 3, 2, 6]
First Iteration (n becomes 2):
- We need to process 2 pairs:
(9,3)
and(2,6)
- For
i = 0
(even index):- Extract pair:
a = nums[0] = 9
,b = nums[1] = 3
- Apply min rule:
nums[0] = min(9, 3) = 3
- Extract pair:
- For
i = 1
(odd index):- Extract pair:
a = nums[2] = 2
,b = nums[3] = 6
- Apply max rule:
nums[1] = max(2, 6) = 6
- Extract pair:
- Array after iteration:
[3, 6, 2, 6]
(only first 2 elements matter)
Second Iteration (n becomes 1):
- We need to process 1 pair:
(3,6)
- For
i = 0
(even index):- Extract pair:
a = nums[0] = 3
,b = nums[1] = 6
- Apply min rule:
nums[0] = min(3, 6) = 3
- Extract pair:
- Array after iteration:
[3, 6, 2, 6]
(only first element matters)
Result: Since n = 1
, we stop and return nums[0] = 3
The key insight is how we reuse the array: in each iteration, we read from positions 2*i
and 2*i+1
, but write to position i
. This allows us to overwrite the array in-place without losing the data we need for the current iteration.
Solution Implementation
1class Solution:
2 def minMaxGame(self, nums: List[int]) -> int:
3 """
4 Simulates a min-max game where pairs of elements are repeatedly combined
5 until only one element remains.
6
7 Args:
8 nums: List of integers to process
9
10 Returns:
11 The final remaining integer after all operations
12 """
13 n = len(nums)
14
15 # Continue until only one element remains
16 while n > 1:
17 # Halve the number of elements for next round
18 n //= 2
19
20 # Process pairs of elements
21 for i in range(n):
22 # Get the pair of elements at positions 2*i and 2*i+1
23 left_value = nums[2 * i]
24 right_value = nums[2 * i + 1]
25
26 # Apply min operation for even indices, max for odd indices
27 if i % 2 == 0:
28 nums[i] = min(left_value, right_value)
29 else:
30 nums[i] = max(left_value, right_value)
31
32 # Return the final remaining element
33 return nums[0]
34
1class Solution {
2 public int minMaxGame(int[] nums) {
3 // Continue until only one element remains
4 for (int currentLength = nums.length; currentLength > 1;) {
5 // Halve the current length for the next iteration
6 currentLength = currentLength >> 1; // Equivalent to currentLength / 2
7
8 // Process pairs of elements and store results in the first half of the array
9 for (int i = 0; i < currentLength; i++) {
10 // Get the pair of elements at positions 2*i and 2*i+1
11 int leftElement = nums[i * 2];
12 int rightElement = nums[i * 2 + 1];
13
14 // Apply min/max rule based on whether index i is even or odd
15 if (i % 2 == 0) {
16 // Even index: take minimum of the pair
17 nums[i] = Math.min(leftElement, rightElement);
18 } else {
19 // Odd index: take maximum of the pair
20 nums[i] = Math.max(leftElement, rightElement);
21 }
22 }
23 }
24
25 // Return the final remaining element
26 return nums[0];
27 }
28}
29
1class Solution {
2public:
3 int minMaxGame(vector<int>& nums) {
4 // Continue the game until only one element remains
5 for (int currentSize = nums.size(); currentSize > 1; ) {
6 // Halve the current size for the next round
7 currentSize >>= 1;
8
9 // Process each pair of elements in the current round
10 for (int index = 0; index < currentSize; ++index) {
11 // Get the pair of elements at positions 2*index and 2*index+1
12 int leftElement = nums[index << 1];
13 int rightElement = nums[index << 1 | 1];
14
15 // Apply min-max rule based on index parity:
16 // - Even index: take minimum of the pair
17 // - Odd index: take maximum of the pair
18 nums[index] = (index % 2 == 0) ? min(leftElement, rightElement)
19 : max(leftElement, rightElement);
20 }
21 }
22
23 // Return the final remaining element
24 return nums[0];
25 }
26};
27
1/**
2 * Performs a min-max game on an array of numbers.
3 * In each round, pairs of elements are combined using min for even indices and max for odd indices,
4 * until only one element remains.
5 *
6 * @param nums - The input array of numbers
7 * @returns The final remaining number after all rounds
8 */
9function minMaxGame(nums: number[]): number {
10 // Continue the game while more than one element remains
11 let currentLength: number = nums.length;
12
13 while (currentLength > 1) {
14 // Halve the current length for the next round
15 currentLength = currentLength >> 1;
16
17 // Process each pair of elements
18 for (let index: number = 0; index < currentLength; index++) {
19 // Get the pair of elements to compare
20 const leftElement: number = nums[index << 1]; // Element at position index * 2
21 const rightElement: number = nums[(index << 1) | 1]; // Element at position index * 2 + 1
22
23 // Apply min operation for even indices, max operation for odd indices
24 if (index % 2 === 0) {
25 nums[index] = Math.min(leftElement, rightElement);
26 } else {
27 nums[index] = Math.max(leftElement, rightElement);
28 }
29 }
30 }
31
32 // Return the final remaining element
33 return nums[0];
34}
35
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array nums
.
To analyze this, observe that in each iteration of the while loop, the array size is halved (n >>= 1
). The inner for loop processes n/2
elements in the first iteration, n/4
in the second, n/8
in the third, and so on. The total number of operations is n/2 + n/4 + n/8 + ... + 1
, which forms a geometric series that sums to n - 1
. Therefore, the overall time complexity is O(n)
.
The space complexity is O(1)
as the algorithm modifies the input array in-place without using any additional data structures that scale with the input size. Only a constant amount of extra variables (n
, i
, a
, b
) are used regardless of the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Index Mapping When Processing Pairs
A common mistake is incorrectly mapping the indices when extracting pairs from the original array. Developers often confuse the relationship between the new array index i
and the corresponding pair indices 2*i
and 2*i + 1
.
Incorrect Implementation:
# Wrong: Using i and i+1 instead of 2*i and 2*i+1
for i in range(n):
if i % 2 == 0:
nums[i] = min(nums[i], nums[i + 1]) # Wrong!
else:
nums[i] = max(nums[i], nums[i + 1]) # Wrong!
Correct Implementation:
for i in range(n):
left_value = nums[2 * i] # Correct: 2*i
right_value = nums[2 * i + 1] # Correct: 2*i + 1
if i % 2 == 0:
nums[i] = min(left_value, right_value)
else:
nums[i] = max(left_value, right_value)
2. Overwriting Values Before Using Them
When modifying the array in-place, there's a risk of overwriting values that are still needed for subsequent calculations within the same iteration.
Problematic Scenario:
If you don't carefully consider the order of operations, you might overwrite nums[2]
and nums[3]
while processing index 1, but these values are still needed when processing subsequent indices.
Solution:
Process all indices in order from 0 to n-1, and ensure that when writing to nums[i]
, you're reading from nums[2*i]
and nums[2*i + 1]
. Since i < 2*i
always holds true, you won't overwrite values before reading them.
3. Forgetting to Update the Working Length
Another pitfall is forgetting to properly update the working length n
after each iteration, or updating it incorrectly.
Incorrect:
while n > 1:
for i in range(n // 2): # Using n//2 here
# process pairs
n = n // 2 # Easy to forget this line or place it incorrectly
Correct:
while n > 1:
n //= 2 # Update n first
for i in range(n): # Then use the updated n
# process pairs
4. Edge Case: Single Element Array
While the problem states the array length is a power of 2 (including 1), forgetting to handle the case where the initial array has only one element can cause issues.
Solution:
The while loop condition while n > 1
naturally handles this case by not entering the loop at all when n = 1
, directly returning nums[0]
.
5. Integer Division vs Bit Shifting Confusion
While both n // 2
and n >> 1
work correctly for positive integers, mixing different styles or using them incorrectly can lead to readability issues or subtle bugs in other contexts.
Best Practice:
Choose one style and stick with it consistently throughout your solution. Integer division (//
) is more readable, while bit shifting (>>
) might be marginally faster but less intuitive.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
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
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
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!