Facebook Pixel

2293. Min Max Game

EasyArraySimulation
Leetcode Link

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:

  1. Check the current length n of nums. If n == 1, stop - you've found your answer. Otherwise, continue to step 2.

  2. Create a new array newNums with half the length (n/2).

  3. Fill the new array according to these rules:

    • For even indices i (0, 2, 4, ...): Set newNums[i] = min(nums[2*i], nums[2*i + 1])
    • For odd indices i (1, 3, 5, ...): Set newNums[i] = max(nums[2*i], nums[2*i + 1])
  4. Replace nums with newNums.

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

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

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 to n = n // 2 (halving the size)
  • i << 1 is equivalent to i * 2 (getting the first element of pair i)
  • i << 1 | 1 is equivalent to i * 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:

  1. Initialize the length: Start with n = len(nums), which represents the current working size of the array.

  2. Main loop: Continue the process while n > 1:

    • Halve the working size: Use n >>= 1 (bit shift right by 1) to divide n by 2
    • Process pairs: For each index i from 0 to n-1:
      • Extract the pair of elements:
        • a = nums[i << 1] (equivalent to nums[2*i])
        • b = nums[i << 1 | 1] (equivalent to nums[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)
    • The result is stored directly in the first half of the array
  3. Return the result: After the loop completes (when n == 1), return nums[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 2
    • i << 1 for multiplication by 2
    • i << 1 | 1 for 2*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 Evaluator

Example 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
  • For i = 1 (odd index):
    • Extract pair: a = nums[2] = 2, b = nums[3] = 6
    • Apply max rule: nums[1] = max(2, 6) = 6
  • 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
  • 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.

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

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

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

Load More