Facebook Pixel

1352. Product of the Last K Numbers

Problem Description

This problem asks you to design a data structure that maintains a stream of integers and can efficiently calculate the product of the last k integers in the stream.

You need to implement a ProductOfNumbers class with the following methods:

  1. ProductOfNumbers(): Constructor that initializes an empty stream.

  2. add(int num): Adds an integer num to the end of the stream.

  3. getProduct(int k): Returns the product of the last k integers in the stream. You're guaranteed that the stream always has at least k numbers when this method is called.

The key insight for the solution is using a prefix product array. The solution maintains an array s where each element stores the cumulative product up to that position.

How the solution works:

  • The array s is initialized with [1] as a base case.
  • When adding a number:
    • If the number is 0, reset s to [1] (since any product involving 0 will be 0)
    • Otherwise, append s[-1] * num to s (multiply the last cumulative product by the new number)
  • When getting the product of last k numbers:
    • If the length of s is less than or equal to k, return 0 (this means there was a 0 in the last k numbers)
    • Otherwise, return s[-1] // s[-k - 1] (divide the current cumulative product by the cumulative product from k positions back)

The division s[-1] // s[-k - 1] effectively gives us the product of elements from position [-k-1] to [-1], which are the last k elements.

Example walkthrough:

  • After adding [3, 0, 2, 5, 4], the prefix product array would be reset at 0 and then built as [1, 2, 10, 40]
  • getProduct(2) returns 40 // 2 = 20 (product of last 2: 5 * 4)
  • getProduct(3) returns 40 // 1 = 40 (product of last 3: 2 * 5 * 4)
  • getProduct(4) returns 0 (since array length 4 ≤ k=4, meaning the 0 is included)

This approach achieves O(1) time complexity for both add and getProduct operations.

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

Intuition

The naive approach would be to store all numbers in a list and calculate the product of the last k numbers each time getProduct is called. This would take O(k) time for each query, which isn't efficient when we have many queries.

The key observation is that we can use prefix products to calculate any range product in O(1) time. Think about prefix sums - if we know the sum from index 0 to i and from 0 to j, we can get the sum from i+1 to j by subtraction. Similarly, with products, we can use division.

For example, if we have numbers [2, 3, 4, 5] and their prefix products [2, 6, 24, 120]:

  • To get the product of last 2 numbers (4 * 5), we divide 120 / 6 = 20
  • To get the product of last 3 numbers (3 * 4 * 5), we divide 120 / 2 = 60

The pattern is: to get the product of last k numbers, we divide the total product by the product up to position n-k-1.

But what about zeros? A zero anywhere in the sequence makes all products involving it equal to 0. The clever trick here is to reset the prefix product array whenever we encounter a zero. This way:

  • If we query for k numbers and our array has less than or equal to k elements, we know there was a zero within the last k numbers, so we return 0
  • Otherwise, we can safely use our division formula

By starting our prefix array with [1] as a sentinel value, we handle edge cases elegantly - when we want the product of all stored numbers, we divide by s[0] which is 1.

This approach transforms an O(k) operation into O(1) by trading a small amount of space to store cumulative products, making both add and getProduct operations constant time.

Learn more about Math, Data Stream and Prefix Sum patterns.

Solution Approach

The solution uses a prefix product array to achieve O(1) time complexity for both operations.

Data Structure:

  • We maintain a list s where s[i] represents the product of all numbers from the start up to index i
  • Initialize s with [1] as a base value to handle edge cases

Implementation of add(num):

  1. Check if num is 0:
    • If yes, reset s to [1]
    • This effectively "forgets" all previous numbers since any product involving 0 is 0
  2. If num is not 0:
    • Calculate the new cumulative product: s[-1] * num
    • Append this value to s

Implementation of getProduct(k):

  1. Check if len(s) <= k:
    • If true, return 0 (this means a zero was added within the last k numbers)
  2. Otherwise:
    • Return s[-1] // s[-k - 1]
    • This divides the current total product by the product up to position n-k-1
    • The result is the product of elements from position n-k to n-1 (the last k elements)

Why this works:

  • When there's no zero in the last k numbers, we have a continuous prefix product array of at least k+1 elements
  • The product of numbers from index i to j equals prefix_product[j] / prefix_product[i-1]
  • For the last k numbers, this becomes s[-1] / s[-k-1]

Handling zeros:

  • Resetting to [1] when encountering 0 is crucial
  • It creates a "checkpoint" - any query spanning across this point will correctly return 0
  • The length check len(s) <= k elegantly handles this case

Time Complexity:

  • add(): O(1) - just one multiplication and append operation
  • getProduct(): O(1) - just one division operation
  • Space: O(n) where n is the number of non-zero elements added since the last zero

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 concrete example to see how the prefix product approach works.

Initial State:

  • Start with s = [1] (our base case)

Operation 1: add(2)

  • 2 is not zero, so calculate: s[-1] * 2 = 1 * 2 = 2
  • Append to get s = [1, 2]

Operation 2: add(3)

  • 3 is not zero, so calculate: s[-1] * 3 = 2 * 3 = 6
  • Append to get s = [1, 2, 6]

Operation 3: add(4)

  • 4 is not zero, so calculate: s[-1] * 4 = 6 * 4 = 24
  • Append to get s = [1, 2, 6, 24]

Operation 4: getProduct(2)

  • Check: len(s) = 4 is NOT ≤ k = 2, so proceed
  • Calculate: s[-1] // s[-2-1] = s[-1] // s[-3] = 24 // 2 = 12
  • This gives us the product of the last 2 numbers: 3 * 4 = 12

Operation 5: add(0)

  • Number is zero, so reset: s = [1]
  • All previous products are now "forgotten"

Operation 6: add(5)

  • 5 is not zero, so calculate: s[-1] * 5 = 1 * 5 = 5
  • Append to get s = [1, 5]

Operation 7: getProduct(3)

  • Check: len(s) = 2 IS ≤ k = 3
  • Return 0 (because there was a zero within the last 3 numbers)

Why the division formula works: When s = [1, 2, 6, 24], each element represents:

  • s[0] = 1 (base)
  • s[1] = 2 (product of first 1 number)
  • s[2] = 6 (product of first 2 numbers: 2*3)
  • s[3] = 24 (product of first 3 numbers: 234)

To get the product of numbers from position 2 to 3 (which are 3 and 4):

  • We take s[3] / s[1] = 24 / 2 = 12
  • This effectively "removes" the product of numbers before position 2

Solution Implementation

1class ProductOfNumbers:
2    def __init__(self):
3        """
4        Initialize the data structure.
5        Maintains a prefix product array starting with 1.
6        """
7        # Prefix product array - stores cumulative products
8        # Starting with 1 allows for easier calculation of products
9        self.prefix_products = [1]
10
11    def add(self, num: int) -> None:
12        """
13        Add a number to the stream.
14      
15        Args:
16            num: The number to add to the stream
17          
18        If num is 0, reset the prefix products array since any product 
19        containing 0 will be 0.
20        Otherwise, append the cumulative product.
21        """
22        if num == 0:
23            # Reset the array when encountering 0
24            # Any product containing 0 will be 0
25            self.prefix_products = [1]
26            return
27      
28        # Append the cumulative product
29        # New product = last cumulative product * current number
30        self.prefix_products.append(self.prefix_products[-1] * num)
31
32    def getProduct(self, k: int) -> int:
33        """
34        Return the product of the last k numbers in the stream.
35      
36        Args:
37            k: Number of recent elements to multiply
38          
39        Returns:
40            The product of the last k numbers, or 0 if any of them is 0
41        """
42        # If we don't have k elements in our prefix array,
43        # it means there was a 0 in the last k elements
44        if len(self.prefix_products) <= k:
45            return 0
46      
47        # Calculate product of last k elements using prefix products
48        # Product = (product of all elements) / (product of elements before the last k)
49        return self.prefix_products[-1] // self.prefix_products[-k - 1]
50
51
52# Your ProductOfNumbers object will be instantiated and called as such:
53# obj = ProductOfNumbers()
54# obj.add(num)
55# param_2 = obj.getProduct(k)
56
1class ProductOfNumbers {
2    // List to store cumulative products
3    // Each element at index i stores the product of all elements from index 0 to i
4    private List<Integer> cumulativeProducts;
5
6    /**
7     * Constructor initializes the list with 1 as the first element
8     * This serves as a base for calculating products
9     */
10    public ProductOfNumbers() {
11        cumulativeProducts = new ArrayList<>();
12        cumulativeProducts.add(1);
13    }
14
15    /**
16     * Adds a number to the data stream
17     * @param num The number to be added
18     */
19    public void add(int num) {
20        // If num is 0, reset the list since any product involving 0 will be 0
21        if (num == 0) {
22            cumulativeProducts.clear();
23            cumulativeProducts.add(1);
24            return;
25        }
26      
27        // Add the cumulative product by multiplying the last cumulative product with the new number
28        int lastProduct = cumulativeProducts.get(cumulativeProducts.size() - 1);
29        cumulativeProducts.add(lastProduct * num);
30    }
31
32    /**
33     * Returns the product of the last k numbers in the current list
34     * @param k The number of recent elements to calculate the product for
35     * @return The product of the last k numbers, or 0 if there was a 0 in the last k numbers
36     */
37    public int getProduct(int k) {
38        int currentSize = cumulativeProducts.size();
39      
40        // If we don't have k elements (excluding the initial 1), it means there was a 0 
41        // in the last k numbers, so return 0
42        if (currentSize <= k) {
43            return 0;
44        }
45      
46        // Calculate product of last k elements by dividing the cumulative product at the end
47        // by the cumulative product just before the k elements
48        return cumulativeProducts.get(currentSize - 1) / cumulativeProducts.get(currentSize - k - 1);
49    }
50}
51
52/**
53 * Your ProductOfNumbers object will be instantiated and called as such:
54 * ProductOfNumbers obj = new ProductOfNumbers();
55 * obj.add(num);
56 * int param_2 = obj.getProduct(k);
57 */
58
1class ProductOfNumbers {
2private:
3    vector<int> prefixProducts;  // Stores prefix products from the last zero (or beginning)
4  
5public:
6    /**
7     * Initialize the data structure
8     * Start with 1 as the initial prefix product (identity element for multiplication)
9     */
10    ProductOfNumbers() {
11        prefixProducts.push_back(1);
12    }
13  
14    /**
15     * Add a number to the back of the current list
16     * @param num: The number to be added
17     * 
18     * If num is 0, reset the prefix products array since any product 
19     * containing 0 will be 0
20     * Otherwise, append the cumulative product to the array
21     */
22    void add(int num) {
23        if (num == 0) {
24            // Reset prefix products when encountering zero
25            prefixProducts.clear();
26            prefixProducts.push_back(1);
27            return;
28        }
29      
30        // Append the new cumulative product
31        prefixProducts.push_back(prefixProducts.back() * num);
32    }
33  
34    /**
35     * Get the product of the last k numbers in the current list
36     * @param k: Number of recent elements to multiply
37     * @return: Product of the last k numbers, or 0 if there's a zero in the range
38     * 
39     * If the array size <= k, it means there's a zero in the last k elements
40     * Otherwise, divide the total product by the prefix product before the k elements
41     */
42    int getProduct(int k) {
43        int arraySize = prefixProducts.size();
44      
45        // If we don't have enough elements, there must be a zero in the range
46        if (arraySize <= k) {
47            return 0;
48        }
49      
50        // Product of last k elements = total product / product before those k elements
51        return prefixProducts.back() / prefixProducts[arraySize - k - 1];
52    }
53};
54
55/**
56 * Your ProductOfNumbers object will be instantiated and called as such:
57 * ProductOfNumbers* obj = new ProductOfNumbers();
58 * obj->add(num);
59 * int param_2 = obj->getProduct(k);
60 */
61
1// Array to store cumulative products
2// Initialize with 1 as the identity element for multiplication
3let cumulativeProducts: number[] = [1];
4
5/**
6 * Adds a number to the stream and updates the cumulative product array
7 * If the number is 0, reset the array since any product containing 0 will be 0
8 * Otherwise, append the cumulative product up to this point
9 * @param num - The number to add to the stream
10 */
11function add(num: number): void {
12    if (num === 0) {
13        // Reset the array when encountering 0
14        // All previous products become irrelevant
15        cumulativeProducts = [1];
16    } else {
17        // Calculate and store the cumulative product
18        const currentLength = cumulativeProducts.length;
19        cumulativeProducts[currentLength] = cumulativeProducts[currentLength - 1] * num;
20    }
21}
22
23/**
24 * Returns the product of the last k numbers in the stream
25 * Uses the cumulative product array to calculate in O(1) time
26 * @param k - The number of recent elements to multiply
27 * @returns The product of the last k numbers, or 0 if k exceeds available elements
28 */
29function getProduct(k: number): number {
30    const currentLength = cumulativeProducts.length;
31  
32    // If k is larger than available elements (excluding the initial 1), return 0
33    // This also handles cases where a 0 was encountered within the last k elements
34    if (k > currentLength - 1) {
35        return 0;
36    }
37  
38    // Calculate product using division of cumulative products
39    // Product of last k elements = cumulativeProducts[n-1] / cumulativeProducts[n-k-1]
40    return cumulativeProducts[currentLength - 1] / cumulativeProducts[currentLength - k - 1];
41}
42

Time and Space Complexity

Time Complexity:

  • __init__(): O(1) - Simply initializes a list with one element
  • add(num): O(1) - Either resets the list to [1] when num == 0, or appends a single calculated value (self.s[-1] * num) to the list
  • getProduct(k): O(1) - Performs a length check and at most one division operation between two list elements accessed by index

Overall, each operation has O(1) time complexity.

Space Complexity:

  • The space complexity is O(n), where n is the total number of non-zero elements added since the last zero (or since initialization)
  • The list self.s grows by one element for each non-zero number added via add()
  • When a zero is added, the list resets to [1], effectively releasing previous memory
  • In the worst case where no zeros are added, the list grows to size n + 1 (including the initial 1), resulting in O(n) space complexity

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

Common Pitfalls

1. Integer Overflow in Prefix Products

The prefix product array can grow extremely large when continuously multiplying positive integers. For example, adding just twenty 10s would result in 10^20, which exceeds typical integer limits in many languages.

Solution:

  • In Python, integers have arbitrary precision, so overflow isn't an issue
  • In languages like Java/C++, use long long or consider using modular arithmetic if the problem specifies a modulo value
  • Alternatively, store logarithms of products and convert back when needed (though this introduces floating-point precision issues)

2. Incorrect Handling of Multiple Zeros

A common mistake is not properly resetting the prefix array when encountering zeros. Some might try to track zero positions separately or use complex logic to handle multiple zeros.

Why the current approach works:

# Incorrect approach - trying to track zeros separately
class ProductOfNumbers:
    def __init__(self):
        self.products = []
        self.zero_indices = []
  
    # This becomes unnecessarily complex!

Correct approach (as shown):

if num == 0:
    self.prefix_products = [1]  # Simple reset handles all cases

3. Off-by-One Error in Index Calculation

When calculating getProduct(k), it's easy to make indexing errors:

Common mistake:

# Wrong - this would get k+1 elements
return self.prefix_products[-1] // self.prefix_products[-k]

# Wrong - this would skip the last element
return self.prefix_products[-2] // self.prefix_products[-k-1]

Correct calculation:

return self.prefix_products[-1] // self.prefix_products[-k-1]

4. Not Initializing with [1]

Starting the prefix array with an empty list or [0] instead of [1] breaks the algorithm:

Wrong initialization:

def __init__(self):
    self.prefix_products = []  # Would require special handling for first element
    # OR
    self.prefix_products = [0]  # Would make all products 0

Correct initialization:

def __init__(self):
    self.prefix_products = [1]  # Serves as identity element for multiplication

5. Using Regular Division Instead of Integer Division

In Python 3, using / instead of // returns a float, which may cause issues:

Wrong:

return self.prefix_products[-1] / self.prefix_products[-k-1]  # Returns float

Correct:

return self.prefix_products[-1] // self.prefix_products[-k-1]  # Returns integer

6. Not Understanding Why len(s) <= k Returns 0

This condition is subtle but crucial. When len(prefix_products) <= k, it means:

  • We've seen fewer than k non-zero numbers since the last zero
  • Therefore, the last k numbers must include at least one zero
  • So the product should be 0

Example to illustrate:

# After adding [2, 3, 0, 4, 5]
# prefix_products = [1, 4, 20]  (reset at 0, then built with 4, 5)
# getProduct(3) should return 0 because the 3rd-last number was 0
# len(prefix_products) = 3, k = 3, so 3 <= 3 is true → return 0 ✓
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

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

Load More