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:
- Find the minimum value in
nums
- If there are multiple occurrences of this minimum value, select the first one (leftmost position)
- 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.
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 andn
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:
- Sort the heap to get elements in ascending order of their current values
- Each element gets
k // n
base multiplications - 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 EvaluatorExample 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 takesO(n)
time- Finding the maximum value
m = max(nums)
takesO(n)
time - The while loop runs while
pq[0][0] < m
andk > 0
. In the worst case, we need to multiply the minimum element approximatelylog M
times (whereM
is the maximum value) to reachm
. Each iteration involves:heappop()
:O(log n)
heappush()
:O(log n)
- This gives us
O(log n)
per iteration, and with up tomin(k, n × log M)
iterations, this part isO(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)
sincepow(multiplier, k // n + int(i < k % n), mod)
takesO(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 storesn
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)
Which technique can we use to find the middle of a linked list?
Recommended Readings
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
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
Want a Structured Path to Master System Design Too? Don’t Miss This!