LeetCode #1248 Medium

Count Number of Nice Subarrays

Return the number of subarrays containing exactly k odd numbers.

sliding-windowprefix-sumarray
Open on LeetCode ↗
02

Intuition

The trap is trying to slide a window directly on "exactly k odd numbers." Exactly-k is not monotone: adding an element can jump you from valid to invalid with no shrink that fixes it, so there is no correct rule for when to move left. At-most-k, by contrast, is monotone — shrinking a window can only help — so compute atMost(k) - atMost(k-1). Every subarray with at most k odds, minus every subarray with at most k-1, leaves exactly those with precisely k. Two easy passes replace one hard one.

How to spot this pattern

"Exactly k odd numbers" solved as atMost(k) - atMost(k-1). A window can't test for exactly-k because the condition isn't monotone, but at-most is — the same decomposition as Binary Subarrays With Sum, with parity in place of value.

03

Approach

1

Reduce the array to parity

Only oddness matters, so treat the array as binary: odd is 1, even is 0. The question becomes counting subarrays whose sum is exactly k, which makes the structure obvious.

2

Write atMost(k)

A standard window: grow right, add the parity, and while the running count exceeds k, shrink from the left. After each step, add right - left + 1 to the total — that is the number of subarrays ending at right that are legal, since every start from left to right works.

3

Subtract to get exactly

exactly(k) = atMost(k) - atMost(k-1). Each pass is O(n), so the whole thing is O(n) with O(1) space. Guard atMost for a negative k by returning 0, which matters when k is 0. The same pattern solves Subarrays with K Different Integers, and it is worth learning as a template rather than a one-off.

04

Solution & live demo

1class Solution:
2 def numberOfSubarrays(self, nums, k):
3 def atMost(m):
4 if m < 0:
5 return 0
6 left = odd = total = 0
7 for right, v in enumerate(nums):
8 odd += v & 1
9 while odd > m:
10 left += 1
11 odd -= nums[left - 1] & 1
12 total += right - left + 1
13 return total
14 return atMost(k) - atMost(k - 1)
05

Common pitfalls

Removing the wrong element when shrinking

✗ Wrong
odd -= nums[left] & 1
left += 1
✓ Right
left += 1
odd -= nums[left - 1] & 1

Both orderings can be written correctly, but they must agree — increment then subtract nums[left-1], or subtract nums[left] then increment. Mixing them skips an element and leaves the counter out of step with the window.

Omitting the negative guard

✗ Wrong
def atMost(m):
    left = odd = total = 0
✓ Right
if m < 0:
    return 0

For k = 0 the second call receives −1, and the shrink loop then runs past the right edge producing a negative count. No subarray contains at most −1 odd numbers, so 0 is correct.

Testing oddness with % 2 == 1

✗ Wrong
odd += 1 if v % 2 == 1 else 0
✓ Right
odd += v & 1

Equivalent for positive values here, but % on negatives returns −1 in C++ and Java, so the test silently fails. & 1 is correct for every sign and faster.

06

Edge cases

k = 0

atMost(-1) must return 0; the answer counts subarrays with no odd numbers at all.

k larger than the count of odds

Both terms are equal and the difference is 0.

All numbers even

Only k = 0 yields a non-zero answer.

All numbers odd

The window shrinks constantly and the count is n - k + 1.

07

Complexity

Time
O(n)
Space
O(1)
Two linear passes. The at-most-minus-at-most trick is the reusable idea.