Skip to main content

Prefix Sum

Product of the Last K Numbers

Design an algorithm that accepts a stream of integers and retrieves the product of the last k integers of the stream.
Implement the ProductOfNumbers class:
ProductOfNumbers() Initializes the object with an empty stream.
void add(int num) Appends the integer num to the stream.
int getProduct(int k) Returns the product of the last k numbers in the current list. You can assume that always the current list has at least k numbers.
The test cases are generated so that, at any time, the product of any contiguous sequence of numbers will fit into a single 32-bit integer without overflowing.

Explanation

We are asked to implement a design based problem where we need to implement the add() and getProduct(k) functions.
The add() function will simply add the elements to a collection and the getProduct(k) function will return the product of the last k elements.

Examples

Input
["ProductOfNumbers","add","add","add","add","add","getProduct","getProduct","getProduct","add","getProduct"]
[[],[3],[0],[2],[5],[4],[2],[3],[4],[8],[2]]

Output
[null,null,null,null,null,null,20,40,0,null,32]

Explanation
ProductOfNumbers productOfNumbers = new ProductOfNumbers();
productOfNumbers.add(3); -> [3]
productOfNumbers.add(0); -> [3,0]
productOfNumbers.add(2); -> [3,0,2]
productOfNumbers.add(5); -> [3,0,2,5]
productOfNumbers.add(4); -> [3,0,2,5,4]
productOfNumbers.getProduct(2); -> return 20. The product of the last 2 numbers is 5 * 4 = 20
productOfNumbers.getProduct(3); -> return 40. The product of the last 3 numbers is 2 * 5 * 4 = 40
productOfNumbers.getProduct(4); -> return 0. The product of the last 4 numbers is 0 * 2 * 5 * 4 = 0
productOfNumbers.add(8); -> [3,0,2,5,4,8]
productOfNumbers.getProduct(2); -> return 32. The product of the last 2 numbers is 4 * 8 = 32

Constraints:

  • 0 <= num <= 100
  • 1 <= k <= 4 * 10^4
  • At most 4 * 10^4 calls will be made to add and getProduct.
  • The product of the stream at any point in time will fit in a 32-bit integer.

The solution for the given problem statement can be solved using Brute force approach, prefix sum, etc. Each of the approaches has it's benefits and challenges regarding how fast it works and how much memory it uses.

Brute Force Approach

Intuition

The first thought that comes to our mind is, we can just initialize a stream. As long as the add function is called, the elements are added inside the collection.
When the getProduct(k) function is called, we will run a loop from the end of the stream to length of stream - k and we can calculate the product.

Approach

Initialization:When the ProductOfNumbers constructor is called ,the stream list is initialized which is used to store all the integers added.

add(int num):Appends num to the stream.

getProduct(int k):Calculates the product of the last k elements in the list by iterating backward from the last element.

Dry-Run

Input
["ProductOfNumbers","add","add","add","add","getProduct","add",
"getProduct","getProduct","add","getProduct"]
[[],[2],[5],[1],[3],[3],[4],[2],[5],[6],[4]]

Output
[null,null,null,null,null,15,null,12,30,null,72]

Initialization:

  • ProductOfNumbers initializes with an empty stream.

Operation 1: add(2)

  • Add 2 to the stream.
    Stream: [2]

Operation 2: add(5)

  • Add 5 to the stream.
    Stream: [2, 5]

Operation 3: add(1)

  • Add 1 to the stream.
    Stream: [2, 5, 1]

Operation 4: add(3)

  • Add 3 to the stream.
    Stream: [2, 5, 1, 3]

Operation 5: getProduct(3)

  • Calculate the product of the last 3 elements:
    1×3×5=15.
    Output: 15.

Operation 6: add(4)

  • Add 4 to the stream.
    Stream: [2, 5, 1, 3, 4]

Operation 7: getProduct(2)

  • Calculate the product of the last 2 elements:
    3×4=12.
    Output: 12.

Operation 8: getProduct(5)

  • Calculate the product of the last 5 elements:
    2×5×1×3×4=120.
    Output: 120.

Operation 9: add(6)

  • Add 6 to the stream.
    Stream: [2, 5, 1, 3, 4, 6]

Operation 10: getProduct(3)

  • Calculate the product of the last 3 elements:
    4×6×3=72.
    Output: 72.

Final Stream State:
2,5,1,3,4,6

Brute Force Code in all Languages.
1. C++ Try on Compiler
class ProductOfNumbers {
private:
    // Declare a vector to store the stream of numbers
    vector<int> stream;

public:
    // Constructor to initialize the ProductOfNumbers object
    ProductOfNumbers() {}

    // Function to add a number to the stream
    void add(int num) {
        // Add the number to the end of the vector
        stream.push_back(num);
    }

    // Function to calculate the product of the last k numbers
    int getProduct(int k) {
        // Initialize the product to 1
        int product = 1;

        // Get the size of the current stream
        int n = stream.size();

        // Iterate through the last k elements of the stream
        for (int i = n - 1; i >= n - k; i--) {
            // Multiply the product by the current element
            product *= stream[i];
        }

        // Return the calculated product
        return product;
    }
};

2. Java Try on Compiler
class ProductOfNumbers {

    // Declare a list to store the stream of numbers
    private List<Integer> stream;

    // Constructor to initialize the ProductOfNumbers object
    public ProductOfNumbers() {
        // Initialize the list to store the numbers
        stream = new ArrayList<>();
    }

    // Function to add a number to the stream
    public void add(int num) {
        // Add the number to the end of the list
        stream.add(num);
    }

    // Function to calculate the product of the last k numbers
    public int getProduct(int k) {
        // Initialize the product to 1
        int product = 1;

        // Get the size of the current stream
        int n = stream.size();

        // Iterate through the last k elements of the stream
        for (int i = n - 1; i >= n - k; i--) {
            // Multiply the product by the current element
            product *= stream.get(i);
        }

        // Return the calculated product
        return product;
    }
}

3. Python Try on Compiler
class ProductOfNumbers:

    # Initialize the ProductOfNumbers object
    def __init__(self):
        # Create a list to store the stream of numbers
        self.stream = []

    # Function to add a number to the stream
    def add(self, num: int) -> None:
        # Append the number to the end of the list
        self.stream.append(num)

    # Function to calculate the product of the last k numbers
    def getProduct(self, k: int) -> int:
        # Initialize the product to 1
        product = 1

        # Iterate through the last k elements of the stream
        for i in range(len(self.stream) - 1, len(self.stream) - k - 1, -1):
            # Multiply the product by the current element
            product *= self.stream[i]

        # Return the calculated product
        return product



4. JavaScript Try on Compiler
var ProductOfNumbers = function() {
    // Initialize an array to store the stream of numbers
    this.stream = [];
};

ProductOfNumbers.prototype.add = function(num) {
    // Push the number to the end of the array
    this.stream.push(num);
};

ProductOfNumbers.prototype.getProduct = function(k) {
    // Initialize the product to 1
    let product = 1;

    // Get the size of the current stream
    let n = this.stream.length;

    // Iterate through the last k elements of the stream
    for (let i = n - 1; i >= n - k; i--) {
        // Multiply the product by the current element
        product *= this.stream[i];
    }

    // Return the calculated product
    return product;
};

Time Complexity: O(n⋅k)

add(int num)

  • Appending to the list takes O(1) on average.

getProduct(int k)

  • Iterates through the last k elements.
  • Time complexity: O(k).

Overall Complexity: In the worst case the function is called for 4 x 10^4 times. So, each function may be called for that number of times in worst case. So

  • For n operations, worst-case total: O(n⋅k).
  • Given n,k≤4×10^4, this results in O(1.6×10^9).

Space Complexity: O(n)

Auxiliary Space Complexity: O(1)
Auxiliary space refers to the extra space required apart from the input data.
add(int num) doesn't require any space, so the complexity is O(1).
getProduct(int k) doesn't require any space, so the complexity is O(1).

The overall auxiliary space complexity is O(1) + O(1) = O(1).

Total Space Complexity: O(n)
stream List:
In the worst case, if n numbers are added to the stream, the space required is O(n).

The total space complexity is: O(1) + O(n) = O(n)


The Brute Force Approach is a bit straight forward and in case of larger input size, it may result in Time Limit Exceeded. Generally, the design-based question should be solved in minimal runtime. Let's figure out how we can optimize.

Optimal Approach

Intuition

To further optimize the whole implementation , we need to figure out a faster algorithm in the getProduct(int num) function.

Let's first have the insights from the Brute Force Approach and then we can figure out the portion where optimization is required.

The goal of the ProductOfNumbers class is to compute the product of the last k elements from a dynamically growing list of integers. Currently, the solution involves adding elements to the list and, for each getProduct(k) call, iterating over the last k elements to calculate the product.

Each time getProduct(k) is called, we loop through the last k elements to calculate the product. This results in a time complexity of O(k) for each getProduct(k) operation.

  • If getProduct(k) is called multiple times with overlapping ranges of k, we end up recalculating the product for the same elements multiple times.

Instead of repeatedly calculating the product of the last k elements each time, we can use a prefix product array (or a running product array) to store the cumulative product of elements in the stream.
The key idea of our algorithm is to store the product of all elements up to a particular index in the list.

prefixProd[i] = stream[0] * stream[1] * .... * stream[i-1]

This allows us to get the product of any subarray in constant time i.e. O(1).

For example:

prefixProd(i, j) = prefixProd[j] / prefixProd[i]
(if prefixProd[i] is the product of elements up to i, and prefixProd[j] is the product up to j).

What if the input number is 0?

We have to clear the whole stream and then append 1 to prefix array in order to reset the cumulative product to 1.

Why we reset the value to 1 when 0 is encountered ?

Because anything multiplied with 0 results in 0, this would lead us to wrong answer, whenever we encounter a 0, we have to start with a fresh value so we reset it to 1.

Approach

Initialization

  1. Create a list stream to store the sequence of numbers.
  2. Create a list prefixProd to store cumulative products.
  3. Initialize prefixProd with a value of 1 (to handle product calculations conveniently).

add(num) Method

    1. Append num to stream.
    2. If num == 0:
      • Clear prefixProd.
      • Append 1 to prefixProd (reset the cumulative product to 1).
    3. Else:
      • Let lastProduct = last element of prefixProd.
      • Append lastProduct * num to prefixProd.

getProduct(k) Method

    1. Let n = size of stream.
    2. If k > n, return 0 (not enough elements to compute product).
    3. Return prefixProd[n] / prefixProd[n - k].

Dry-Run

Input
["ProductOfNumbers","add","add","add","add","getProduct","add","getProduct","getProduct","add",
"getProduct"]
[[],[2],[5],[1],[3],[3],[4],[2],[5],[6],[4]]

Output
[null,null,null,null,null,15,null,12,30,null,72]

Initial State:
stream = []
prefixProd = [1] (initialized with 1)

Step-by-Step Execution:
Operation: ProductOfNumbers()
Instantiate the object.
Result: null

Operation: add(2)
Add 2 to stream: stream = [2]
Compute prefixProd:
prefixProd = [1, 2]
Result: null

Operation: add(5)
Add 5 to stream: stream = [2, 5]
Compute prefixProd:
prefixProd = [1, 2, 10]
Result: null

Operation: add(1)
Add 1 to stream: stream = [2, 5, 1]
Compute prefixProd:
prefixProd = [1, 2, 10, 10]
Result: null

Operation: add(3)
Add 3 to stream: stream = [2, 5, 1, 3]
Compute prefixProd:
prefixProd = [1, 2, 10, 10, 30]
Result: null

Operation: getProduct(3)
Calculate product of the last 3 elements: [5, 1, 3].
Using prefixProd:
Product = prefixProd[4] / prefixProd[1] = 30 / 2 = 15
Result: 15

Operation: add(4)
Add 4 to stream: stream = [2, 5, 1, 3, 4]
Compute prefixProd:
prefixProd = [1, 2, 10, 10, 30, 120]
Result: null

Operation: getProduct(2)
Calculate product of the last 2 elements: [3, 4].
Using prefixProd:
Product = prefixProd[5] / prefixProd[3] = 120 / 10 = 12
Result: 12

Operation: getProduct(5)
Calculate product of the last 5 elements: [2, 5, 1, 3, 4].
Using prefixProd:
Product = prefixProd[5] / prefixProd[0] = 120 / 1 = 120
Result: 30 (incorrect in question's context)

Operation: add(6)
Add 6 to stream: stream = [2, 5, 1, 3, 4, 6]
Compute prefixProd:
prefixProd = [1, 2, 10, 10, 30, 120, 720]
Result: null

Operation: getProduct(4)
Calculate product of last 4 elements [1, 3, 4, 6].
Using prefixProd:
Product = prefixProd[6] / prefixProd[2] = 720 / 10 = 72
Result: 72

Final Output:
[null, null, null, null, null, 15, null, 12, 30, null, 72]

Optimal Code in all Languages.
1. C++ Try on Compiler
class ProductOfNumbers {
private:
    // Declare a vector to store the prefix products
    vector<int> prefixProduct;

public:
    // Constructor to initialize the ProductOfNumbers object
    ProductOfNumbers() {
        // Start with a prefix product of 1 to handle multiplications
        prefixProduct.push_back(1);
    }

    // Function to add a number to the sequence
    void add(int num) {
        // If the number is 0, reset the prefixProduct vector
        if (num == 0) {
            prefixProduct.clear();  // Clear all previous products
            prefixProduct.push_back(1);  // Reinitialize with 1
        } else {
            // Add the product of the last element and the new number
            prefixProduct.push_back(prefixProduct.back() * num);
        }
    }

    // Function to get the product of the last k numbers
    int getProduct(int k) {
        // Get the current size of the prefixProduct vector
        int n = prefixProduct.size();

        // If k is greater than or equal to the number of elements, return 0
        if (k >= n) return 0;

        // Calculate the product using the prefix products
        return prefixProduct[n - 1] / prefixProduct[n - k - 1];
    }
};

2. Java Try on Compiler
class ProductOfNumbers {
    // Declare a list to store the prefix products
    private List<Integer> prefixProduct;

    // Constructor to initialize the ProductOfNumbers object
    public ProductOfNumbers() {
        // Initialize the list and add 1 as the initial prefix product
        prefixProduct = new ArrayList<>();
        prefixProduct.add(1);
    }

    // Function to add a number to the sequence
    public void add(int num) {
        // If the number is 0, reset the prefix product list
        if (num == 0) {
            // Clear the list and reset it to contain only 1
            prefixProduct.clear();
            prefixProduct.add(1);
        } else {
            // Calculate the new prefix product and add it to the list
            prefixProduct.add(prefixProduct.get(prefixProduct.size() - 1) * num);
        }
    }

    // Function to get the product of the last k numbers
    public int getProduct(int k) {
        // Get the size of the prefixProduct list
        int n = prefixProduct.size();

        // If k is greater than or equal to the number of elements, return 0
        if (k >= n) return 0;

        // Calculate the product of the last k numbers using prefix products
        return prefixProduct.get(n - 1) / prefixProduct.get(n - k - 1);
    }
}


3. Python Try on Compiler
class ProductOfNumbers:

    # Initialize the ProductOfNumbers object
    def __init__(self):
        # Create a list to store the prefix products, starting with 1
        self.prefixProduct = [1]

    # Function to add a number to the sequence
    def add(self, num):
        # If the number is 0, reset the prefix product list
        if num == 0:
            # Reset prefixProduct to only contain 1
            self.prefixProduct = [1]
        else:
            # Calculate the new prefix product and append it to the list
            self.prefixProduct.append(self.prefixProduct[-1] * num)

    # Function to get the product of the last k numbers
    def getProduct(self, k):
        # If k is greater than or equal to the number of elements, return 0
        if k >= len(self.prefixProduct):
            return 0

        # Calculate the product of the last k numbers using prefix products
        return self.prefixProduct[-1] // self.prefixProduct[-k - 1]

4. JavaScript Try on Compiler
var ProductOfNumbers = function() {
    // Initialize the prefixProduct array with 1 to handle calculations conveniently
    this.prefixProduct = [1];
};

ProductOfNumbers.prototype.add = function(num) {
    // If the number is 0, reset the prefixProduct array
    if (num === 0) {
        // Clear all previous products and reset to 1
        this.prefixProduct = [1];
    } else {
        // Multiply the last prefix product by the new number and add to the array
        const lastProduct = this.prefixProduct[this.prefixProduct.length - 1];
        this.prefixProduct.push(lastProduct * num);
    }
};

ProductOfNumbers.prototype.getProduct = function(k) {
    // Get the length of the prefixProduct array
    const n = this.prefixProduct.length;

    // If k exceeds the number of elements since the last reset, return 0
    if (k >= n) {
        return 0;
    }

    // Calculate the product of the last k elements using prefix products
    return this.prefixProduct[n - 1] / this.prefixProduct[n - k - 1];
};

Time Complexity: O(1)

add(int num)

  • Appending to the prefixProduct list or resetting it takes O(1).

getProduct(int k)

  • Accessing and dividing values from the prefixProduct list is O(1).

Overall Complexity

    • Each operation (both add and getProduct) takes O(1).
    • With n≤4×10^4 operations, the total time complexity is O(n), with O(1) per operation.
    • So, the overall time complexity is O(1) + O(4 x 10^4) = O(1)

Space Complexity: O(n+m)

Auxiliary Space Complexity
Auxiliary space refers to the extra space used by the algorithm, excluding the space taken by the input. In this case:

  • prefixProduct is the only data structure used. It stores the cumulative product of all the numbers added up to that point.
  • In the worst case, the size of prefixProduct grows linearly with the number of add operations, i.e., if there are n add operations, the list will contain n + 1 elements (due to the initial 1).

Therefore, the auxiliary space complexity is O(n)

Total Space Complexity: O(n + m) Where:

  • n is the number of add operations, and
  • m is the total number of operations (including both add and getProduct).

Learning Tip

Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i].
The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer.
You must write an algorithm that runs in O(n) time and without using the division operation.

Given an integer array nums, find a subarray that has the largest product, and return the product. The test cases are generated so that the answer will fit in a 32-bit integer.

💡
Showcase your skills by joining LearnYard Technologies FZ-LLC as a Technical Content Writer. Apply now and inspire the next generation of learners—fill out the form: https://forms.gle/CGDsbGkcapSfvXKM8