Facebook Pixel

3266. Final Array State After K Multiplication Operations II

Problem Description

You have an integer array nums, and you need to perform k operations on it using a given multiplier.

For each operation:

  1. Find the minimum value in nums
  2. If there are multiple occurrences of this minimum value, select the first one (leftmost position)
  3. Replace this selected value with the result of multiplying it by multiplier

After completing all k operations, apply modulo 10^9 + 7 to every value in the array.

Return the final state of the array after all operations and modulo application.

Example walkthrough:

If nums = [2, 5, 3], k = 3, multiplier = 2:

  • Operation 1: Minimum is 2 (at index 0), replace with 2 * 2 = 4. Array becomes [4, 5, 3]
  • Operation 2: Minimum is 3 (at index 2), replace with 3 * 2 = 6. Array becomes [4, 5, 6]
  • Operation 3: Minimum is 4 (at index 0), replace with 4 * 2 = 8. Array becomes [8, 5, 6]
  • Apply modulo to get final result: [8, 5, 6]

The challenge involves efficiently finding and updating the minimum value k times, then applying the modulo operation to handle large numbers.

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

Intuition

The key observation is that after enough operations, the pattern becomes predictable. Let's think about what happens as we keep applying operations:

Initially, we repeatedly multiply the smallest element by multiplier. This smallest element grows and eventually becomes larger than other elements. Then the next smallest element gets selected and multiplied, and so on.

After some operations, we reach a state where all elements have been multiplied at least once and are relatively close in value. At this point, something interesting happens: the operations start cycling through the array in a round-robin fashion. The element that was just multiplied becomes the largest, and we move to the next smallest element.

Consider when all elements are in the range [m, m * multiplier) where m is some value. After multiplying the smallest element, it becomes at least m * multiplier, making it the largest. The next operation selects a different element, and this pattern continues.

This leads us to a two-phase approach:

Phase 1 - Simulation: Use a min-heap to simulate operations until all elements reach a certain threshold (specifically, until the smallest element is at least as large as the initial maximum value). This handles the irregular early operations where elements are growing at different rates.

Phase 2 - Mathematical Distribution: Once elements are balanced, we can mathematically determine how many more multiplications each element needs:

  • If we have k' remaining operations and n elements
  • Each element gets at least k' // n additional multiplications
  • The smallest k' % n elements get one extra multiplication

The special case of multiplier = 1 is handled separately since multiplication by 1 doesn't change values, making all operations pointless.

For the final calculation, we use modular exponentiation since x * multiplier^p can be computed efficiently as x * pow(multiplier, p, mod) % mod.

Learn more about Heap (Priority Queue) patterns.

Solution Approach

The implementation follows the two-phase approach identified in our intuition:

Special Case Handling: First, we check if multiplier == 1. If so, we return the original array since multiplying by 1 doesn't change any values.

Phase 1 - Heap Simulation:

We create a min-heap containing tuples (value, index) for each element. The heap orders by value first, then by index to handle ties (ensuring we select the first occurrence).

pq = [(x, i) for i, x in enumerate(nums)]
heapify(pq)

We also track the initial maximum value m = max(nums).

We then simulate operations while two conditions hold:

  • We still have operations left (k > 0)
  • The smallest element is less than the initial maximum (pq[0][0] < m)
while k and pq[0][0] < m:
    x, i = heappop(pq)
    heappush(pq, (x * multiplier, i))
    k -= 1

This phase ensures all elements reach a balanced state where they're all at least m in value.

Phase 2 - Mathematical Distribution:

After simulation, we need to distribute the remaining k operations among the n elements. Since operations now cycle through elements in order of their current values:

  1. Sort the heap to get elements in ascending order of their current values
  2. Each element gets k // n base multiplications
  3. The first k % n elements get one additional multiplication
pq.sort()
for i, (x, j) in enumerate(pq):
    power = k // n + int(i < k % n)
    nums[j] = x * pow(multiplier, power, mod) % mod

The pow(multiplier, power, mod) efficiently computes multiplier^power % mod using fast exponentiation, avoiding overflow issues.

Key Data Structures:

  • Min-heap: For efficient access to the minimum element in O(log n) time
  • Tuple storage: (value, index) pairs maintain both the current value and original position

Time Complexity: O(n log n + k') where k' is the number of simulated operations (at most k) Space Complexity: O(n) for the heap

The modulo operation 10^9 + 7 is applied during the final calculation to prevent integer overflow while maintaining the correct relative values.

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 walk through a detailed example with nums = [3, 7, 2, 5], k = 6, and multiplier = 3.

Initial Setup:

  • Create min-heap: [(2, 2), (3, 0), (5, 3), (7, 1)] (sorted by value, then index)
  • Initial maximum m = 7

Phase 1 - Heap Simulation (until smallest ≥ 7):

Operation 1:

  • Pop minimum: (2, 2)
  • Multiply: 2 * 3 = 6
  • Push back: (6, 2)
  • Heap: [(3, 0), (5, 3), (6, 2), (7, 1)]
  • Array state: [3, 7, 6, 5]

Operation 2:

  • Pop minimum: (3, 0)
  • Multiply: 3 * 3 = 9
  • Push back: (9, 0)
  • Heap: [(5, 3), (6, 2), (7, 1), (9, 0)]
  • Array state: [9, 7, 6, 5]

Operation 3:

  • Pop minimum: (5, 3)
  • Multiply: 5 * 3 = 15
  • Push back: (15, 3)
  • Heap: [(6, 2), (7, 1), (9, 0), (15, 3)]
  • Array state: [9, 7, 6, 15]

Operation 4:

  • Pop minimum: (6, 2)
  • Multiply: 6 * 3 = 18
  • Push back: (18, 2)
  • Heap: [(7, 1), (9, 0), (15, 3), (18, 2)]
  • Array state: [9, 7, 18, 15]

Now smallest element (7) ≥ initial maximum (7), so Phase 1 ends.

  • Operations used: 4
  • Remaining k: 2

Phase 2 - Mathematical Distribution:

Sort heap by value: [(7, 1), (9, 0), (15, 3), (18, 2)]

Distribute remaining 2 operations among 4 elements:

  • Base multiplications: 2 // 4 = 0
  • Extra multiplications for first 2 % 4 = 2 elements

Final calculations:

  • Element at index 1: 7 * 3^1 = 21
  • Element at index 0: 9 * 3^1 = 27
  • Element at index 3: 15 * 3^0 = 15
  • Element at index 2: 18 * 3^0 = 18

Final Result: [27, 21, 18, 15]

The algorithm efficiently handles the initial imbalanced growth phase through heap simulation, then mathematically distributes remaining operations once elements are balanced, avoiding unnecessary simulation overhead.

Solution Implementation

1from typing import List
2from heapq import heapify, heappop, heappush
3
4class Solution:
5    def getFinalState(self, nums: List[int], k: int, multiplier: int) -> List[int]:
6        # Edge case: if multiplier is 1, no changes will occur
7        if multiplier == 1:
8            return nums
9      
10        # Create a min-heap with (value, index) pairs
11        # This allows us to track both the minimum value and its original position
12        priority_queue = [(value, index) for index, value in enumerate(nums)]
13        heapify(priority_queue)
14      
15        # Find the maximum value in the original array
16        max_value = max(nums)
17      
18        # Phase 1: Keep multiplying the minimum until it reaches or exceeds the original maximum
19        # This ensures all values become relatively close in magnitude
20        while k > 0 and priority_queue[0][0] < max_value:
21            current_value, index = heappop(priority_queue)
22            heappush(priority_queue, (current_value * multiplier, index))
23            k -= 1
24      
25        # Phase 2: If k operations remain, distribute them evenly across all elements
26        n = len(nums)
27        MOD = 10**9 + 7
28      
29        # Sort the heap to determine the order for remaining operations
30        priority_queue.sort()
31      
32        # Apply remaining multiplications with modular arithmetic
33        for i, (value, original_index) in enumerate(priority_queue):
34            # Each element gets k//n multiplications, plus one extra if it's among the first k%n elements
35            additional_multiplications = k // n + (1 if i < k % n else 0)
36            # Use modular exponentiation for efficiency with large numbers
37            nums[original_index] = value * pow(multiplier, additional_multiplications, MOD) % MOD
38      
39        return nums
40
1class Solution {
2    public int[] getFinalState(int[] nums, int k, int multiplier) {
3        // Special case: if multiplier is 1, no changes will occur
4        if (multiplier == 1) {
5            return nums;
6        }
7      
8        // Priority queue to store [value, index] pairs
9        // Sorted by value first, then by index if values are equal
10        PriorityQueue<long[]> priorityQueue = new PriorityQueue<>(
11            (a, b) -> a[0] == b[0] ? Long.compare(a[1], b[1]) : Long.compare(a[0], b[0])
12        );
13      
14        int arrayLength = nums.length;
15        int maxValue = Arrays.stream(nums).max().getAsInt();
16      
17        // Initialize priority queue with all elements and their indices
18        for (int i = 0; i < arrayLength; ++i) {
19            priorityQueue.offer(new long[] {nums[i], i});
20        }
21      
22        // Phase 1: Multiply smallest elements until they reach or exceed maxValue
23        // This ensures all elements become relatively close in magnitude
24        while (k > 0 && priorityQueue.peek()[0] < maxValue) {
25            long[] smallestElement = priorityQueue.poll();
26            smallestElement[0] *= multiplier;
27            priorityQueue.offer(smallestElement);
28            k--;
29        }
30      
31        // Phase 2: Distribute remaining operations evenly
32        final int MOD = (int) 1e9 + 7;
33      
34        // Process each element from the priority queue
35        for (int i = 0; i < arrayLength; ++i) {
36            long[] element = priorityQueue.poll();
37            long value = element[0];
38            int originalIndex = (int) element[1];
39          
40            // Calculate how many additional multiplications this element gets
41            // k/n operations for everyone, plus 1 extra for the first (k%n) elements
42            int additionalMultiplications = k / arrayLength + (i < k % arrayLength ? 1 : 0);
43          
44            // Apply the multiplications using modular exponentiation
45            nums[originalIndex] = (int) ((value % MOD) * 
46                quickPower(multiplier, additionalMultiplications, MOD) % MOD);
47        }
48      
49        return nums;
50    }
51  
52    /**
53     * Computes (base^exponent) % mod using binary exponentiation
54     * @param base The base number
55     * @param exponent The power to raise the base to
56     * @param mod The modulus for the operation
57     * @return The result of (base^exponent) % mod
58     */
59    private int quickPower(long base, long exponent, long mod) {
60        long result = 1 % mod;
61      
62        // Binary exponentiation algorithm
63        while (exponent > 0) {
64            // If current bit is 1, multiply result by current base
65            if ((exponent & 1) == 1) {
66                result = result * base % mod;
67            }
68            // Square the base for the next bit
69            base = base * base % mod;
70            // Move to the next bit
71            exponent >>= 1;
72        }
73      
74        return (int) result;
75    }
76}
77
1class Solution {
2public:
3    vector<int> getFinalState(vector<int>& nums, int k, int multiplier) {
4        // Edge case: multiplier is 1, no change occurs
5        if (multiplier == 1) {
6            return nums;
7        }
8
9        // Type aliases for readability
10        using ll = long long;
11        using pli = pair<ll, int>;  // pair of (value, index)
12      
13        // Custom comparator for min-heap
14        // Prioritizes smaller values first, then smaller indices if values are equal
15        auto cmp = [](const pli& a, const pli& b) {
16            if (a.first == b.first) {
17                return a.second > b.second;  // Smaller index has higher priority
18            }
19            return a.first > b.first;  // Smaller value has higher priority
20        };
21      
22        // Min-heap to track smallest elements efficiently
23        priority_queue<pli, vector<pli>, decltype(cmp)> minHeap(cmp);
24
25        int n = nums.size();
26        int maxValue = *max_element(nums.begin(), nums.end());
27
28        // Initialize heap with all elements and their indices
29        for (int i = 0; i < n; ++i) {
30            minHeap.emplace(nums[i], i);
31        }
32
33        // Phase 1: Apply operations until minimum element >= initial maximum
34        // This ensures all elements reach a similar magnitude
35        while (k > 0 && minHeap.top().first < maxValue) {
36            auto [value, index] = minHeap.top();
37            minHeap.pop();
38            value *= multiplier;
39            minHeap.emplace(value, index);
40            --k;
41        }
42
43        // Fast modular exponentiation function
44        // Computes (base^exponent) % mod efficiently
45        auto modPow = [&](ll base, ll exponent, ll mod) {
46            ll result = 1 % mod;
47            base = base % mod;
48            while (exponent > 0) {
49                if (exponent & 1) {  // If exponent is odd
50                    result = result * base % mod;
51                }
52                base = base * base % mod;
53                exponent >>= 1;  // Divide exponent by 2
54            }
55            return result;
56        };
57
58        const int MOD = 1e9 + 7;
59      
60        // Phase 2: Distribute remaining operations evenly
61        // Each element gets k/n operations, with first k%n elements getting one extra
62        for (int i = 0; i < n; ++i) {
63            auto [value, index] = minHeap.top();
64            minHeap.pop();
65          
66            // Calculate number of operations for this element
67            int operationsForElement = k / n + (i < k % n ? 1 : 0);
68          
69            // Apply all operations at once using modular exponentiation
70            ll finalValue = (value % MOD) * modPow(multiplier, operationsForElement, MOD) % MOD;
71            nums[index] = static_cast<int>(finalValue);
72        }
73
74        return nums;
75    }
76};
77
1function getFinalState(nums: number[], k: number, multiplier: number): number[] {
2    // Edge case: multiplier is 1, no change occurs
3    if (multiplier === 1) {
4        return nums;
5    }
6
7    // Custom comparator for min-heap
8    // Prioritizes smaller values first, then smaller indices if values are equal
9    const comparator = (a: [number, number], b: [number, number]): number => {
10        if (a[0] === b[0]) {
11            return a[1] - b[1];  // Smaller index has higher priority
12        }
13        return a[0] - b[0];  // Smaller value has higher priority
14    };
15
16    // Min-heap to track smallest elements efficiently
17    // Using array with manual heap operations since TypeScript doesn't have built-in priority queue
18    const minHeap: [number, number][] = [];
19  
20    // Helper functions for heap operations
21    const heapPush = (heap: [number, number][], item: [number, number]): void => {
22        heap.push(item);
23        heap.sort(comparator);
24    };
25  
26    const heapPop = (heap: [number, number][]): [number, number] | undefined => {
27        return heap.shift();
28    };
29
30    const n = nums.length;
31    const maxValue = Math.max(...nums);
32
33    // Initialize heap with all elements and their indices
34    for (let i = 0; i < n; i++) {
35        heapPush(minHeap, [nums[i], i]);
36    }
37
38    // Phase 1: Apply operations until minimum element >= initial maximum
39    // This ensures all elements reach a similar magnitude
40    while (k > 0 && minHeap[0][0] < maxValue) {
41        const element = heapPop(minHeap);
42        if (!element) break;
43      
44        let [value, index] = element;
45        value *= multiplier;
46        heapPush(minHeap, [value, index]);
47        k--;
48    }
49
50    // Fast modular exponentiation function
51    // Computes (base^exponent) % mod efficiently
52    const modPow = (base: number, exponent: number, mod: number): number => {
53        let result = 1 % mod;
54        base = base % mod;
55      
56        while (exponent > 0) {
57            if (exponent & 1) {  // If exponent is odd
58                result = (result * base) % mod;
59            }
60            base = (base * base) % mod;
61            exponent >>= 1;  // Divide exponent by 2
62        }
63      
64        return result;
65    };
66
67    const MOD = 1000000007;
68  
69    // Phase 2: Distribute remaining operations evenly
70    // Each element gets Math.floor(k/n) operations, with first k%n elements getting one extra
71    for (let i = 0; i < n; i++) {
72        const element = heapPop(minHeap);
73        if (!element) break;
74      
75        const [value, index] = element;
76      
77        // Calculate number of operations for this element
78        const operationsForElement = Math.floor(k / n) + (i < k % n ? 1 : 0);
79      
80        // Apply all operations at once using modular exponentiation
81        const finalValue = ((value % MOD) * modPow(multiplier, operationsForElement, MOD)) % MOD;
82        nums[index] = finalValue;
83    }
84
85    return nums;
86}
87

Time and Space Complexity

Time Complexity: O(n × log n × log M + n × log k)

The time complexity breaks down as follows:

  • Creating the initial priority queue from the list takes O(n) time
  • heapify(pq) operation takes O(n) time
  • Finding the maximum value m = max(nums) takes O(n) time
  • The while loop runs while pq[0][0] < m and k > 0. In the worst case, we need to multiply the minimum element approximately log M times (where M is the maximum value) to reach m. Each iteration involves:
    • heappop(): O(log n)
    • heappush(): O(log n)
    • This gives us O(log n) per iteration, and with up to min(k, n × log M) iterations, this part is O(n × log n × log M)
  • Sorting the priority queue: O(n log n)
  • The final loop to update nums with modular exponentiation: O(n × log k) since pow(multiplier, k // n + int(i < k % n), mod) takes O(log k) time for each element

The dominant terms are O(n × log n × log M) and O(n × log k), giving us the overall time complexity of O(n × log n × log M + n × log k).

Space Complexity: O(n)

The space complexity is determined by:

  • The priority queue pq which stores n tuples: O(n)
  • No other significant auxiliary space is used
  • The sorting is done in-place on pq

Therefore, the total space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Integer Overflow During Intermediate Calculations

The Pitfall: Even though we apply modulo at the end, intermediate calculations like value * pow(multiplier, additional_multiplications, MOD) can overflow before the modulo operation is applied. In Python, this is less of an issue due to arbitrary precision integers, but in languages like Java or C++, this would cause incorrect results.

Solution: Apply modulo operations throughout the calculation chain:

# Instead of:
nums[original_index] = value * pow(multiplier, additional_multiplications, MOD) % MOD

# Use:
nums[original_index] = (value % MOD) * pow(multiplier, additional_multiplications, MOD) % MOD

2. Incorrect Heap Ordering with Duplicate Values

The Pitfall: When multiple elements have the same value, Python's heap might not maintain the expected index ordering. The tuple comparison (value, index) works correctly, but developers might mistakenly use just values or implement custom comparisons incorrectly.

Solution: Always include the index as a tiebreaker in the tuple:

# Correct: heap will order by value first, then by index
priority_queue = [(value, index) for index, value in enumerate(nums)]

# Wrong: doesn't handle the "first occurrence" requirement
priority_queue = [value for value in nums]  # Missing index information

3. Forgetting to Handle the multiplier = 1 Edge Case

The Pitfall: When multiplier = 1, multiplying doesn't change values, so the algorithm would waste time performing k operations that have no effect. Worse, the Phase 1 loop condition priority_queue[0][0] < max_value might never terminate if the minimum equals the maximum.

Solution: Always check for this special case at the beginning:

if multiplier == 1:
    return nums

4. Modulo Application Timing

The Pitfall: Applying modulo too early during Phase 1 simulation would break the algorithm's logic. The comparison priority_queue[0][0] < max_value needs actual values, not modulo-reduced values.

Solution: Only apply modulo in the final phase:

# Phase 1: Use actual values for comparisons
while k > 0 and priority_queue[0][0] < max_value:
    current_value, index = heappop(priority_queue)
    heappush(priority_queue, (current_value * multiplier, index))  # No modulo here
    k -= 1

# Phase 2: Apply modulo only at the end
nums[original_index] = value * pow(multiplier, additional_multiplications, MOD) % MOD

5. Incorrect Distribution of Remaining Operations

The Pitfall: After Phase 1, distributing remaining k operations incorrectly. A common mistake is giving all remaining operations to one element or not properly calculating which elements get the extra operation.

Solution: Use integer division and modulo to distribute evenly:

for i, (value, original_index) in enumerate(priority_queue):
    # Base operations for everyone: k // n
    # Extra operation for first k % n elements: (1 if i < k % n else 0)
    additional_multiplications = k // n + (1 if i < k % n else 0)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

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

Load More