DSA DSA In Depth
0%
design sliding-windowtwo-pointersmonotonicity

Fixed & Variable Sliding Windows: Monotonicity & Two-Pointer Validity

7 min read Module 1.2: Contiguous Arrays, Bit Manipulation & Pointer Idioms Updated Oct 23, 2026
📄 Cheat Sheet
Sliding Window Technique — Algorithmic Mental Models & Monotonicity
Ryan Schachte Sliding Window Technique — Algorithmic Mental Models & Monotonicity Watch on YouTube ↗
0:00 / --:--

When Is a Sliding Window Mathematically Valid?

Given an array $A[0 \dots N-1]$ and a validity predicate $P(L, R) \in \{\text{true}, \text{false}\}$ defined on contiguous subarrays $A[L \dots R]$, a variable sliding window advances $R$ from $0$ to $N-1$ and increments $L$ while maintaining validity. For this algorithm to be correct without ever backtracking $L$, the predicate $P$ must be hereditary (monotonic under inclusion):

$$[L', R'] \subseteq [L, R] \text{ and } P(L, R) = \text{true} \implies P(L', R') = \text{true}$$

Equivalently, by contraposition: if $[L, R]$ violates the constraint ($P(L, R) = \text{false}$), then any super-interval $[L, R']$ with $R' > R$ also violates the constraint, and any sub-interval $[L', R]$ with $L' > L$ moves closer to satisfying it. Define $L^*(R)$ as the smallest index $L$ (for longest valid window) or largest index $L$ (for shortest valid window) paired with right endpoint $R$. Monotonicity guarantees:

$$R_1 < R_2 \implies L^*(R_1) \le L^*(R_2)$$

Because both $L$ and $R$ start at $0$, only increment, and never exceed $N$, the inner while loop executes at most $N$ times across the entire run of the outer loop, proving an exact bound of at most $2N$ pointer steps ($O(1)$ amortized work per element).

Architectural Invariant: The "Exactly $K$" Reduction via $\text{atMost}(K) - \text{atMost}(K - 1)$

Suppose we want to count the number of subarrays with exactly $K$ distinct elements (or sum $= K$ over binary/non-negative arrays). A naive single sliding window fails because for a fixed $R$, there is not a single boundary $L$, but a whole contiguous range $[L_{\text{min}}, L_{\text{max}}]$ of valid left endpoints where the window has exactly $K$ distinct elements.

However, the predicate $P_K(L, R) \equiv (\text{distinct}(A[L \dots R]) \le K)$ is strictly monotonic! For each $R$, once we shrink $L$ until $\text{distinct}(A[L \dots R]) \le K$, every subarray starting at $i \in [L, R]$ and ending at $R$ is a subset of $[L, R]$ and therefore also has $\le K$ distinct elements—contributing exactly $(R - L + 1)$ valid subarrays ending at $R$. By set difference:

$$\text{countExactly}(K) = \text{countAtMost}(K) - \text{countAtMost}(K - 1)$$

Sliding Window Archetypes & Validity Conditions

Window Archetype Monotonicity Prerequisite Inner Loop Condition State Update at Step $R$ Failure Case (When It Breaks)
Fixed Window of Size $K$ Invertible or rolling aggregator if (R >= K) remove(A[R - K]) ans = best(ans, state) Non-invertible state without monotonic deque
Longest Valid Window Expanding $R$ can only break validity; shrinking $L$ restores it while (!valid(L, R)) remove(A[L++]) ans = max(ans, R - L + 1) after loop Negative numbers in sum $\le K$ (shrinking $L$ can increase sum!)
Shortest Valid Window Expanding $R$ achieves validity; shrinking $L$ tests minimality while (valid(L, R)) { ans = min(..); remove(A[L++]); } Recorded inside inner while loop Negative numbers in sum $\ge K$
Count Exactly $K$ Metric $f(L, R)$ is non-decreasing with window length Two calls: atMost(K) - atMost(K - 1) total += (R - L + 1) per $R$ Non-monotonic metric (e.g. arbitrary signed sums)

Visualizing Monotonic Frontier $L^*(R)$ & The $\text{atMost}(K)$ Counting Wedge

Once [L .. R] has <= K distinct elements, all (R - L + 1) suffixes ending at R are also valid! idx 0 idx 1 idx 2 (L) idx 3 idx 4 idx 5 idx 6 (R) idx 7 <-- R - L + 1 = 5 valid subarrays ending at R=6

Amortized Execution Flow of Variable Sliding Window

flowchart LR A["Advance R: 0 .. N-1
Insert A[R] into window state"] --> B{"Is window [L..R]
valid?"} B -- "No (Invalid)" --> C["Remove A[L] from state
L++ (Monotonic advance)"] C --> B B -- "Yes (Valid)" --> D["Update optimal answer
or add (R - L + 1) to count"]
Hardware Reality: Flat Frequency Arrays vs. std::unordered_map in Sliding Windows

Inside a sliding window loop, every step increments freq[A[R]] and decrements freq[A[L]]. Using std::unordered_map<char, int> for ASCII/UTF-8 byte streams or bounded integer alphabets performs a hash computation, bucket pointer dereference, and linked-list node chase on every single character—turning a 1-cycle operation into a 40-cycle L1/L2 cache stall. Allocating a stack-resident flat array int32_t freq[256] = {0} occupies exactly 1 KB (16 cache lines), remains permanently pinned in L1d cache, and executes window expansions/contractions in ~1.2 CPU clock cycles per byte.

Two-Pointers & Variable Sliding Window Invariant Stepper
O(N) Amortized 2N Steps

Trace variable-length sliding window [L..R] and Dutch National Flag 3-way partitioning. Both L and R advance monotonically, guaranteeing at most 2N pointer movements.

Step 1 / 15
Custom Input:
Timeline

Invariant & State

Window [0..0] sum = 2 (Target ≥ 8) | Monotonic L=0, R=0 never move left
📊 Live Magnitude & Pointer Stage — Two-Pointers & Variable Sliding Window Invariant Stepper Active Pivot Match/Sorted
L,R 2 [0] 3 [1] 1 [2] 2 [3] 4 [4] 3 [5] 7 [6] 2 [7]
[0]2L,R
[1]3
[2]1
[3]2
[4]4
[5]3
[6]7
[7]2
Window [L..R]
[0..0] (len 1)
Window Sum
2
Best Valid Len
∞
Pointer Ops (≤ 2N)
1
[Step 1] Expand R -> 0 (added A[0]=2), window sum = 2

Production Implementation: Count Subarrays with Exactly K Distinct Integers

#include <cstdint>
#include <vector>

// Prerequisite: 0 <= nums[i] <= max_val (or coordinate-compressed to [0, N))
std::int64_t subarrays_at_most_k_distinct(const std::vector<int>& nums, int k, int max_val) {
  if (k <= 0) return 0;
  std::vector<int> freq(static_cast<std::size_t>(max_val + 1), 0);
  int distinct = 0;
  std::size_t left = 0;
  std::int64_t total_subarrays = 0;

  for (std::size_t right = 0; right < nums.size(); ++right) {
    if (freq[nums[right]]++ == 0) {
      ++distinct;
    }
    while (distinct > k) {
      if (--freq[nums[left]] == 0) {
        --distinct;
      }
      ++left;
    }
    // Every subarray [i .. right] with left <= i <= right has <= k distinct elements
    total_subarrays += static_cast<std::int64_t>(right - left + 1);
  }
  return total_subarrays;
}

std::int64_t subarrays_exactly_k_distinct(const std::vector<int>& nums, int k, int max_val) {
  return subarrays_at_most_k_distinct(nums, k, max_val)
       - subarrays_at_most_k_distinct(nums, k - 1, max_val);
}
Production Pitfall: Applying Two-Pointer Sliding Windows to Arrays with Negative Numbers

Never use a standard two-pointer sliding window to find the shortest or longest subarray with sum $\ge K$ or $\le K$ when the input array can contain negative numbers! With negative values, shrinking the window by advancing $L$ past a negative number $A[L] < 0$ increases the window sum, and advancing $R$ across a negative number decreases the window sum. Monotonicity is destroyed, and $L^*(R)$ is no longer non-decreasing. When negative numbers are present:

  • For Subarray Sum Equals $K$: use Prefix Sums + Hash Map ($O(N)$ time, $O(N)$ space).
  • For Shortest Subarray with Sum $\ge K$ (LeetCode 862): use Prefix Sums + a Monotonic Increasing Deque ($O(N)$ time, $O(N)$ space).

Check yourself Question 1 of 4
Concept Check Amortized O(N) Complexity Proof of Variable Sliding Window recall

A variable-size sliding window algorithm consists of an outer `for (right = 0; right < N; ++right)` loop and an inner `while (!valid(left, right)) { ++left; }` loop.

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why is `int32_t freq[256]` ~25x faster per byte than `std::unordered_map` in this loop?

Key Takeaways

A two-pointer sliding window is valid if and only if the window predicate P(L, R) is monotonic under interval inclusion, guaranteeing L*(R) is non-decreasing.
Because L and R each start at 0 and increment at most N times without ever backtracking, the inner while loop runs at most N times total across the entire algorithm (O(N) amortized).
Problems asking for 'exactly K' of a monotonic metric (like distinct elements or odd numbers) reduce to countAtMost(K) - countAtMost(K - 1).
Negative numbers destroy sum monotonicity; replace plain sliding windows with Prefix Sums + Hash Map (for exact sum K) or Prefix Sums + Monotonic Deque (for shortest sum >= K).
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue