1868. Product of Two Run-Length Encoded Arrays
Problem Description
This problem asks you to find the product of two run-length encoded arrays.
Run-length encoding is a compression technique where consecutive repeated numbers in an array are represented as [value, frequency]
pairs. For example, the array [1,1,1,2,2,2,2,2]
can be encoded as [[1,3],[2,5]]
, meaning "three 1's followed by five 2's".
Given two run-length encoded arrays encoded1
and encoded2
that represent arrays of the same length, you need to:
- Conceptually expand both encoded arrays back to their full forms (though you shouldn't actually do this in implementation)
- Multiply the corresponding elements at each position
- Compress the resulting product array back into run-length encoded format
For example:
- If
encoded1 = [[1,3],[2,5]]
represents[1,1,1,2,2,2,2,2]
- And
encoded2 = [[3,3],[4,5]]
represents[3,3,3,4,4,4,4,4]
- The element-wise product would be
[3,3,3,8,8,8,8,8]
- Which compresses to
[[3,3],[8,5]]
The key insight is that you don't need to fully expand the arrays. Instead, you can work directly with the encoded format by:
- Using two pointers to track positions in both encoded arrays
- Taking the minimum frequency between current segments to determine how many products to compute
- Multiplying the values and tracking the frequency
- Merging consecutive segments with the same product value
- Advancing pointers when a segment is exhausted
The solution must return the compressed run-length encoded array with the minimum possible length (meaning consecutive segments with the same value should be merged).
Intuition
The naive approach would be to fully expand both encoded arrays, multiply them element by element, and then compress the result. However, this would be inefficient in terms of both time and space, especially when dealing with large frequencies.
The key observation is that we can work directly with the compressed format. Think of the encoded arrays as describing "segments" or "blocks" of repeated values. When we multiply two arrays element-wise, we're essentially aligning these segments and multiplying their values.
Consider how segments align: if encoded1
has a segment [val1, freq1]
and encoded2
has a segment [val2, freq2]
, these segments might not perfectly align. One segment might "span" multiple segments from the other array. For example:
encoded1 = [[2, 6]]
represents[2,2,2,2,2,2]
encoded2 = [[3, 4], [4, 2]]
represents[3,3,3,3,4,4]
Here, the segment [2, 6]
from encoded1
overlaps with both segments from encoded2
.
The insight is to process these segments in "chunks" by taking the minimum frequency at each step. We maintain pointers for both arrays and at each step:
- Take the smaller frequency between the current segments - this tells us how many elements we can process together
- Multiply the values to get the product value for this chunk
- Decrease the frequencies of both current segments by the amount we processed
- When a segment's frequency reaches zero, move to the next segment in that array
This way, we're essentially "consuming" both arrays in parallel, always processing the maximum number of elements we can handle at once without needing to split a multiplication operation.
To maintain the minimum length requirement, whenever we add a new [product, frequency]
pair to our result, we check if it has the same value as the last pair we added. If so, we merge them by adding the frequencies instead of creating a new entry.
Learn more about Two Pointers patterns.
Solution Approach
The solution uses a two-pointer approach to process both encoded arrays simultaneously without expanding them.
Algorithm Steps:
-
Initialize variables:
ans
- result list to store the run-length encoded productj = 0
- pointer forencoded2
(we iterate throughencoded1
directly)
-
Iterate through each segment in
encoded1
:- For each segment
[vi, fi]
inencoded1
, we need to multiplyvi
with corresponding values fromencoded2
- Use a while loop to process all
fi
elements of the current segment
- For each segment
-
Process segments in chunks:
- Calculate
f = min(fi, encoded2[j][1])
- this is the number of elements we can process together - Compute the product:
v = vi * encoded2[j][0]
- This gives us a chunk of
f
elements with valuev
- Calculate
-
Merge consecutive segments with same value:
- If
ans
is not empty and the last segment has the same valuev
, merge by adding frequencies:ans[-1][1] += f
- Otherwise, append a new segment:
ans.append([v, f])
- If
-
Update frequencies and pointers:
- Decrease
fi
byf
(elements processed from currentencoded1
segment) - Decrease
encoded2[j][1]
byf
(elements processed from currentencoded2
segment) - If
encoded2[j][1]
becomes 0, move to next segment:j += 1
- Decrease
Example walkthrough:
encoded1 = [[1,3], [2,2]] # represents [1,1,1,2,2] encoded2 = [[3,2], [4,3]] # represents [3,3,4,4,4] Step 1: vi=1, fi=3, j=0 - f = min(3, 2) = 2 - v = 1 * 3 = 3 - ans = [[3, 2]] - fi = 1, encoded2[0][1] = 0, j = 1 Step 2: vi=1, fi=1, j=1 - f = min(1, 3) = 1 - v = 1 * 4 = 4 - ans = [[3, 2], [4, 1]] - fi = 0, encoded2[1][1] = 2 Step 3: vi=2, fi=2, j=1 - f = min(2, 2) = 2 - v = 2 * 4 = 8 - ans = [[3, 2], [4, 1], [8, 2]] Result: [[3,2], [4,1], [8,2]] # represents [3,3,4,8,8]
The time complexity is O(m + n)
where m
and n
are the lengths of the encoded arrays, as we process each segment exactly once. The space complexity is O(1)
if we don't count the output array.
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 concrete example to illustrate the solution approach:
Input:
encoded1 = [[2,3], [3,2]]
which represents[2,2,2,3,3]
encoded2 = [[5,2], [1,3]]
which represents[5,5,1,1,1]
Expected output: The product [10,10,2,3,3]
encoded as [[10,2], [2,1], [3,2]]
Step-by-step execution:
Initial state:
ans = []
(result array)j = 0
(pointer for encoded2)- Start processing first segment of encoded1:
[2,3]
Iteration 1 - Processing segment [2,3] from encoded1:
vi = 2, fi = 3
(value and frequency from encoded1)encoded2[j] = [5,2]
(current segment in encoded2)f = min(3, 2) = 2
(can process 2 elements)v = 2 * 5 = 10
(product value)- Add
[10, 2]
to ans →ans = [[10,2]]
- Update:
fi = 3-2 = 1
,encoded2[0][1] = 2-2 = 0
- Since
encoded2[0][1] = 0
, incrementj = 1
Iteration 1 continued - Still processing [2,3]:
fi = 1
(remaining frequency from current encoded1 segment)encoded2[j] = [1,3]
(moved to next segment)f = min(1, 3) = 1
(can process 1 element)v = 2 * 1 = 2
(product value)- Add
[2, 1]
to ans →ans = [[10,2], [2,1]]
- Update:
fi = 1-1 = 0
,encoded2[1][1] = 3-1 = 2
fi = 0
, so move to next segment in encoded1
Iteration 2 - Processing segment [3,2] from encoded1:
vi = 3, fi = 2
(value and frequency from encoded1)encoded2[j] = [1,2]
(current segment with remaining frequency)f = min(2, 2) = 2
(can process 2 elements)v = 3 * 1 = 3
(product value)- Add
[3, 2]
to ans →ans = [[10,2], [2,1], [3,2]]
- Update:
fi = 2-2 = 0
,encoded2[1][1] = 2-2 = 0
Final result: [[10,2], [2,1], [3,2]]
This represents the array [10,10,2,3,3]
, which is indeed the element-wise product of [2,2,2,3,3]
and [5,5,1,1,1]
.
The key insight demonstrated here is how we "consume" both arrays simultaneously using the minimum frequency at each step, avoiding the need to expand the arrays while still correctly computing the product.
Solution Implementation
1class Solution:
2 def findRLEArray(
3 self, encoded1: List[List[int]], encoded2: List[List[int]]
4 ) -> List[List[int]]:
5 """
6 Multiply two run-length encoded arrays element-wise and return the result
7 as a run-length encoded array.
8
9 Args:
10 encoded1: First RLE array where each element is [value, frequency]
11 encoded2: Second RLE array where each element is [value, frequency]
12
13 Returns:
14 RLE array representing the element-wise product
15 """
16 result = []
17 encoded2_index = 0
18
19 # Process each segment in the first encoded array
20 for value1, frequency1 in encoded1:
21 # Continue processing while current segment from encoded1 has elements
22 while frequency1 > 0:
23 # Take the minimum frequency between current segments
24 min_frequency = min(frequency1, encoded2[encoded2_index][1])
25
26 # Calculate the product value for this segment
27 product_value = value1 * encoded2[encoded2_index][0]
28
29 # Merge with the last segment if values are the same
30 if result and result[-1][0] == product_value:
31 result[-1][1] += min_frequency
32 else:
33 # Add new segment to result
34 result.append([product_value, min_frequency])
35
36 # Decrease frequencies of both segments
37 frequency1 -= min_frequency
38 encoded2[encoded2_index][1] -= min_frequency
39
40 # Move to next segment in encoded2 if current is exhausted
41 if encoded2[encoded2_index][1] == 0:
42 encoded2_index += 1
43
44 return result
45
1class Solution {
2 public List<List<Integer>> findRLEArray(int[][] encoded1, int[][] encoded2) {
3 // Result list to store the product run-length encoded array
4 List<List<Integer>> result = new ArrayList<>();
5
6 // Pointer for encoded2 array
7 int index2 = 0;
8
9 // Iterate through each run-length pair in encoded1
10 for (int[] pair1 : encoded1) {
11 int value1 = pair1[0];
12 int frequency1 = pair1[1];
13
14 // Process the current segment from encoded1
15 while (frequency1 > 0) {
16 // Take the minimum frequency between current segments of both arrays
17 int minFrequency = Math.min(frequency1, encoded2[index2][1]);
18
19 // Calculate the product of values from both arrays
20 int productValue = value1 * encoded2[index2][0];
21
22 // Check if we can merge with the last segment in result
23 int resultSize = result.size();
24 if (resultSize > 0 && result.get(resultSize - 1).get(0) == productValue) {
25 // Merge with the last segment by updating its frequency
26 result.get(resultSize - 1).set(1, result.get(resultSize - 1).get(1) + minFrequency);
27 } else {
28 // Add a new segment to the result
29 result.add(new ArrayList<>(List.of(productValue, minFrequency)));
30 }
31
32 // Decrease the remaining frequencies
33 frequency1 -= minFrequency;
34 encoded2[index2][1] -= minFrequency;
35
36 // Move to the next segment in encoded2 if current one is exhausted
37 if (encoded2[index2][1] == 0) {
38 index2++;
39 }
40 }
41 }
42
43 return result;
44 }
45}
46
1class Solution {
2public:
3 vector<vector<int>> findRLEArray(vector<vector<int>>& encoded1, vector<vector<int>>& encoded2) {
4 vector<vector<int>> result;
5 int index2 = 0; // Current index in encoded2 array
6
7 // Iterate through each run-length encoded segment in encoded1
8 for (auto& segment1 : encoded1) {
9 int value1 = segment1[0]; // Value from encoded1
10 int frequency1 = segment1[1]; // Frequency/count from encoded1
11
12 // Process the current segment from encoded1
13 while (frequency1 > 0) {
14 // Take the minimum frequency between remaining frequency1 and current encoded2 segment
15 int minFrequency = min(frequency1, encoded2[index2][1]);
16
17 // Calculate the product of values from both arrays
18 int productValue = value1 * encoded2[index2][0];
19
20 // Merge with the last segment if values are the same, otherwise add new segment
21 if (!result.empty() && result.back()[0] == productValue) {
22 result.back()[1] += minFrequency; // Extend the last segment
23 } else {
24 result.push_back({productValue, minFrequency}); // Add new segment
25 }
26
27 // Update remaining frequencies
28 frequency1 -= minFrequency;
29 encoded2[index2][1] -= minFrequency;
30
31 // Move to next segment in encoded2 if current segment is exhausted
32 if (encoded2[index2][1] == 0) {
33 ++index2;
34 }
35 }
36 }
37
38 return result;
39 }
40};
41
1function findRLEArray(encoded1: number[][], encoded2: number[][]): number[][] {
2 // Result array to store the product run-length encoding
3 const result: number[][] = [];
4
5 // Current index in the second encoded array
6 let indexInEncoded2 = 0;
7
8 // Iterate through each run-length encoded segment in the first array
9 for (const segmentFromEncoded1 of encoded1) {
10 // Extract value and frequency from the current segment of encoded1
11 const valueFromEncoded1 = segmentFromEncoded1[0];
12 let remainingFrequency1 = segmentFromEncoded1[1];
13
14 // Process the current segment from encoded1 until its frequency is exhausted
15 while (remainingFrequency1 > 0) {
16 // Get the current segment from encoded2
17 const currentSegmentEncoded2 = encoded2[indexInEncoded2];
18
19 // Calculate the minimum frequency to process in this iteration
20 // This ensures we don't exceed the available frequency in either array
21 const frequencyToProcess = Math.min(
22 remainingFrequency1,
23 currentSegmentEncoded2[1]
24 );
25
26 // Calculate the product of values from both arrays
27 const productOfValues = valueFromEncoded1 * currentSegmentEncoded2[0];
28
29 // Check if we can merge with the last segment in the result
30 // Merge if the product value is the same as the last segment's value
31 if (result.length > 0 && result[result.length - 1][0] === productOfValues) {
32 // Extend the frequency of the last segment
33 result[result.length - 1][1] += frequencyToProcess;
34 } else {
35 // Add a new segment with the product value and frequency
36 result.push([productOfValues, frequencyToProcess]);
37 }
38
39 // Decrease the remaining frequencies after processing
40 remainingFrequency1 -= frequencyToProcess;
41 currentSegmentEncoded2[1] -= frequencyToProcess;
42
43 // Move to the next segment in encoded2 if the current one is fully consumed
44 if (currentSegmentEncoded2[1] === 0) {
45 indexInEncoded2++;
46 }
47 }
48 }
49
50 return result;
51}
52
Time and Space Complexity
Time Complexity: O(n + m)
where n
is the length of encoded1
and m
is the length of encoded2
.
The algorithm iterates through each element in encoded1
once using the outer loop. For each element [vi, fi]
in encoded1
, the inner while loop processes exactly fi
units. The key insight is that each unit in the expanded form of both arrays is processed exactly once:
- Each frequency unit from
encoded1
is consumed exactly once through thefi -= f
operation - Each frequency unit from
encoded2
is consumed exactly once through theencoded2[j][1] -= f
operation - The pointer
j
moves throughencoded2
sequentially and never backtracks
Since the total number of operations is proportional to the number of segments in both encoded arrays (not the expanded frequencies), the time complexity is O(n + m)
.
Space Complexity: O(1)
excluding the output array, or O(n + m)
including the output array.
The algorithm uses only a constant amount of extra space:
- Variable
j
for indexingencoded2
- Variables
vi
,fi
,f
, andv
for temporary storage during processing
The output array ans
can contain at most n + m
segments in the worst case (when no adjacent segments can be merged), giving a total space complexity of O(n + m)
when including the output.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Modifying Input Arrays
The provided solution directly modifies encoded2
by decreasing its frequency values (encoded2[encoded2_index][1] -= min_frequency
). This is problematic because:
- It mutates the input, which may be unexpected behavior
- If the function needs to be called multiple times with the same input, it will produce incorrect results
- It violates the principle of keeping functions pure when possible
Solution: Create local copies of the frequency values instead of modifying the input directly:
def findRLEArray(self, encoded1, encoded2):
result = []
i, j = 0, 0
freq1, freq2 = 0, 0
while i < len(encoded1) and j < len(encoded2):
# Use local copies of frequencies
if freq1 == 0:
val1, freq1 = encoded1[i]
i += 1
if freq2 == 0:
val2, freq2 = encoded2[j]
j += 1
# Process the minimum frequency
min_freq = min(freq1, freq2)
product = val1 * val2
# Merge or append to result
if result and result[-1][0] == product:
result[-1][1] += min_freq
else:
result.append([product, min_freq])
# Update local frequency counters
freq1 -= min_freq
freq2 -= min_freq
return result
2. Forgetting to Merge Consecutive Segments
A common mistake is to append every product segment without checking if it can be merged with the previous one. This leads to unnecessarily long output arrays like [[3,1], [3,2], [8,3]]
instead of [[3,3], [8,3]]
.
Solution: Always check if the last segment in the result has the same value before appending a new segment.
3. Index Out of Bounds
When using encoded2[encoded2_index]
, failing to ensure encoded2_index
stays within bounds can cause crashes, especially if the arrays aren't actually the same expanded length as promised.
Solution: Add boundary checks or restructure the loop to naturally handle boundaries:
while i < len(encoded1) or j < len(encoded2):
if i >= len(encoded1) or j >= len(encoded2):
break # Or handle error case
# ... rest of logic
4. Not Handling Edge Cases
The solution might fail on edge cases like:
- Empty arrays
- Single-element encoded arrays
[[5, 1]]
- Arrays with zero frequencies (though these shouldn't exist in valid RLE)
Solution: Add initial validation and handle edge cases explicitly:
if not encoded1 or not encoded2: return []
How many ways can you arrange the three letters A, B and C?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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!