DSA sheet · Arrays · Sliding Window pattern

Subarray Product Less Than K

This is the first "count the windows" problem of the sliding window pattern. The teacher first writes a brute force with two loops (the inner loop walks backwards from each index), finds an important base case from the constraints (k ≤ 1 → answer 0), then goes through her four array patterns to pick sliding window. The heart of the video is one line: count += right − left + 1. She spends a lot of time showing why that formula counts every valid subarray exactly once, and so do these notes.

Why it matters: "count subarrays whose (sum / product / distinct count …) stays within a limit" always uses this same right − left + 1 trick. Later problems (like Subarrays with K Different Integers) build directly on it.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a subarray?

A subarray is a piece of the array whose elements sit next to each other (we say contiguous). You pick a start index and an end index and take everything between them. You are not allowed to skip an element in the middle. (For strings, the same idea is called a substring.)

index0123
nums10526[5, 2] is a subarray
nums10526[10, 2] is not (5 is skipped)

An array of length n has n·(n+1)/2 non-empty subarrays. For n = 4 that's 10.

The brute force over all windows, and why it repeats work

The simplest way to solve any "subarray" question is: fix one end, move the other end one step at a time, and check every piece. That is two nested loops, so about n²/2 checks → O(n²).

The waste: the piece [5, 2, 6] and the piece [5, 2] share almost everything. The brute force forgets what it learned about [5, 2] and starts again from scratch for the next start index. Sliding window keeps what it already knows and only adjusts the edges.

The window [left..right]

A window is just the current subarray, described by two pointers: left (its first index) and right (its last index). Its length is right − left + 1. We keep a small summary of what's inside (a sum, a product, a count map…) so we never re-add the whole window.

Both pointers only ever move forward. Neither ever goes back. That is why the whole thing is O(n).

Fixed-size vs variable-size windows

Fixed-size windowVariable-size window
question looks like"every subarray of size k…""longest / shortest / how many subarrays such that …"
how it movesadd nums[right]; once the size passes k, remove nums[right − k]grow with right; when the window breaks the rule, shrink with left until it's fine again
this problem?no, the size isn't givenyes

Three kinds of variable-window questions

kindwhen do we record the answer?example
longest valid windowafter shrinking back to valid: best = max(best, right − left + 1)Fruits Into Baskets, Max Consecutive Ones III
shortest valid windowinside the shrinking loop, while it's still valid: best = min(best, right − left + 1), then shrinkMinimum Size Subarray Sum
count of valid windowsafter shrinking back to valid: count += right − left + 1this problem

A related trick you'll meet later: "count subarrays with exactly K of something" = atMost(K) − atMost(K − 1). We don't need it here, because "product < k" is already an "at most" style rule.

Why sliding window needs a "monotonic" rule

Shrinking is only safe if removing an element always moves the window towards valid, and growing only ever moves it towards invalid. Here every number is ≥ 1, so:

If numbers could be 0 or negative, or fractions below 1, this promise breaks, and the window can skip answers. (With sums, the same thing happens when negative numbers are allowed.)

Other tools you'll see in this pattern (not needed here)

The teacher's 4 array patterns

Whenever she wants to optimise an array problem, she checks four patterns from the first video of the playlist:

patternwhen it fits
Two pointersthe array is sorted and only the two end points matter (e.g. to compute a length or decide which pointer moves)
Sliding windowwe care about everything between two points, a contiguous range
Prefix sumcontinuous sums between indexes, often many range-sum queries
Kadane'smaximum sum when numbers can be negative

Part A · Brute force: every subarray, walking left from each index

LeetCode 713

1The question in simple words

You get an array of positive integers nums and an integer k. Return how many subarrays have a product (all elements multiplied together) that is strictly less than k. "Strictly" matters: if k = 100, a product of 99 counts, but 100 does not.

index0123
nums10526k = 100 → answer 8

The teacher lists every subarray and its product, grouped by where it starts:

starts atsubarrayproduct< 100?
10[10]10yes
[10, 5]50yes
[10, 5, 2]100no (equal is not less)
[10, 5, 2, 6]600no
5[5]5yes
[5, 2]10yes
[5, 2, 6]60yes
2[2]2yes
[2, 6]12yes
6[6]6yes

Valid products: 10, 50, 5, 10, 60, 2, 12, 6 → 8 subarrays, which matches the expected output.

2What the constraints tell us

The base case: k ≤ 1 → answer 0

The smallest possible product of any subarray is 1 (every number is ≥ 1, and multiplying more numbers ≥ 1 never goes below that). We need a product strictly less than k.

Base caseif k <= 1: return 0 — no subarray can ever be valid.

3Intuition: list every subarray, multiply, and count

If we could visit every subarray, multiply its numbers and check "< k?", we'd just do count += 1 each time it's true. Two loops can list all subarrays: one finger i fixes one end, a second finger j walks away from it.

The teacher points out that j may walk in either direction from i, as long as you pick one. She chooses to walk leftwards: j goes from i down to 0. So for each i, we list all subarrays that end at i.

4Building the logic from the example

A product variable, starting at 1 (not 0)

For sums we start at 0. For a product we must start at 1. If we started at 0, multiplying anything into it would keep it 0 forever. And just like a sum, the product must be reset for each new i, because a new i means a new family of subarrays.

Walk it on [10, 5, 2, 6], k = 100

Why we can break the moment it fails

Once [10, 5, 2, 6] has product ≥ k, could stretching it further (say there were one more 5 on the left) bring it back under k? No. Multiplying by a number ≥ 1 can never make the product smaller. So every longer subarray in this direction also fails. There's no point continuing → break.

Doubt 1: in the earlier "Subarray Sum Equals K" problem we couldn't break early. Why is it allowed here?
→ That problem had negative numbers, so a sum that went too high could come back down. Here every number is ≥ 1, so the product can only grow (or stay the same) as the subarray gets longer. Once it fails, it fails forever in that direction.
Doubt 2: she explained the subarrays going rightwards ([10], [10,5], …) but coded j going leftwards. Does it matter?
→ No. Rightwards lists subarrays by where they start; leftwards lists them by where they end. Both visit every subarray exactly once and both can break early. Leftwards has a bonus: "count of valid subarrays that end at i" is exactly the idea the sliding window uses in Part B.

5Approach steps

  1. If k ≤ 1, return 0.
  2. count = 0.
  3. For each end index i from 0 to n − 1: set product = 1.
  4. For j from i down to 0: multiply in nums[j].
  5. If product < k, count it. Otherwise break.
  6. Return count.

6Code (Python)

Brute force: O(n²) in the worst case
class Solution:
    def numSubarrayProductLessThanK(self, nums, k):
        if k <= 1:                          # no product can be < 1
            return 0
        n = len(nums)
        count = 0
        for i in range(n):                  # subarrays that END at i
            product = 1                     # fresh product, starts at 1
            for j in range(i, -1, -1):      # j walks left: i, i-1, ..., 0
                product *= nums[j]          # product of nums[j..i]
                if product < k:
                    count += 1
                else:
                    break                   # longer ones are even bigger
        return count

7Code line by line

linewhat it means
if k <= 1: return 0The base case from the constraints: products are always ≥ 1, so nothing is strictly below 0 or 1.
for i in range(n):Fix the end of the subarray.
product = 1Reset for this end. 1 is the "empty product" (multiplying by 1 changes nothing).
for j in range(i, -1, -1):Walk the start leftwards from i to 0. Python's range(i, -1, -1) means i, i−1, …, 0.
product *= nums[j]Stretch the subarray one step left. Now product = nums[j] × … × nums[i].
if product < k: count += 1Valid subarray, count it.
else: breakFailed. Stretching further can only make it bigger, so stop this i.

8Dry run (hand table)

nums = [10, 5, 2, 6], k = 100. Each row is one end index i; the products are listed as j walks left.

i (end)products as j goes i → 0valid onescount after
010 ✓[10]1
15 ✓, 50 ✓[5], [10,5]3
22 ✓, 10 ✓, 100 ✗ break[2], [5,2]5
36 ✓, 12 ✓, 60 ✓, 600 ✗ break[6], [2,6], [5,2,6]8

Final answer: 8 ✓. Notice the "valid ones" column: for each end, the valid subarrays are a block of consecutive starts right next to the end. Keep that in mind for Part B.

9Complexity & remember

The teacher notes that this code actually gets accepted, but slowly. The reason it doesn't always hit n²: the break often stops j early (for example, if the first two numbers already multiply past k, j never goes further). It's still O(n²) in the worst case (think k = 10⁶ with all ones), and an interviewer will definitely ask for better.

Remember the brute forceFor each end i: product = 1, walk j leftwards, multiply, count while < k, break at the first failure (numbers ≥ 1 never shrink a product). Don't forget k ≤ 1 → 0.

Part B · Optimal: sliding window + count += right − left + 1

LeetCode 713

1The question again, with the new goal

Same question: count subarrays with product < k. The new goal is one pass, O(n), where both pointers only move forward.

2Choosing the pattern (what the constraints tell us now)

The teacher goes through her four patterns:

Also from the constraints: n up to 3·10⁴ → we want O(n). And numbers ≥ 1 → the product is monotonic (grows when we add, shrinks when we remove), which is exactly what sliding window needs.

Doubt: couldn't we turn the product into a sum with logarithms and use prefix sums?
→ That's a known trick (log(a·b) = log a + log b), but it brings floating-point rounding problems and still needs a binary search. It's not what the teacher teaches, and the sliding window is simpler and exact. (My addition, just so you know it exists.)

3Intuition: expand while it's fine, shrink when it breaks

Both left and right start at index 0. We keep a running product of the window, starting at 1.

4Building the logic from examples

The big question: why count += right − left + 1?

The teacher builds this slowly on a separate little array, [2, 3, 5, 6], pretending every subarray is valid (imagine a huge k) so we only focus on counting. The idea: each time right moves, count only the new subarrays, the ones that end at right. The older ones (ending earlier) were already counted on earlier steps.

r = 02356ending at 2: [2] → 1 new
r = 12356ending at 3: [3], [2,3] → 2 new
r = 22356ending at 5: [5], [3,5], [2,3,5] → 3 new
r = 32356ending at 6: [6], [5,6], [3,5,6], [2,3,5,6] → 4 new
rightleftnew subarrays (all end at right)how manyright − left + 1
00[2]10 − 0 + 1 = 1
10[3], [2,3]21 − 0 + 1 = 2
20[5], [3,5], [2,3,5]32 − 0 + 1 = 3
30[6], [5,6], [3,5,6], [2,3,5,6]43 − 0 + 1 = 4

Total 1 + 2 + 3 + 4 = 10, and indeed an array of 4 elements has exactly 10 subarrays. In every row, "how many new subarrays" equals the window length right − left + 1.

Why the length? Every new subarray must end at right, and it can start at any index from left to right. There are exactly right − left + 1 such start positions, one subarray per start.

Why every one of them is validThe window [left..right] has product < k. Any subarray that ends at right and starts inside the window is a part of the window. Its product is the window's product with some numbers ≥ 1 removed, so it's ≤ the window's product < k. Valid ✓.
Why none outside is validA subarray ending at right that starts before left contains the element(s) we threw away. We only moved left past an index because the window including it had product ≥ k, and adding more numbers ≥ 1 never lowers a product. So those are invalid ✗, and we're right not to count them.
Why nothing is counted twiceEach subarray has exactly one end index. We count it only at the step where right equals that end. So each valid subarray is counted exactly once.
Doubt 1: why count only subarrays that end at right, not all subarrays inside the window?
→ Because the others were counted on earlier steps. For example, at right = 2 the window [2,3,5] also contains [2,3], but [2,3] ends at index 1, and we already counted it when right was 1. Counting by end index is what stops double counting.

Now the real example, with the shrinking

Back to [10, 5, 2, 6], k = 100.

Shrinking: divide, don't subtract

With sums, removing the left element means subtracting it. Here we multiplied it in, so to take it out we divide: product /= nums[left], then left += 1. Removing 10: 100 / 10 = 10, left now points at 5. Window [5, 2], product 10 < 100 → valid again → count += 2 − 1 + 1 = 2 → ([2], [5,2]). Total 5.

Doubt 2: why a while and not an if for shrinking?
→ Removing one element might not be enough. The teacher's point: an if moves left only once, but we must keep moving left until the condition is true again. Example: [2, 2, 2, 50], k = 60: when 50 comes in, the product is 400. Removing one 2 → 200, still ≥ 60; another → 100, still ≥ 60; another → 50 < 60. Three removals. So while product >= k.
Doubt 3: why is the while condition product >= k and not product > k?
→ The question says strictly less. A product equal to k is already invalid (like 100 for k = 100), so we must shrink on "≥".
Doubt 4: in Python, should I write product /= nums[left]?
→ Use //= (integer division). /= turns the product into a float. The division is always exact here (we only divide out a number we multiplied in), so // gives the same value and keeps it a clean integer.
Doubt 5: why does the code still need the k ≤ 1 check? Wouldn't the loop just count 0?
→ With k = 1, even an empty window has product 1 ≥ 1, so the while would keep moving left past right and try to divide by elements that aren't in the window (and eventually go out of range). The base case stops that before the loop starts. With k ≤ 1 there's never a valid window, so returning 0 is correct.
Doubt 6: what if the array could contain a 0?
→ The constraints rule it out (nums[i] ≥ 1), but it's worth knowing the code breaks. A 0 makes the product 0 forever, so the window never shrinks and later counts are wrong, and dividing by 0 would crash. Example [0, 10, 10], k = 50: the right answer is 5 ([0], [0,10], [0,10,10], [10], [10]), but the plain window gives 6. A safe version is in the revision page (my addition, not from the video): every subarray that contains a zero has product 0 < k, and the window restarts just after the zero.

Finishing the example

right = 3: product 10 × 6 = 60 < 100 → no shrink → count += 3 − 1 + 1 = 3 → ([6], [2,6], [5,2,6]). Total 8 ✓.

The teacher's summary of the final count: ending at 10 → 1; ending at 5 → 2 more (3); ending at 2 → 2 more (5); ending at 6 → 3 more (8).

5Approach steps

  1. If k ≤ 1, return 0.
  2. left = 0, product = 1, count = 0.
  3. For right from 0 to n − 1: multiply in nums[right] (expand).
  4. While product ≥ k: divide out nums[left] and move left forward (shrink).
  5. Now the window is valid: count += right − left + 1 (all valid subarrays ending at right).
  6. Return count.

6Code (Python)

Optimal: sliding window, O(n)
class Solution:
    def numSubarrayProductLessThanK(self, nums, k):
        if k <= 1:                          # base case: nothing can be < k
            return 0
        left = 0
        product = 1
        count = 0
        for right in range(len(nums)):
            product *= nums[right]          # expand: take nums[right] in
            while product >= k:             # invalid: shrink from the left
                product //= nums[left]      # take nums[left] out
                left += 1
            count += right - left + 1       # valid subarrays ending at right
        return count

7Code line by line

linewhat it means
if k <= 1: return 0No product can be below 1. This also protects the while loop from running left past right.
left = 0 product = 1 count = 0Window starts empty at index 0. Product of nothing is 1. No subarrays counted yet.
for right in range(len(nums)):The right pointer visits every index once. Each step adds one element to the window.
product *= nums[right]Expansion. Now product = product of nums[left..right].
while product >= k:The window is invalid (equal counts as invalid). Keep shrinking until it isn't.
product //= nums[left] left += 1Shrinking. Undo the multiplication of the leftmost element, then drop it from the window.
count += right - left + 1Every start from left to right gives a valid subarray ending at right. That's the window's length.
return countThe total over all ends.

8Dry run

nums = [10, 5, 2, 6], k = 100.

stepright (value added)window [left..right]product after addingshrink? (what leaves, why)window after shrinking+ (r − l + 1) → count
10 (10)[10] (0..0)10no, 10 < 100[10] (0..0)+1 → 1
21 (5)[10,5] (0..1)50no, 50 < 100[10,5] (0..1)+2 → 3
32 (2)[10,5,2] (0..2)100yes: 100 ≥ 100 → 10 leaves → product 10[5,2] (1..2)+2 → 5
43 (6)[5,2,6] (1..3)60no, 60 < 100[5,2,6] (1..3)+3 → 8

The windows counted at each step (each list = all subarrays ending at right):

The array at the key moments (yellow = window, grey = already left behind):

step 210526product 50 → +2
LR  
step 310526product 100 ≥ 100 → shrink
L R 
after10526divide by 10 → product 10 → +2
 LR 
step 410526product 60 → +3 → total 8
 L R

Final answer: 8 ✓, the same as the brute force. Compare the "+" column with the brute force's "valid ones" column: they're identical (1, 2, 2, 3). The window's left is exactly where the brute force's backwards walk would have stopped, but we find it without walking back.

9Complexity & remember

She submitted it and it beat about 99.8% of solutions, far faster than the O(n²) version.

Remember the optimal k ≤ 1 → 0 · product = 1 · for each right: product *= x → while product ≥ k: product //= nums[left]; left += 1 → count += right − left + 1.
"right − left + 1" = number of valid subarrays that end at right.

Part C · Revision page

Brute forceSliding window
ideafor each end, walk left multiplying, break at first failurekeep the longest valid window ending at right; it has right − left + 1 valid subarrays ending at right
loopstwo nested (j restarts for every i)one for + a while that never restarts
remove an elementnever (start over)divide: product //= nums[left]
base casek ≤ 1 → 0 (products are always ≥ 1)
time / spaceO(n²) / O(1)O(n) (≈2n moves) / O(1)
on LeetCodeaccepted but slow~99.8%
patternwhy it does / doesn't fit
Two pointersneeds sorted data and only two points matter
Prefix sumbuilt for sums, here we multiply
Kadane'smax sum with negatives; we count and have no negatives
Sliding windoweverything between two points matters, and numbers ≥ 1 make the product monotonic
If you remember only 5 lines 1. Products start at 1, not 0. And k ≤ 1 means the answer is 0.
2. Expand with right (multiply), shrink with left (divide) while product ≥ k.
3. After shrinking, every start from left to right gives a valid subarray ending at right.
4. So count += right − left + 1. Each subarray is counted once, at its end index.
5. Both pointers only move forward → O(2n) = O(n).
Mistakes to avoid ✗ starting the product at 0
✗ forgetting if k <= 1: return 0 (the while runs past right)
✗ shrinking with if instead of while
✗ while product > k (must be ≥, the question says strictly less)
✗ counting before shrinking (you'd count an invalid window)
✗ count += 1 instead of count += right − left + 1
✗ using /= in Python (turns the product into a float)
extra (my addition): a version that also works if zeros were allowed
def count_with_zeros(nums, k):
    if k <= 0:
        return 0
    count, left, product, last_zero = 0, 0, 1, -1
    for right, x in enumerate(nums):
        if x == 0:                          # every subarray ending here contains the 0
            last_zero, left, product = right, right + 1, 1
            count += right + 1
            continue
        product *= x
        while product >= k and left <= right:
            product //= nums[left]
            left += 1
        # windows after the zero + subarrays that reach back over the last zero
        count += (right - left + 1) + (last_zero + 1)
    return count
test it yourself (paste under either Solution above)
s = Solution()
print(s.numSubarrayProductLessThanK([10, 5, 2, 6], 100))   # 8
print(s.numSubarrayProductLessThanK([1, 2, 3], 0))         # 0  (base case)
print(s.numSubarrayProductLessThanK([1, 1, 1], 1))         # 0  (1 is not < 1)
print(s.numSubarrayProductLessThanK([1, 1, 1], 2))         # 6  (all subarrays)
print(s.numSubarrayProductLessThanK([2, 2, 2, 50], 60))    # 7  (needs while, not if)
print(s.numSubarrayProductLessThanK([100, 200], 50))       # 0  (no valid window)
print(count_with_zeros([0, 10, 10], 50))                   # 5

Based on this video: Subarray Product Less Than K | Sliding Window