Final Array State After K Multiplication Operations II
HardPrompt
You are given an integer array nums, an integer k, and an integer multiplier.
You need to perform k operations on nums. In each operation:
- Find the minimum value
xinnums. If there are multiple occurrences of the minimum value, select the one that appears first. - Replace the selected minimum value
xwithx * multiplier.
After the k operations, apply modulo 109 + 7 to every value in nums.
Return an integer array denoting the final state of nums after performing all k operations and then applying the modulo.
Example 1:
Input: nums = [2,1,3,5,6], k = 5, multiplier = 2
Output: [8,4,6,5,6]
Explanation:
| Operation | Result |
|---|---|
| After operation 1 | [2, 2, 3, 5, 6] |
| After operation 2 | [4, 2, 3, 5, 6] |
| After operation 3 | [4, 4, 3, 5, 6] |
| After operation 4 | [4, 4, 6, 5, 6] |
| After operation 5 | [8, 4, 6, 5, 6] |
| After applying modulo | [8, 4, 6, 5, 6] |
Example 2:
Input: nums = [100000,2000], k = 2, multiplier = 1000000
Output: [999999307,999999993]
Explanation:
| Operation | Result |
|---|---|
| After operation 1 | [100000, 2000000000] |
| After operation 2 | [100000000000, 2000000000] |
| After applying modulo | [999999307, 999999993] |
Constraints:
1 <= nums.length <= 1041 <= nums[i] <= 1091 <= k <= 1091 <= multiplier <= 106
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem. It iterates k times, and in each iteration, it performs a linear scan of the array to find the minimum element, then updates it. This is the most straightforward way to conceptualize the solution but is not practical given the constraints.
Algorithm
- Loop
ktimes fromi = 0tok-1. - In each iteration, find the index of the minimum element in the
numsarray.- Initialize
min_valtoinfinityandmin_idxto-1. - Iterate through the
numsarray fromj = 0ton-1. - If
nums[j]is less thanmin_val, updatemin_val = nums[j]andmin_idx = j.
- Initialize
- After finding the minimum element at
min_idx, update it:nums[min_idx] = nums[min_idx] * multiplier. - After the main loop finishes, iterate through the
numsarray one last time. - Apply modulo
10^9 + 7to each element:nums[i] = nums[i] % (10^9 + 7). - Return the modified
numsarray.
Walkthrough
The brute-force method follows the problem statement literally. We run a loop for k iterations. Inside this loop, we search the entire array to find the minimum value and its first occurring index. This search takes linear time, O(n), where n is the length of the array. Once found, we multiply this minimum value by the multiplier. This process is repeated k times. Since the values can become very large, we should use a data type that can handle large numbers, like long in Java, to avoid overflow during multiplication. After all k operations are complete, a final pass is made over the array to apply the modulo 10^9 + 7 to each element.
class Solution { public int[] finalArray(int[] nums, int k, int multiplier) { int n = nums.length; long[] longNums = new long[n]; for (int i = 0; i < n; i++) { longNums[i] = nums[i]; } for (int i = 0; i < k; i++) { int minIndex = -1; long minVal = Long.MAX_VALUE; for (int j = 0; j < n; j++) { if (longNums[j] < minVal) { minVal = longNums[j]; minIndex = j; } } if (minIndex != -1) { longNums[minIndex] *= multiplier; } } int[] result = new int[n]; int MOD = 1_000_000_007; for (int i = 0; i < n; i++) { result[i] = (int)(longNums[i] % MOD); } return result; }}Complexity
Time
O(k * n) - The outer loop runs `k` times, and the inner loop to find the minimum runs `n` times. With `k` up to `10^9` and `n` up to `10^4`, this is far too slow.
Space
O(n) or O(1) - O(n) if a copy of the array is made to handle large numbers (like `long[]`), otherwise O(1) if the original array can be modified and its data type is sufficient (which is not the case here due to potential overflow).
Trade-offs
Pros
Simple to understand and implement.
Cons
Extremely inefficient and will time out for large values of
k.
To optimize the process of finding the minimum element in each step, we can use a min-heap (Priority Queue). A min-heap allows us to retrieve the minimum element in O(log n) time instead of O(n). We store pairs of (value, index) in the heap to respect the tie-breaking rule. While this is a significant improvement over the brute-force approach, it still simulates each of the k operations individually.
Algorithm
- To handle the tie-breaking rule (smallest index first), we store pairs of
(value, original_index). - Create a Min-Heap (PriorityQueue in Java) that compares elements first by
value, then byindex. - Insert all initial
(nums[i], i)pairs into the heap. - Loop
ktimes:- Extract the minimum pair
(val, idx)from the heap. - Calculate the new value:
new_val = val * multiplier. - Insert the new pair
(new_val, idx)back into the heap.
- Extract the minimum pair
- After
koperations, the heap contains the final state. - Create a result array of size
n. - Poll all elements from the heap and place their values in the result array at their corresponding original indices.
- Apply modulo
10^9 + 7to each element in the result array.
Walkthrough
The bottleneck in the brute-force approach is the repeated linear scan to find the minimum. A min-heap is the perfect data structure to speed this up. We can build a min-heap containing all the numbers from the input array. To handle ties where the element with the smaller original index should be chosen, we store pairs of (value, index) in the heap. The heap's comparator will prioritize smaller values, and for equal values, it will prioritize smaller indices.
The simulation then proceeds for k steps. In each step, we extract the minimum element from the heap, multiply its value by the multiplier, and insert the updated pair back into the heap. Each such operation (extraction and insertion) takes O(log n) time. After k operations, we reconstruct the final array from the elements in the heap.
Note that the values can grow extremely large, exceeding the capacity of long. A BigInteger is required to store the values in the heap to prevent overflow.
import java.math.BigInteger;import java.util.PriorityQueue; class Solution { public int[] finalArray(int[] nums, int k, int multiplier) { int n = nums.length; // A min-heap storing {value, original_index} // BigInteger is needed as values can get very large. PriorityQueue<Object[]> pq = new PriorityQueue<>((a, b) -> { BigInteger valA = (BigInteger) a[0]; BigInteger valB = (BigInteger) b[0]; int idxA = (int) a[1]; int idxB = (int) b[1]; int cmp = valA.compareTo(valB); if (cmp != 0) { return cmp; } return Integer.compare(idxA, idxB); }); for (int i = 0; i < n; i++) { pq.add(new Object[]{new BigInteger(String.valueOf(nums[i])), i}); } BigInteger bigMultiplier = new BigInteger(String.valueOf(multiplier)); for (int i = 0; i < k; i++) { Object[] top = pq.poll(); BigInteger val = (BigInteger) top[0]; int idx = (int) top[1]; BigInteger newVal = val.multiply(bigMultiplier); pq.add(new Object[]{newVal, idx}); } BigInteger[] finalValues = new BigInteger[n]; while (!pq.isEmpty()) { Object[] top = pq.poll(); BigInteger val = (BigInteger) top[0]; int idx = (int) top[1]; finalValues[idx] = val; } int[] result = new int[n]; int MOD = 1_000_000_007; BigInteger bigMod = new BigInteger(String.valueOf(MOD)); for (int i = 0; i < n; i++) { result[i] = finalValues[i].mod(bigMod).intValue(); } return result; }}Complexity
Time
O((n+k) log n) - Building the heap takes `O(n log n)`. Each of the `k` operations takes `O(log n)`. Reconstructing the array takes `O(n log n)`. The total is dominated by the `k` operations. This is still too slow if `k` is large.
Space
O(n) - To store `n` elements in the priority queue.
Trade-offs
Pros
Much faster than brute-force for finding the minimum element in each step.
Cons
Still too slow for large values of
kas it performs one operation at a time.Requires using
BigIntegerfor values to prevent overflow, which adds overhead to operations.
Since k can be enormous, simulating each operation is infeasible. The key is to analyze the properties of the final array. The greedy nature of the operations (always picking the minimum) leads to a state where all final values are relatively close to each other. This property allows us to determine the number of multiplications each element receives without simulating the process. We can use binary search to find the 'level' to which all numbers are raised. This transforms the problem from a lengthy simulation into a search problem, which is much more efficient.
Algorithm
- Handle Edge Case: If
multiplieris 1, the array values never change. Simply apply modulo to the initial array and return. - Key Insight: After many operations, the final values
v_ibecome relatively close. Specifically,max(v) < min(v) * multiplier. This implies that their logarithms to the basemultiplier,log_m(v_i), all lie within an interval of length 1. Let this be[T, T+1). - Binary Search: We can binary search for this target "level"
T. The search space forTcan be from0tok + log_m(max(nums)). We perform binary search for a fixed number of iterations (e.g., 100) to find a preciseT.- The
check(T)function calculates the total operations needed to bring everynums[i]to a level of at leastT. For eachnum, the operations needed isc_i = ceil(T - log_m(num)). Ifsum(c_i) <= k, it meansTis achievable.
- The
- Calculate Final Counts: After finding the optimal
T, calculate the base number of multiplications for each element:c_i = ceil(T - log_m(nums[i])). - Distribute Remainder: The sum of these
c_i's,k_used, might be less thank. The remainingk_rem = k - k_usedoperations must be given to thek_remelements that are currently smallest. We find these by sorting pairs of(log_m(nums[i]) + c_i, i)and picking the firstk_rem. - Compute Final Array: With the final counts
c_ifor each element, calculate the final value(nums[i] * (multiplier ^ c_i)) % MOD. Use modular exponentiation formultiplier ^ c_ito handle large powers efficiently.
Walkthrough
This approach avoids simulation by finding the final state analytically. The core idea is that the k multiplications are distributed among the n numbers in a way that keeps their final values close. This means final_value_i < final_value_j * multiplier for any i, j. Taking log_multiplier on both sides, we find that log_m(final_value_i) for all i must lie in an interval of length 1.
We can binary search for the lower bound of this interval, let's call it T. For a given T, we can calculate the minimum number of multiplications c_i needed for each nums[i] to make log_m(nums[i] * multiplier^{c_i}) >= T. This count is c_i = ceil(T - log_m(nums[i])). We sum these c_i's to get a total ops_needed. If ops_needed <= k, it means T is a possible level, and we can try for a higher one. Otherwise, T is too high.
After the binary search finds the optimal T, we calculate the initial counts c_i. The sum of these counts will be less than or equal to k. Any remaining operations k_rem are distributed one by one to the elements that are currently smallest. We identify these by sorting based on their current logarithmic value log_m(nums[i]) + c_i.
Finally, with the definitive count of multiplications c_i for each nums[i], we compute (nums[i] * power(multiplier, c_i)) % MOD using modular exponentiation.
import java.util.Arrays;import java.util.Comparator; class Solution { private static final int MOD = 1_000_000_007; public int[] finalArray(int[] nums, int k, int multiplier) { int n = nums.length; if (multiplier == 1) { int[] result = new int[n]; for (int i = 0; i < n; i++) { result[i] = nums[i] % MOD; } return result; } double logMultiplier = Math.log(multiplier); double low = 0, high = k + 40; // A safe upper bound for the target level for(int iter = 0; iter < 100; iter++) { // 100 iterations for precision double mid = low + (high - low) / 2; if (check(mid, nums, k, logMultiplier)) { low = mid; } else { high = mid; } } long[] counts = new long[n]; long opsUsed = 0; for (int i = 0; i < n; i++) { double logNum = Math.log(nums[i]); long c = (long) Math.ceil((low * logMultiplier - logNum) / logMultiplier); if (c > 0) { counts[i] = c; opsUsed += c; } } long kRem = k - opsUsed; if (kRem > 0) { Double[][] sortedLogs = new Double[n][2]; for (int i = 0; i < n; i++) { sortedLogs[i][0] = (Math.log(nums[i]) + counts[i] * logMultiplier) / logMultiplier; sortedLogs[i][1] = (double) i; } Arrays.sort(sortedLogs, Comparator.comparingDouble(a -> a[0])); for (int i = 0; i < kRem; i++) { int originalIndex = sortedLogs[i][1].intValue(); counts[originalIndex]++; } } int[] result = new int[n]; for (int i = 0; i < n; i++) { long p = power(multiplier, counts[i], MOD); result[i] = (int) (((long) nums[i] * p) % MOD); } return result; } private boolean check(double T, int[] nums, long k, double logMultiplier) { long opsNeeded = 0; for (int num : nums) { double logNum = Math.log(num); // We need num * mult^c >= T_val, where log(T_val) = T * logMultiplier // c >= T - log_m(num) double ops = (T * logMultiplier - logNum) / logMultiplier; if (ops > 0) { opsNeeded += (long) Math.ceil(ops); } if (opsNeeded > k) { return false; } } return true; } private long power(long base, long exp, int mod) { long res = 1; base %= mod; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % mod; base = (base * base) % mod; exp /= 2; } return res; }}Complexity
Time
O(n log n + n log k) - The binary search part takes `O(100 * n) = O(n)`. Calculating initial counts is `O(n)`. Distributing remaining operations requires sorting, which is `O(n log n)`. The final computation involves `n` modular exponentiations, each taking `O(log k)` time (since counts can be up to `k`). The total complexity is dominated by sorting and modular exponentiation.
Space
O(n) - To store the counts for each number and the pairs for sorting to distribute remaining operations.
Trade-offs
Pros
Highly efficient and can handle very large
k.Avoids simulating individual operations, leading to a much better time complexity.
Cons
More complex to understand and implement.
Relies on floating-point arithmetic (
log), which can have precision issues, though typically manageable withdoubleand enough binary search iterations.
Solutions
Solution
class Solution {public int[] getFinalState(int[] nums, int k, int multiplier) { if (multiplier == 1) { return nums; } PriorityQueue<long[]> pq = new PriorityQueue<>((a, b)->a[0] == b[0] ? Long.compare(a[1], b[1]) : Long.compare(a[0], b[0])); int n = nums.length; int m = Arrays.stream(nums).max().getAsInt(); for (int i = 0; i < n; ++i) { pq.offer(new long[]{nums[i], i}); } for (; k > 0 && pq.peek()[0] < m; --k) { long[] p = pq.poll(); p[0] *= multiplier; pq.offer(p); } final int mod = (int)1 e9 + 7; for (int i = 0; i < n; ++i) { long[] p = pq.poll(); long x = p[0]; int j = (int)p[1]; nums[j] = (int)((x % mod) * qpow(multiplier, k / n + (i < k % n ? 1 : 0), mod) % mod); } return nums; }private int qpow(long a, long n, long mod) { long ans = 1 % mod; for (; n > 0; n >>= 1) { if ((n & 1) == 1) { ans = ans * a % mod; } a = a * a % mod; } return (int)ans; }}class Solution {public: vector<int> getFinalState(vector<int> &nums, int k, int multiplier) { if (multiplier == 1) { return nums; } using ll = long long; using pli = pair<ll, int>; auto cmp = [](const pli &a, const pli &b) { if (a.first == b.first) { return a.second > b.second; } return a.first > b.first; }; priority_queue<pli, vector<pli>, decltype(cmp)> pq(cmp); int n = nums.size(); int m = *max_element(nums.begin(), nums.end()); for (int i = 0; i < n; ++i) { pq.emplace(nums[i], i); } while (k > 0 && pq.top().first < m) { auto p = pq.top(); pq.pop(); p.first *= multiplier; pq.emplace(p); --k; } auto qpow = [&](ll a, ll n, ll mod) { ll ans = 1 % mod; a = a % mod; while (n > 0) { if (n & 1) { ans = ans * a % mod; } a = a * a % mod; n >>= 1; } return ans; }; const int mod = 1e9 + 7; for (int i = 0; i < n; ++i) { auto p = pq.top(); pq.pop(); long long x = p.first; int j = p.second; nums[j] = static_cast<int>( (x % mod) * qpow(multiplier, k / n + (i < k % n ? 1 : 0), mod) % mod); } return nums; }};class Solution: def getFinalState(self, nums: List[int], k: int, multiplier: int) -> List[int]: if multiplier == 1: return nums pq = [(x, i) for i, x in enumerate(nums)] heapify(pq) m = max(nums) while k and pq[0][0] < m: x, i = heappop(pq) heappush(pq, (x * multiplier, i)) k -= 1 n = len(nums) mod = 10 ** 9 + 7 pq . sort() for i, (x, j) in enumerate(pq): nums[j] = x * pow(multiplier, k // n + int(i < k % n), mod) % mod return numsVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.