title: Subarray Product Less Than K
slug: subarray-product-less-than-k
difficulty: medium
leetcode_id: 713
leetcode_url: https://leetcode.com/problems/subarray-product-less-than-k/
categories:
  - arrays
  - two-pointers
patterns:
  - sliding-window

description: |
  Given an array of integers `nums` and an integer `k`, return *the number of contiguous subarrays where the product of all the elements in the subarray is strictly less than* `k`.

constraints: |
  - `1 <= nums.length <= 3 * 10^4`
  - `1 <= nums[i] <= 1000`
  - `0 <= k <= 10^6`

examples:
  - input: "nums = [10, 5, 2, 6], k = 100"
    output: "8"
    explanation: "The 8 subarrays that have product less than 100 are: [10], [5], [2], [6], [10, 5], [5, 2], [2, 6], [5, 2, 6]. Note that [10, 5, 2] is not included as the product of 100 is not strictly less than k."
  - input: "nums = [1, 2, 3], k = 0"
    output: "0"
    explanation: "Since k = 0, no product of positive integers can be less than 0, so the answer is 0."

explanation:
  intuition: |
    Imagine you're sliding a flexible window across the array, and inside this window you're multiplying all the numbers together. The window can expand by including more elements from the right, but if the product becomes too large (>= k), you need to shrink the window from the left until the product is valid again.

    The key insight is this: **when the window is valid (product < k), every subarray ending at the current right pointer and starting anywhere within the window is also valid**. If your window spans from index `left` to `right`, then the subarrays ending at `right` are: `[right]`, `[right-1, right]`, `[right-2, right-1, right]`, ... all the way to `[left, ..., right]`. That's exactly `right - left + 1` new valid subarrays.

    Think of it like this: each time you extend the window to include a new element, you're asking "how many new valid subarrays does this create?" The answer is the current window size, because each starting position within the window gives you a unique subarray ending at the current position.

  approach: |
    We solve this using the **Sliding Window** technique:

    **Step 1: Handle edge cases**

    - If `k <= 1`, return `0` immediately since all elements are >= 1, so no product can be < 1 (or < 0)

    &nbsp;

    **Step 2: Initialise variables**

    - `left`: Left boundary of window, starts at `0`
    - `product`: Running product of elements in current window, starts at `1`
    - `count`: Total count of valid subarrays, starts at `0`

    &nbsp;

    **Step 3: Expand the window**

    - Iterate `right` from `0` to `n-1`
    - Multiply `product` by `nums[right]` to include the new element

    &nbsp;

    **Step 4: Shrink if needed**

    - While `product >= k` and `left <= right`:
      - Divide `product` by `nums[left]` to remove the leftmost element
      - Increment `left` to shrink the window

    &nbsp;

    **Step 5: Count valid subarrays**

    - After shrinking, the window from `left` to `right` is valid
    - Add `right - left + 1` to `count` (this counts all subarrays ending at `right`)

    &nbsp;

    **Step 6: Return result**

    - Return `count` after processing all elements

  common_pitfalls:
    - title: Missing the Edge Case k <= 1
      description: |
        Since all elements in `nums` are >= 1, the product of any non-empty subarray is at least 1. If `k <= 1`, no subarray can have a product strictly less than `k`.

        Forgetting this check can lead to incorrect results or infinite loops when the window never becomes valid.
      wrong_approach: "Not handling k <= 1 specially"
      correct_approach: "Return 0 immediately if k <= 1"

    - title: Counting Subarrays Incorrectly
      description: |
        A common mistake is to count all subarrays within the window at each step, leading to overcounting. Or counting only 1 subarray per valid window position.

        The correct insight is: **at each position `right`, we add exactly `right - left + 1` new subarrays** — these are all the subarrays that *end* at `right` and start anywhere from `left` to `right`.
      wrong_approach: "Counting window_size * (window_size + 1) / 2 or just 1"
      correct_approach: "Add right - left + 1 at each step"

    - title: The Brute Force Trap
      description: |
        The naive approach checks every possible subarray and computes its product:
        - Outer loop for start index
        - Inner loop for end index
        - Multiply all elements in each subarray

        This is O(n^2) or O(n^3) depending on implementation. With `nums.length <= 3 * 10^4`, this will cause **Time Limit Exceeded**.
      wrong_approach: "Nested loops checking all subarrays"
      correct_approach: "Sliding window with O(n) time"

    - title: Integer Overflow Concerns
      description: |
        With `nums[i] <= 1000` and `nums.length <= 3 * 10^4`, the product can become extremely large. However, since we shrink the window whenever `product >= k` and `k <= 10^6`, the product stays bounded.

        In languages without arbitrary precision integers, be aware of potential overflow, though Python handles this naturally.

  key_takeaways:
    - "**Sliding window for counting**: When counting valid subarrays with a monotonic property, sliding window often gives O(n) time"
    - "**Count subarrays ending at each position**: Adding `right - left + 1` at each step counts all new subarrays introduced by expanding to `right`"
    - "**Product vs sum windows**: For products, divide to shrink (vs subtract for sums). This works because all elements are positive"
    - "**Related problems**: This pattern applies to subarray sum problems, longest substring problems, and other contiguous sequence counting tasks"

  time_complexity: "O(n). Each element is added to the window once (when `right` advances) and removed at most once (when `left` advances)."
  space_complexity: "O(1). We only use a fixed number of variables (`left`, `product`, `count`) regardless of input size."

solutions:
  - approach_name: Sliding Window
    is_optimal: true
    code: |
      def numSubarrayProductLessThanK(nums: list[int], k: int) -> int:
          # Edge case: no product of positive integers can be < 1
          if k <= 1:
              return 0

          left = 0
          product = 1
          count = 0

          for right in range(len(nums)):
              # Expand window by including nums[right]
              product *= nums[right]

              # Shrink window from left while product is too large
              while product >= k:
                  product //= nums[left]
                  left += 1

              # All subarrays ending at 'right' and starting from 'left' to 'right' are valid
              # That's (right - left + 1) subarrays
              count += right - left + 1

          return count
    explanation: |
      **Time Complexity:** O(n) — Each element enters and leaves the window at most once.

      **Space Complexity:** O(1) — Only three variables used.

      The sliding window maintains the invariant that the product of elements from `left` to `right` is always less than `k`. At each position, we count all valid subarrays ending at that position.

  - approach_name: Brute Force
    is_optimal: false
    code: |
      def numSubarrayProductLessThanK(nums: list[int], k: int) -> int:
          n = len(nums)
          count = 0

          # Try every starting position
          for i in range(n):
              product = 1
              # Extend to every ending position
              for j in range(i, n):
                  product *= nums[j]
                  # If product is still valid, count this subarray
                  if product < k:
                      count += 1
                  else:
                      # Product can only grow, so no point continuing
                      break

          return count
    explanation: |
      **Time Complexity:** O(n^2) — Nested loops, though we break early when product exceeds k.

      **Space Complexity:** O(1) — Only tracking product and count.

      This approach tries every starting index and extends until the product becomes too large. While the early break helps in practice, worst case is still O(n^2). Too slow for large inputs but demonstrates the problem structure.
