2892. Minimizing Array After Replacing Pairs With Their Product 🔒
Problem Description
You are given an integer array nums
and an integer k
. You can perform operations on the array to reduce its length.
In each operation, you can:
- Select two adjacent elements in the array (let's call them
x
andy
) - Check if their product
x * y
is less than or equal tok
- If yes, replace both elements with a single element whose value is
x * y
For example, with array [1, 2, 2, 3]
and k = 5
:
- You could merge the first two elements
1
and2
(since1 * 2 = 2 ≤ 5
) to get[2, 2, 3]
- Or you could merge the middle two elements
2
and2
(since2 * 2 = 4 ≤ 5
) to get[1, 4, 3]
- But you cannot merge
2
and3
(since2 * 3 = 6 > 5
)
The goal is to find the minimum possible length of the array after performing any number of such operations. You can perform as many operations as you want, in any order, as long as the merge condition (x * y ≤ k
) is satisfied.
The key insight is to greedily merge adjacent elements whenever possible. Starting from the left, we try to accumulate the product of consecutive elements as long as the product stays within k
. When adding another element would exceed k
, we start a new segment. The number of such segments gives us the minimum array length.
Special case: If any element in the array is 0
, the entire array can be reduced to length 1
since 0
multiplied by any number is 0
, which is always ≤ k
.
Intuition
The key observation is that we want to merge as many adjacent elements as possible to minimize the final array length. Think of it like trying to pack items into boxes - we want to fit as many items as possible into each box before starting a new one.
Since we can only merge adjacent elements, and merging creates a new element with their product, we should ask: what's the best strategy for deciding when to merge?
Consider walking through the array from left to right. At each position, we have a choice: either merge the current element with the previous group (if possible), or start a new group. The constraint is that the product of all elements in a group cannot exceed k
.
This naturally leads to a greedy approach: keep accumulating the product of consecutive elements as long as the product stays within k
. When we encounter an element that would cause the product to exceed k
, we have no choice but to start a new group with that element.
Why does this greedy strategy work? Because if we can merge elements now, there's no benefit to keeping them separate. Merging reduces the array length immediately, and since we're processing left to right, it doesn't affect our ability to make future merges. Each group we create represents a maximal sequence of elements that can be merged together.
The special case of 0
is interesting: if any element is 0
, then 0 * anything = 0 ≤ k
, meaning we can merge the 0
with its neighbors repeatedly until the entire array becomes a single element with value 0
. This is why the presence of any 0
immediately tells us the answer is 1
.
The algorithm essentially counts how many "breaking points" we have - positions where we must start a new group because the accumulated product would exceed k
. The number of groups equals the minimum array length.
Learn more about Greedy and Dynamic Programming patterns.
Solution Approach
The implementation uses a greedy algorithm with two key variables:
ans
: tracks the minimum array length (initially1
since we start with at least one group)y
: maintains the running product of the current group (initiallynums[0]
)
We traverse the array starting from the second element. For each element x
:
Case 1: Element is Zero (x == 0
)
- If we encounter a
0
, we immediately return1
- This is because
0 * anything = 0 ≤ k
, allowing us to merge the entire array into a single element
Case 2: Can Merge (x * y ≤ k
)
- We can safely merge
x
with the current group - Update the running product:
y = y * x
- The array length (
ans
) remains unchanged since we're extending the current group
Case 3: Cannot Merge (x * y > k
)
- The product would exceed
k
, so we cannot merge - Start a new group with
x
: sety = x
- Increment the array length:
ans = ans + 1
The algorithm processes each element exactly once, making decisions based on whether merging would violate the constraint product ≤ k
.
Example walkthrough with nums = [2, 3, 3, 7, 3, 5]
and k = 20
:
- Initialize:
ans = 1
,y = 2
- Process
3
:2 * 3 = 6 ≤ 20
→ merge,y = 6
- Process
3
:6 * 3 = 18 ≤ 20
→ merge,y = 18
- Process
7
:18 * 7 = 126 > 20
→ cannot merge, start new group,ans = 2
,y = 7
- Process
3
:7 * 3 = 21 > 20
→ cannot merge, start new group,ans = 3
,y = 3
- Process
5
:3 * 5 = 15 ≤ 20
→ merge,y = 15
- Return
ans = 3
The time complexity is O(n)
where n
is the length of the array, and space complexity is O(1)
as we only use a constant amount of extra space.
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 small example to illustrate the solution approach.
Example: nums = [3, 2, 4, 1]
and k = 10
We'll use the greedy approach, maintaining a running product and counting how many groups we need.
Initial State:
ans = 1
(we start with at least one group)y = 3
(the running product starts with the first element)
Step 1: Process element 2
(index 1)
- Check if we can merge:
y * 2 = 3 * 2 = 6
- Since
6 ≤ 10
, we can merge - Update running product:
y = 6
- Array conceptually becomes:
[6, 4, 1]
ans
remains1
(still one group)
Step 2: Process element 4
(index 2)
- Check if we can merge:
y * 4 = 6 * 4 = 24
- Since
24 > 10
, we cannot merge with the current group - Start a new group with this element:
y = 4
- Increment group count:
ans = 2
- Array conceptually becomes:
[6] [4, 1]
(two separate groups)
Step 3: Process element 1
(index 3)
- Check if we can merge:
y * 1 = 4 * 1 = 4
- Since
4 ≤ 10
, we can merge - Update running product:
y = 4
- Array conceptually becomes:
[6] [4]
(two groups) ans
remains2
Final Result: The minimum array length is 2
.
The algorithm efficiently determines that we can merge elements [3, 2]
into one group (product = 6) and elements [4, 1]
into another group (product = 4), but we cannot merge across these groups because 6 * 4 = 24 > 10
.
Solution Implementation
1class Solution:
2 def minArrayLength(self, nums: List[int], k: int) -> int:
3 # Initialize the result counter and current product
4 result_length = 1 # Start with at least one element
5 current_product = nums[0] # Track the product of current group
6
7 # Iterate through remaining elements
8 for current_num in nums[1:]:
9 # Special case: if any element is 0, minimum length is 1
10 # (since 0 * anything = 0 <= k, all elements can be merged)
11 if current_num == 0:
12 return 1
13
14 # Check if current element can be merged with previous group
15 if current_num * current_product <= k:
16 # Merge: multiply into current product
17 current_product *= current_num
18 else:
19 # Cannot merge: start a new group
20 current_product = current_num
21 result_length += 1
22
23 return result_length
24
1class Solution {
2 /**
3 * Finds the minimum number of subarrays needed where the product of each subarray is at most k.
4 *
5 * @param nums the input array of integers
6 * @param k the maximum allowed product for each subarray
7 * @return the minimum number of subarrays needed
8 */
9 public int minArrayLength(int[] nums, int k) {
10 // Initialize the count of subarrays
11 int subarrayCount = 1;
12
13 // Track the current running product
14 long currentProduct = nums[0];
15
16 // Iterate through the array starting from the second element
17 for (int i = 1; i < nums.length; i++) {
18 int currentElement = nums[i];
19
20 // If we encounter a zero, we need at least one subarray containing it
21 // Since 0 * anything = 0 <= k, return 1
22 if (currentElement == 0) {
23 return 1;
24 }
25
26 // Check if we can include the current element in the current subarray
27 if (currentElement * currentProduct <= k) {
28 // Include current element in the current subarray
29 currentProduct *= currentElement;
30 } else {
31 // Start a new subarray with the current element
32 currentProduct = currentElement;
33 subarrayCount++;
34 }
35 }
36
37 return subarrayCount;
38 }
39}
40
1class Solution {
2public:
3 int minArrayLength(vector<int>& nums, int k) {
4 // Initialize the minimum array length to 1 (at least one element)
5 int minLength = 1;
6
7 // Start with the first element as the current product
8 long long currentProduct = nums[0];
9
10 // Iterate through the array starting from the second element
11 for (int i = 1; i < nums.size(); ++i) {
12 int currentNum = nums[i];
13
14 // If we encounter a zero, the product becomes 0 (≤ k)
15 // We can merge all elements into one, resulting in length 1
16 if (currentNum == 0) {
17 return 1;
18 }
19
20 // Check if we can merge current element with the accumulated product
21 if (currentNum * currentProduct <= k) {
22 // Merge: multiply the current product with the current number
23 currentProduct *= currentNum;
24 } else {
25 // Cannot merge: start a new segment with the current number
26 currentProduct = currentNum;
27 ++minLength;
28 }
29 }
30
31 return minLength;
32 }
33};
34
1/**
2 * Finds the minimum length of array after performing operations
3 * where consecutive elements can be merged if their product is <= k
4 * @param nums - The input array of numbers
5 * @param k - The maximum allowed product value
6 * @returns The minimum possible array length after operations
7 */
8function minArrayLength(nums: number[], k: number): number {
9 // Initialize result counter and current product accumulator
10 let resultCount: number = 1;
11 let currentProduct: number = nums[0];
12
13 // Iterate through remaining elements starting from index 1
14 for (let i = 1; i < nums.length; i++) {
15 const currentElement: number = nums[i];
16
17 // If we encounter a zero, it cannot be merged with anything
18 // The minimum length is 1 (the zero itself)
19 if (currentElement === 0) {
20 return 1;
21 }
22
23 // Check if current element can be merged with accumulated product
24 if (currentElement * currentProduct <= k) {
25 // Merge by multiplying with accumulated product
26 currentProduct *= currentElement;
27 } else {
28 // Cannot merge, start a new segment
29 currentProduct = currentElement;
30 resultCount++;
31 }
32 }
33
34 return resultCount;
35}
36
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the input array nums
. This is because the algorithm iterates through the array exactly once using a single for loop that processes each element from nums[1:]
, performing constant-time operations for each element (multiplication, comparison, and assignment operations).
The space complexity is O(1)
. The algorithm only uses a fixed amount of extra space regardless of the input size, storing just three variables: ans
to track the result count, y
to maintain the current product value, and x
as the loop variable. No additional data structures that scale with input size are created.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Integer Overflow in Product Calculation
The Problem:
When checking if current_num * current_product <= k
, the multiplication current_num * current_product
can cause integer overflow for large values. In Python, while integers can grow arbitrarily large, in other languages or even in Python when dealing with very large numbers, this can lead to incorrect comparisons or performance issues.
Example Scenario:
nums = [10^9, 10^9, 2] k = 10^9
The product 10^9 * 10^9 = 10^18
might overflow in languages with fixed integer sizes.
Solution: Instead of computing the product directly, rearrange the comparison to avoid overflow:
# Instead of: current_num * current_product <= k # Use: current_num <= k // current_product (when current_product > 0) if current_product > 0 and current_num <= k // current_product: current_product *= current_num else: current_product = current_num result_length += 1
Pitfall 2: Not Handling Zero at the Beginning
The Problem:
The current code checks for zero during iteration but initializes current_product
with nums[0]
. If nums[0]
is zero, the algorithm doesn't immediately return 1, potentially leading to incorrect behavior when multiplying subsequent elements.
Example Scenario:
nums = [0, 5, 3] k = 10
Starting with current_product = 0
, all subsequent multiplications will be 0.
Solution: Check for zero elements at the very beginning or during initialization:
def minArrayLength(self, nums: List[int], k: int) -> int:
# Check for any zeros upfront
if 0 in nums:
return 1
result_length = 1
current_product = nums[0]
for current_num in nums[1:]:
if current_num * current_product <= k:
current_product *= current_num
else:
current_product = current_num
result_length += 1
return result_length
Pitfall 3: Edge Case with Single Element Array
The Problem:
While the current solution handles single-element arrays correctly (returns 1), the logic isn't immediately obvious since we're iterating from nums[1:]
, which would be empty for a single-element array.
Solution: Add explicit handling or documentation for clarity:
def minArrayLength(self, nums: List[int], k: int) -> int:
# Edge case: single element
if len(nums) == 1:
return 1
# ... rest of the code
Pitfall 4: Misunderstanding the Greedy Strategy
The Problem: Developers might think they need to find the optimal merging strategy by considering different orderings or using dynamic programming. However, the greedy left-to-right approach is actually optimal because once you decide not to merge at a position, you cannot "go back" and merge across that boundary later.
Solution: Document why the greedy approach works:
def minArrayLength(self, nums: List[int], k: int) -> int:
"""
Greedy approach: Always merge when possible from left to right.
This is optimal because:
1. Merging reduces array length
2. Once we decide not to merge at position i, we cannot merge
elements across position i later
3. Therefore, merging eagerly from left to right gives minimum segments
"""
# ... implementation
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
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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
Want a Structured Path to Master System Design Too? Don’t Miss This!