Jc-alt logo
jc
LeetCode: Two Pointers II Sliding Window

LeetCode: Two Pointers II Sliding Window

··
110 min read
·data structures and algorithms

Sliding Window Intro

Leetcode problems with elegant solutions using the sliding window technique.

What Is It

Sliding Window is a technique used for iterating over a subset of data within a larger structure (like arrays or strings) by maintaining a moving window that can expand, contract, or shift across the input.

There are two main types of sliding windows:

  1. Fixed size (constant length) window
  2. Variable size (dynamic length) window

Sliding window often replaces nested loops by reusing computation from the previous window to achieve better efficiency.

Why Use Sliding Window

Sliding Window is ideal for problems that involve:

  • Finding optimal subarray/substring
  • Summation or counting within a range
  • Tracking a condition inside a window

It typically reduces time complexity from O(n2) (naive nested loops) to O(n) since each element is processed at most twice (entering/exiting the window).

Syntax Quick Tools

  • Count of Subarrays

At least k: valid window + extend outward -> still valid so count the extensions: left or n - right

left = "how many starts can I slide back (toward index 0) and still be valid?" n - right = "how many ends can I slide forward (toward index n-1) and still be valid?"

At most k: valid window + shrink inward -> still valid so count the sub-windows: right - left + 1

right - left + 1 = "how many starts, from left up to right, still give a valid subarray ending at right?"

Sliding Window Section: Fixed K Window With Requirements

A fixed window keeps exactly k elements. Each step adds the new right element and removes the one that fell off the left, so the window updates in O(1) instead of being recomputed.

Key parts:

  • Window size never changes, so left is always right - k + 1
  • Add the incoming element, remove the outgoing one
  • Some problems also check a requirement on each window (counts match, no duplicates)

Sliding Window Section: Max Window

Find the longest window that satisfies a condition. Always expand right, and only shrink when the window becomes invalid.

Key parts:

  • Expand right every step
  • Shrink from left while invalid, until the window is valid again
  • Compare the length after the shrink loop, since the window is valid again

Sliding Window Section: Min Window

Find the shortest window that satisfies a condition. Expand until the window is valid, then tighten it as far as it stays valid.

Key parts:

  • Expand right until the window becomes valid
  • Shrink from left while valid
  • Compare the length inside the shrink loop, since the window is valid there

Sliding Window Section: Within Or Outside Window

The condition depends on what is outside the window, not what is inside it. Track the counts of everything outside the window.

Key parts:

  • Start with the counts of the whole array as "outside"
  • Moving right into the window removes from outside, moving left out of the window adds back
  • Shrink while the outside is valid, since the window is the part being replaced

Sliding Window Section: Count At Least K

Count subarrays with an "at least" condition (at least k pairs, contains all 3 chars). A valid window stays valid when extended outward, so a whole batch can be counted at once.

Key parts:

  • Verify the property is monotonic: valid window + extend = still valid
  • Shrink from left while valid
  • += left after the loop: starts 0 ... left-1 are valid for this right
  • += n - right inside the loop: ends right ... n-1 are valid for this left

Sliding Window Section: Count At Most K

Count subarrays with an "at most" condition (product less than k, at most k distinct). A valid window stays valid when shrunk inward, so every start from left to right works.

Key parts:

  • Verify the property is monotonic under shrinking
  • Shrink from left while invalid
  • += right - left + 1 after the loop: starts left ... right are valid for this right

Sliding Window Section: Count Exactly K

"Exactly k" is not monotonic in either direction: extending can break it, and so can shrinking. Split it into two "at most" problems and subtract.

Key parts:

  • exactly(k) = atMost(k) - atMost(k-1)
  • Each atMost is a Count At Most K window using right - left + 1
  • Guard atMost(-1) to return 0 when k = 0

Sliding Window Section: Deque For Max Or Min

When the condition needs the max or min of the window, a monotonic deque gives it in O(1). The deque stores indices in decreasing (max) or increasing (min) value order.

Key parts:

  • Pop from the back while the new element beats them, since they can never be the answer again
  • Pop from the front when its index falls out of the window
  • The front of the deque is always the window max (or min)

121. Best Time to Buy and Sell Stock ::3:: - Easy

Topics: Array, Dynamic Programming, Sliding Window

Intro

You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock. Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

Example InputOutput
prices = [7,1,5,3,6,4]5
prices = [7,6,4,3,1]0

Constraints:

1 ≤ prices.length ≤ 105

0 ≤ prices[i] ≤ 104

Abstraction

Given an array of numbers, find the greatest difference, where the smaller number comes before the larger number in the array.

Pseudocode

Sol 1: N Size Window (Left Right Curr Buy Today Sell Prices)
1. (left, right = 0, 0)
1. (maxProfit = 0)
2. while right < n:
   a. currBuyPrice = prices[left] 
   b. todaySellPrice = prices[right]
   c. if currBuyPrice < todaySellPrice:
         maxProfit = max(maxProfit, (todaySellPrice - currBuyPrice))
   d. else:
         left = right
   e. right += 1
3. return maxProfit

Sol 3: Best Buy Sell Price Up To Today State Compression Variables
1. (currBuyPrice = -prices[0])
2. (maxProfit = 0)
2. For i in range(1, n):
   a. currBuyPrice = max(currBuyPrice, -prices[i])
   b. maxProfit = max(maxProfit, currBuyPrice + prices[i])
3. return maxProfit

Solution 1: [Sliding Window] N Size Window Tracking Best Buy And Today Sell With Left Right - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Sliding Window (Variable Size)

        # Stock Representation:
        #   - Window will grow from 0 to n as we iterate
        #   - Left and right represent: 
        #        left: best buy day so far (lowest price seen)
        #        right: current sell day candidate (highest prices seen)
        #   - Treat each day as a potential sell day

        # Idea:
        #   - Always buy before you sell
        #   - If current price is higher than buy price, compute profit
        #   - If current price is lower than buy price, we have found better buy price, reset window

        n = len(prices)

        # Sliding Window Representation:
        # There is no window representation,
        # the window simply grows to include the entire array
        # with our left and right tracking the best buy and sell prices within the window

        # Max profit seen so far given our best buy and best sell
        maxProfit = 0

        # Left: lowest price so far
        # Right: todays price
        left = 0
        right = 0

        # tc: O(n)
        while right < n:

            # Grab current best prices
            currBuyPrice = prices[left]
            todaySellPrice = prices[right]

            # Check: 
            # can we make a profit
            if currBuyPrice < todaySellPrice:
                
                # Calculate profit and compare
                profit = todaySellPrice - currBuyPrice
                maxProfit = max(maxProfit, profit)

            # Check: 
            # is this a better buy
            else:
                # update best buy day
                left = right

            # Iterate to next price
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxProfit

Solution 2: [Dynamic Programming] Track Best Buy And Sell Up To ith Day Complete Table - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Dynamic Programming (Full DP Table)

        # Window Representation:
        #   - Maintain window within [i]
        #   - currBuyPrice[i][0]: best buy day so far:
        #       we represent this by min(-currBuyPrice) for easier later calculations
        #
        #   - currBuyPrice[i][1]: best profit price up to day i    

        # Idea:
        #   - Always buy before you sell (<= index 1)
        #   - Check if new buy day found
        #   - Check if new best profit found


        # Empty check: no profit to be made
        if not prices:
            return 0

        n = len(prices)

        # Initialize DP:
        # sc: O(n)
        dp = []
        
        # Fill dp table:
        #   - dp[i][0]: best buy price up to day i
        #   - dp[i][1]: best profit price up to day i
        # tc: O(n)
        for i in range(n):
            dp.append([0, 0])

        
        # Initialize DP:
        # i = day
        # dp[i][0]: set best buy price to day 0
        # dp[i][1]: set best profit made up to day 0 
        #           (cannot buy and sell on same day, so profit on day 0 is 0)
        # tc: O(1)
        dp[0][0] = -prices[0]
        dp[0][1] = 0

        # start from second day
        # tc: iterate across n days O(n)
        for i in range(1, n):

            # Check: grab todays buy price
            # Use profit to explain our buy price:
            #     profit = hold + todaySellPrice
            #            = (-3) + 10
            #            = 7       
            todayPrice = -prices[i]

            # Check: grab best buy price up to yesterday
            bestBuyUpToYesterday = dp[i-1][0]

            # Check: grab new buy price
            dp[i][0] = max(bestBuyUpToYesterday, todayPrice)     

            # Check: todays profit
            todayProfit = bestBuyUpToYesterday + prices[i]
            
            # Check: grab new best profit, cannot sell today, so use yesterdays price    
            dp[i][1] = max(dp[i-1][1], todayProfit)


        # overall: tc O(n)
        # overall: sc O(n)
        return dp[-1][1]

Solution 3: [Dynamic Programming] Track Best Buy And Sell Up To Today State Compression Variables - Sliding Window/Variable Size Window

    def maxProfit(self, prices: List[int]) -> int:

        # Dynamic Programming (State Compression)

        # Stock Representation:
        #   - Treat each day as potential sell day
        #   - Maintain a state tracking where:
        #        currBuyPrice: best buy price so far (lowest price seen)
        #        profit: most profit made so far (highest profit made using each day as a sell day)

        # Idea:
        #   - Always buy before you sell
        #   - If current price is higher than buy price, compute profit
        #   - If current price is lower than buy price, we have found better buy price, reset window

        n = len(prices)

        # set up compressed dynamic programming table first iteration:        
        # [i][0] -> min buying price up to day i
        # [i][1] -> max profit up to day i

        # Initialize Compressed DP:
        #   - min price up to day 0
        #   - max profit up to day 0
        currBuyPrice = -prices[0]
        maxProfit = 0

        # start from second day
        # tc: iterate over n days O(n)
        for i in range(1, n):
            
            # Put buy price as negative,
            # to ensure we always subtract it from out buy price
            #     profit = hold + todaySellPrice
            #            = (-3) + 10
            #            = 7       
            todayPrice = -prices[i]

            # Check: compare best buy price up to yesterday to today buy price
            currBuyPrice = max(currBuyPrice, todayPrice)

            todayProfit = currBuyPrice + prices[i]

            # profit = hold + todaySellPrice
            #        = (-3) + 10
            #        = 7
            maxProfit = max(maxProfit, todayProfit)  

        # overall: tc O(n)
        # overall: sc O(1)
        return maxProfit

219. Contains Duplicate II ::2:: - Easy

Topics: Array, Hash Table, Sliding Window

Intro

Given an integer array nums and an integer k, return true if there are two distinct indices i and j in the array such that nums[i] == nums[j] and abs(i - j) lte k.

Example InputOutput
nums = [1,2,3,1], k = 3true
nums = [1,0,1,1], k = 1true
nums = [1,2,3,1,2,3], k = 2false

Constraints:

1 ≤ nums.length ≤ 10^5

-10^9 ≤ nums[i] ≤ 10^9

0 ≤ k ≤ 10^9

Abstraction

Given a list of numbers, and sliding window size k, determine if there is a duplicate anywhere in the array.

Pseudocode

Sol 1: MRI HashMap Checking Duplicates In K Window Based On Left Right Pointers
1. if n <= 1 or k <= 0: 
    a. return false
2. (mri = {})
3. For right in range(n):
    a. if nums[right] in mri:
         left = right - k
         prevIndex = mri[nums[right]]
         if left <= prevIndex <= right: 
             return true
   b. mri[nums[right]] = right
4. return false

Sol 2: Window HashSet K Tracking Nums Within Window
1. if n <= 1 or k <= 0: 
    return false
2. (window = {}) 
3. (left = 0)
3. while right < n:
    a. if nums[right] in window: 
         return true
    b. window.add(nums[right])
    c. if k < len(window):
         window.remove(nums[left])
         left += 1
    d. right += 1
4. return false

Solution 1: [Sliding Window] MRI HashMap Checking Duplicates In K Window Based On Left Right Pointers - Sliding Window/Variable Size Window

    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        
        # Sliding Window + HashMap (Fixed Size Window):

        # Window Representation:
        #   - HashMap for each number representing (val -> most recent index)

        # Idea:
        #   - Slide window over num list and add index at right boundary
        #   - Check mri if we have a duplicate of the current element,
        #     and if a duplicate exists within the k window [left, right]
    
        # Track more recent index appearance for num
        # sc: O(n)
        mri = {}

        # Sliding Window Variables
        n = len(nums)
        right = 0

        # Empty Case:
        # if not enough days to have a duplicate
        if n <= 1 or k <= 0:
            return False

        # tc: O(n)
        for right in range(n):

            currNum = nums[right]

            # Check: if num has already been seen
            # tc: O(1)
            if currNum in mri:

                #     k distance
                #  | ------------ |
                # left          right 

                # Index Distance No More Than k Apart (not element count):
                # the furthest left index that is still within k distance
                left = right - k

                # Check: 
                # if previous num copy is within current window, duplicate exists
                if left <= mri[currNum] <= right:
                    return True

            # Implies: no duplicate exists
            # Then: update mri for num
            mri[currNum] = right

        # No duplicates found within distance k

        # overall: tc O(n)
        # overall: sc O(min(n, k))
        return False

Solution 2: [Sliding Window] Window HashSet K With Left Right Boundaries And Set Length Early Exit - Sliding Window/Variable Size Window

    def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
        
        # Sliding Window HashSet Right Left Representation (Fixed Size Window):

        # Window Representation:
        #   - Set() of width k spanning [left, right]
        #   - Numbers enter and exit window via left and right

        # Idea:
        #   - Slide fixed size window over list
        #   - Add element at right, remove element from left
        #   - Check if num we added is already within window boundary

        n = len(nums)

        # Empty Case:
        # Single list or window of length k cannot have duplicates
        if n <= 1 or k <= 0:
            return False

        # Sliding Window Data:
        # sc: O(k)
        window = set()

        # Sliding Window Boundaries:
        left = 0
        right = 0

        # Early exit: 
        # all elements are unique, no duplicates
        if len(set(nums)) == n:
            return False

        # tc: O(n)
        while right < n:

            # Check: 
            # if duplicate num exist in window
            if nums[right] in window:
                return True

            # Window Extend:
            # right index enters window:
            window.add(nums[right])
            right += 1

            # Window Shrink:
            # if we've fixed width of window,
            # left index exits window:
            if k < len(window):
                window.remove(nums[left])
                left += 1



        # No duplicates found within k window

        # overall: tc O(n)
        # overall: sc O(min(n, k))
        return False

567. Permutation in String ::2:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise. In other words, return true if one of s1's permutations is the substring of s2.

Example InputOutput
s1 = "ab", s2 = "eidbaooo"true
s1 = "ab", s2 = "eidboaoo"false

Constraints:

1 ≤ s1.length, s2.length ≤ 104

s1 and s2 consist of lowercase English letters.

Abstraction

Given two arrays, and a fixed size window of the first array, check if the second array contains a permutation of the first array.

(fixed size window, shift window by 1 char)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Needle In Haystack Window Count HashMap For Char And Removing From Left While Window Size Flag Is False - Sliding Window/Fixed Size Window

    def checkInclusion(s1: str, s2: str) -> bool:

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Maintain window [left, right]
        #   - Window size always equals len(s1)
        #   - Hashmap: char -> frequency

        # Idea:
        #   - Build a frequency map for s1
        #   - Expand right pointer to build window in s2
        #   - If window size exceeds len(s1), shrink from left
        #   - If window frequency == s1 frequency, permutation exists

        # Since window size is fixed, left moves whenever
        # window grows larger than lens1)

        n1 = len(s1)
        n2 = len(s2)

        # s2 < s1: permutation not possible
        # tc: O(1)
        if n2 < n1:
            return False

        # s1 Frequency
        # tc: O(n)
        # sc: O(n)
        needleCharCount = defaultdict(int)
        for c in s1:
            needleCharCount[c] += 1

        # Substring window frequency
        # sc: O(1)
        windowCharCount = defaultdict(int)
        
        # left and right boundary of current window
        left = 0
        right = 0

        # tc: O(n)
        for right in range(n2):

            # Add char count to window
            rightChar = s2[right]
            windowCharCount[rightChar] += 1

            # Window length
            currWindowLen = right - left + 1

            # Shrink from left while window is invalid:
            #   - window length is greater than needle length
            #   - since we extend window by 1 char, we only need to decrease by 1 char
            if n1 < currWindowLen:

                # Decrease char count from window
                leftChar = s2[left]
                windowCharCount[leftChar] -= 1

                # Remove char if frequency reached zero,
                # to match needle hashmap
                if windowCharCount[leftChar] == 0:
                    del windowCharCount[leftChar]
                
                # Shrink window
                left += 1
            
            # If window hashmap matches the needle hashmap,
            # valid permutation found
            if windowCharCount == needleCharCount:
                return True

        # overall: tc O(n) 
        # overall: sc O(n)
        return False

Solution 2: [Sliding Window] Needle In Haystack Window Count Array With 97 Lowercase Ascii Shifting [SC Opt] - Sliding Window/Fixed Size Window

    def checkInclusion(self, s1: str, s2: str) -> bool:

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Maintain window [left, right]
        #   - Window size always equals len(s1)
        #   - Frequency array: char -> frequency (size 26 for lowercase letters)

        # Idea:
        #   - Build a frequency array for s1
        #   - Expand right pointer to build window in s2
        #   - If window size exceeds len(s1), shrink from left
        #   - If window frequency == s1 frequency, permutation exists

        # Since window size is fixed, left moves whenever
        # window grows larger than len(s1)

        n1 = len(s1)
        n2 = len(s2)

        # If needle length is greater than haystack length:
        if n2 < n1:
            return False

        # s1 Frequency
        # tc: O(n)
        # sc: O(1)
        needleCharCount = [0] * 26
        for c in s1:
            needleCharCount[ord(c) - 97] += 1

        # Substring window frequency
        # sc: O(1)
        windowCharCount = [0] * 26

        # left and right boundary of current window
        left = 0
        right = 0

        # tc: O(n)
        for right in range(n2):

            # Add char count to window
            rightChar = s2[right]
            windowCharCount[ord(rightChar) - 97] += 1

            # Window length
            currWindowLen = right - left + 1

            # Shrink from left while window is invalid:
            #   - window length is greater than needle length
            #   - since we extend window by 1 char, we only need to decrease by 1 char
            if n1 < currWindowLen:

                # Decrease char count from window
                leftChar = s2[left]
                windowCharCount[ord(leftChar) - 97] -= 1

                # No need to "remove" the 0 count chars
                # since all empty chars are already set to 0

                # Shrink window
                left += 1

            # If window frequency array matches the needle frequency array,
            # valid permutation found
            if windowCharCount == needleCharCount:
                return True

        # overall: tc O(n)
        # overall: sc O(1)
        return False

658. Find K Closest Elements ::2:: - Medium

Topics: Array, Binary Search, Sliding Window, Prefix Sum

Intro

Given a sorted integer array arr, two integers k and x, return the k closest integers to x in the array. The result should also be sorted in ascending order. An integer a is closer to x than an integer b if:

  • |a - x| lt |b - x|, or
  • |a - x| == |b - x| and a lt b
Example InputOutput
arr = [1,2,3,4,5], k = 4, x = 3[1,2,3,4]
arr = [1,1,2,3,4,5], k = 4, x = -1[1,1,2,3]

Constraints:

1 ≤ k ≤ arr.length

1 ≤ arr.length ≤ 10^4

arr is sorted in ascending order

-10^4 ≤ arr[i], c ≤ 10^4

Abstraction

Given a sorted array, return the k elements closest to x (ties go to the smaller value).

(sorted array: the k closest elements must form one contiguous window of length k, so the problem becomes "which window?")

(Shrink: start with the whole array as the window, and repeatedly drop whichever end is farther from x until k elements remain)

(Monotonic: the farther end can never be in the answer, since everything between it and x is closer, so it can be dropped for good without revisiting it)

(Binary Search: the window size is fixed at k, so only the left boundary is unknown, compare the window's two outer edges to see which side to move toward)

(Count what the window holds) right - left + 1 = "how many elements are still in the window? shrink until this equals k"

(Compare the edges) x - arr[mid] vs arr[mid + k] - x = "which edge of the window starting at mid is farther from x?"

(Read the answer from left) arr[left:left + k] = "once only one candidate start remains, what k elements does it hold?"

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Shrink The Farther End Until K Elements Remain - Sliding Window/Variable Size Window

    def findClosestElements(self, arr: list[int], k: int, x: int) -> list[int]:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - Window starts as the whole array and only shrinks
        #   - Window is done when it holds exactly k elements

        # Idea:
        #   - arr is sorted, so the k closest elements form a contiguous window
        #   - Compare the distance from x of the left num and the right num
        #   - The farther one can never be in the answer, since everything between it
        #     and x is closer, so drop it
        #   - On a tie, drop the right num, since the problem prefers the smaller value
        #   - Repeat until k elements remain

        # Index of last element
        lastIndex = len(arr) - 1

        # Sliding Window Boundaries
        left = 0
        right = lastIndex

        # Elements currently in window
        windowLength = right - left + 1

        # Shrink window from n down to k elements
        # tc: O(n - k)
        while windowLength > k:

            # Distance from x of each edge
            leftDistance = abs(arr[left] - x)
            rightDistance = abs(arr[right] - x)

            # Drop the farther num, tie drops right (keeps the smaller value):
            # arr = [1, 2, 3, 4, 5], k = 4
            #   x = 3 -> distances 2 and 2, tie -> drop right -> window [1, 2, 3, 4]
            #   x = 6 -> distances 5 and 1, left is farther -> drop left -> window [2, 3, 4, 5]
            # tc: O(1)
            if leftDistance <= rightDistance:
                right -= 1
            else:
                left += 1

            # Update window length
            windowLength = right - left + 1

        # Window of the k closest elements
        # tc: O(k)
        closest = arr[left:right + 1]

        # overall: tc O(n)
        # overall: sc O(k) for the returned list, O(1) extra
        return closest

Solution 2: [Binary Search] Binary Search The Left Boundary Of A Fixed Size K Window - Sliding Window/Variable Size Window

    def findClosestElements(self, arr: list[int], k: int, x: int) -> list[int]:

        # Binary Search (Left Boundary Of Fixed Size Window)

        # Search Representation:
        #   - [left, right] is the range of possible start indexes for the window
        #   - A window starting at left covers [left, left + k - 1]
        #   - Starts run from 0 to n - k, since the window must fit inside arr
        #   - Search is done when left == right, one candidate start remains

        # Idea:
        #   - arr is sorted, so the k closest elements form a contiguous window of length k
        #   - Only the start index is unknown, so binary search over starts
        #   - For the window starting at mid, compare its left edge arr[mid]
        #     with the element just past its right edge, arr[mid + k]
        #   - If arr[mid + k] is closer to x, the window should slide right, so left = mid + 1
        #   - Otherwise arr[mid] is closer or tied, so the window should stay or slide left,
        #     keep mid as a candidate with right = mid
        #   - Ties keep the left side, since the problem prefers the smaller value

        # Number of elements
        n = len(arr)

        # Binary Search Boundaries (possible window starts)
        left = 0
        right = n - k

        # tc: O(log(n - k))
        while left < right:

            # Middle start
            mid = (left + right) // 2

            # Distance from x of the window's left edge and the element past its right edge
            leftEdgeDistance = x - arr[mid]
            rightEdgeDistance = arr[mid + k] - x

            # Element past the right edge is closer, slide window right:
            # arr = [1, 2, 3, 4, 5], k = 4, x = 6, left = 0, right = 1, mid = 0
            # leftEdgeDistance = 6 - 1 = 5, rightEdgeDistance = 5 - 6 = -1
            # -1 < 5 -> left = 1
            # tc: O(1)
            if rightEdgeDistance < leftEdgeDistance:
                left = mid + 1

            # Left edge is closer or tied, keep mid as a candidate:
            # arr = [1, 2, 3, 4, 5], k = 4, x = 3, left = 0, right = 1, mid = 0
            # leftEdgeDistance = 3 - 1 = 2, rightEdgeDistance = 5 - 3 = 2
            # 2 is not < 2 -> right = 0
            # tc: O(1)
            else:
                right = mid

        # left == right, one candidate start remains
        # Window of the k closest elements
        # tc: O(k)
        closest = arr[left:left + k]

        # overall: tc O(log(n - k) + k)
        # overall: sc O(k) for the returned list, O(1) extra
        return closest

904. Fruit Into Baskets ::2:: - Medium

Topics: Array, Hash Table, Sliding Window

Intro

You are visiting a farm that has a single row of fruit trees arranged from left to right. The trees are represented by an integer array fruits where fruits[i] is the type of fruit the ith tree produces. You want to collect as much fruit as possible. However, the owner has some strict rules that you must follow: You only have two baskets, and each basket can only hold a single type of fruit. There is no limit on the amount of fruit each basket can hold. Starting from any tree of your choice, you must pick exactly one fruit from every tree (including the start tree) while moving to the right. The picked fruits must fit in one of your baskets. Once you reach a tree with fruit that cannot fit in your baskets, you must stop. Given the integer array fruits, return the maximum number of fruits you can pick.

Example InputOutput
fruits = [1,2,1]3
fruits = [0,1,2,2]3
fruits = [1,2,3,2,2]4

Constraints:

1 ≤ fruits.length ≤ 10^5

0 ≤ fruits[i] < fruits.length

Abstraction

Given an array, and a variable size window, find the longest window that contains at most 2 distinct elements.

(expand while valid, shrink while invalid)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Fruit Count HashMap With Shrink From Left While More Than 2 Fruit In Window - Sliding Window/Variable Size Window

    def totalFruit(self, fruits: List[int]) -> int:

        # Sliding Window + Hashmap
        
        # Window Representation:
        #   - HashMap tracks count of fruit within window
        #   - Valid window contains a max of 2 unique fruit

        # Rules:
        #   - Expand variable size window over list by extending right
        #   - Add new fruit to count
        #   - If HashMap has more than 2 unique fruit, shrink window from left until unique fruit is valid again
        #   - For each valid window compare size to max window
        
        # Fruit Count:
        # Holds at most 2 unique fruits at any moment
        # sc: O(1)
        count = defaultdict(int)

        # Window Representation:
        left = 0
        right = 0

        n = len(fruits)

        # Longest string up to now
        maxLen = 0

        # tc: O(n)
        while right < n:

            # Add right num
            rightNum = fruits[right]
            count[rightNum] += 1

            # Shrink from left while window is invalid:
            #   - more than 2 unique fruits in window
            while len(count) > 2:
                
                # Decrease left fruit count
                leftNum = fruits[left]
                count[leftNum] -= 1

                # Remove fruit type if no more fruit remain:
                if count[fruits[left]] == 0:
                    del count[fruits[left]]

                # Shrink window
                left += 1

            # Valid window, compare to max length
            currWindow = right - left + 1
            maxLen = max(maxLen, currWindow)

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxLen

Solution 2: [Sliding Window] MRI HashMap Jump Left When Invalid - Sliding Window/Variable Size Window

    def totalFruit(self, fruits: List[int]) -> int:

        # Sliding Window + Most Recent Index Map

        # Idea:
        #   - Maintain a window [l, r]
        #   - Track last seen index of each fruit type
        #   - If we ever have > 2 fruit types:
        #       remove the fruit with the smallest last seen index
        #       and move left pointer just past it

        mri = {}

        n = len(fruits)-1

        left = 0
        right = 0

        maxLen = 0

        # tc: O(n)
        while right <= n:

            # Update most recent index of current fruit
            mri[fruits[right]] = right

            # Jump from left if window is invalid:
            # - more than 2 unique fruit types, currently 3 types
            if len(mri) > 2:

                # Remove the fruit farthest to the left
                # - jump left to 1 index after farthest fruit
                farthestLeftFruit = min(mri, key=mri.get)
                left = mri[farthestLeftFruit] + 1

                # Remove fruit from map
                del mri[farthestLeftFruit]

            # Valid window, compare to max
            currLen = right - left + 1
            maxLen = max(maxLen, currLen)

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return maxLen

3. Longest Substring Without Repeating Characters ::2:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

Given a string s, find the length of the longest without duplicate characters.

Example InputOutput
s = "abcabcbb"3
s = "bbbbb"1
s = "pwwkew"3

Constraints:

0 ≤ s.length ≤ 5 * 104

s consists of English letters, digits, symbols and spaces.

Abstraction

Given an array, and a variable size window, find longest window without duplicates. (expand while valid, shrink while invalid)

Pseudocode

Sol 1: MRI HashMap With Jumping Left
1. (mri = {})
2. (left, right = 0, 0)
3. (maxLen = 0)
4. (n = len(s)-1)
5. while right <= n:
   a. newChar = s[right]
   b. if newChar in mri:
         prevIndex = mri[newChar]
         if left <= prevIndex <= right: 
             left = prevIndex + 1
   c. mri[newChar] = right
   d. currLen = right - left + 1
   d. maxLen = max(maxLen, currLen)
   e. right += 1
6. return maxLen

Solution 2: Window HashSet Tracking Chars With Removal
1. (window = {}) 
2. (left, right = 0, 0)
3. (maxLen = 0)
4. (n = len(s)-1)
5. while right < n:
   a. newChar = s[right]
   b. while newChar in window:
         window.remove(s[left])
         left += 1
   c. window.add(newChar)
   d. maxLen = max(maxLen, len(window))
   e. right += 1
6. return maxLen

Solution 1: [Sliding Window] MRI HashMap Jump Left When Invalid - Sliding Window/Variable Size Window

    def lengthOfLongestSubstring(self, s: str) -> int:

        # Sliding Window (Variable Size)

        # Window Representation:
        #   - HashSet contains most recent index of all chars encountered
        #   - Window boundaries kept via [left, right]
        #   - Valid window contains no duplicate characters

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - If new character results in a duplicate,
        #     move the left window boundary to 1 in front of the duplicate
        #   - For each valid window compare size to max window

        n = len(s)-1

        # Track more recent index appearance for char
        mri = {} 

        # Sliding Window Variables
        # left and right boundary of substring
        left = 0
        right = 0

        # max length so far
        maxLen = 0

        # tc: O(n)
        while right <= n:
                
                # Character we just added to substring
                newChar = s[right]

                # If we have encountered character already
                if newChar in mri:
                    
                    # Grab index of previous encounter
                    prevIndex = mri[newChar]
                    
                    # Jump from left if window is invalid:
                    # - jump left to 1 index after duplicate
                    if left <= prevIndex <= right:
                        left = prevIndex + 1

                # Update most recent index for character
                mri[newChar] = right

                # Valid window, compare to max
                currLen = right - left + 1
                maxLen = max(maxLen, currLen)

                # Iterate window
                right += 1
            
        # overall: tc O(n)
        # overall: sc O(min(n, k))
        return maxLen

Solution 2: [Sliding Window] HashSet Tracking Window Boundaries And Shrink While Invalid - Sliding Window/Variable Size Window

    def lengthOfLongestSubstring(self, s: str) -> int:

        # Sliding Window HashSet Right Left Representation (Variable Size Window)

        # Window Representation:
        #   - Set() contains chars within window [left, right]    
        #   - Numbers enter and exit window via left and right
        #   - Valid window contains no duplicate characters

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - If duplicate found, shrink window from left until duplicate is removed
        #   - For each valid window compare size to max window

        n = len(s)

        # Sliding Window Data:
        # sc: O(n)
        window = set()

        # Sliding Window Boundaries:
        left = 0
        right = 0

        # Track longest valid window
        maxLen = 0

        # tc: O(n)
        while right < n:

            # Grab char we want to add
            newChar = s[right]

            # Shrink from left while window is invalid:
            # - if duplicate exists in window set
            # tc: O(n)
            while newChar in window:
                window.remove(s[left])
                left += 1

            # Add char to window
            window.add(newChar)

            # Compare valid window length to max
            maxLen = max(maxLen, len(window))

            # Expand valid window
            right += 1

        # overall: tc O(n)
        # overall: sc O(min(n, k))
        return maxLen

1208. Get Equal Substrings Within Budget ::1:: - Medium

Topics: String, Sliding Window

Intro

You are given two strings s and t of the same length and an integer maxCost. You want to change s to t. Changing the ith character of s to ith character of t costs |s[i] - t[i]| (i.e., the absolute difference between the ASCII values of the characters). Return the maximum length of a substring of s that can be changed to be the same as the corresponding substring of t with a cost less than or equal to maxCost. If there is no substring from s that can be changed to its corresponding substring from t, return 0.

Example InputOutput
s = "abcd", t = "bcdf", maxCost = 33
s = "abcd", t = "cdef", maxCost = 31
s = "abcd", t = "acde", maxCost = 01

Constraints:

1 ≤ s.length ≤ 10^5

t.length == s.length

0 ≤ maxCost ≤ 10^6

s and t consist of only lowercase English letters.

Abstraction

Given an array, and a variable size window, find the longest window that can be changed to match another array while staying under the budget of maxCost with cost being the absolute difference between the ASCII values of the characters.

(expand while valid, shrink while invalid)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Cost Tracking Window Cost And Shrink While Invalid - Sliding Window/Variable Size Window

    def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
        
        # Sliding Window + Two Pointers
        
        # Substring Representation:
        #   - Cost tracks cost to change every character in s to match t
        #   - Valid window has a windowChangeCost <= maxCost

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Add new char cost to change s[right] -> t[right] to current total window cost
        #   - If cost exceeds budget, shrink window from left until cost is valid again
        #   - For each valid window compare size to max window

        n = len(s)
        
        # Sliding Window Variables
        left = 0
        right = 0
        maxLen = 0

        # Cost to change entire current window from s -> t
        windowChangeCost = 0

        # tc: O(n)
        while right < n: 

            # Add new char cost to window cost:
            # - cost of single char from s -> t
            rightCharS = s[right]
            rightCharT = t[right]
            cost = abs(ord(rightCharS) - ord(rightCharT))
            windowChangeCost += cost

            # Shrink from left while window is invalid:
            # - curr cost to change entire window is higher than budget
            while maxCost < windowChangeCost:

                # Remove old character and save cost from window cost:
                # - cost of single char from s -> t
                leftCharS = s[right]
                leftCharT = t[right]
                saving = abs(ord(s[left]) - ord(t[left]))
                windowChangeCost -= saving

                # Shrink window
                left += 1
            
            # Valid window, compare to max
            windowLen = (right - left) + 1
            maxLen = max(maxLen, windowLen)

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxLen

1004. Max Consecutive Ones III ::2:: - Medium

Topics: Array, Binary Search, Sliding Window, Prefix Sum

Intro

Given a binary array nums and an integer k, return the maximum number of consecutive 1's in the array if you can flip at most k 0's.

Example InputOutput
nums = [1,1,1,0,0,0,1,1,1,1,0], k = 26
nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], k = 310

Constraints:

1 ≤ nums.length ≤ 10^5

nums[i] is either 0 or 1

0 ≤ k ≤ nums.length

Abstraction

Given a binary array, and a variable size window, find the longest window that can be changed to all 1's while staying under the budget of k flips from 0 to 1.

(expand while valid, shrink while invalid)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Two Pointer Sliding Window With Shrinking Max Tracking - Sliding Window/Variable Size Window

    def longestOnes(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Zero tracks total flips needed for substring to be consecutive 1's
        #   - Valid window only has consecutive 1's
        #   - Valid window has zeroCount <= k

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Add new int to zero count
        #   - If zeroCount exceeds budget, shrink window from left until num of zeros is valid again
        #   - For each valid window compare size to max window

        n = len(nums)-1

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Number of zeros in current window
        zeroCount = 0

        # Longest valid window seen
        maxLen = 0

        # tc: O(n)
        while right <= n:

            # Update zero count:
            if nums[right] == 0:
                zeroCount += 1

            # Shrink from left while window is invalid:
            #   - num of 0's must be <= k
            # tc: O(1) amortized
            while k < zeroCount:

                # Update count
                if nums[left] == 0:
                    zeroCount -= 1
                left += 1

            # Validate window, compare size to max
            currWindow = right - left + 1
            maxLen = max(maxLen, currWindow)

        # overall: tc O(n)
        # overall: sc O(1)
        return maxLen

424. Longest Repeating Character Replacement ::1:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

You are given a string s and an integer k. You can choose any character of the string and change it to any other uppercase English character. You can perform this operation at most k times. Return the length of the longest substring containing the same letter you can get after performing the above operations.

Example InputOutput
s = "ABAB", k = 24
s = "AABABBA", k = 14

Constraints:

1 ≤ s.length ≤ 105

s consists of only uppercase English letters.

0 ≤ k ≤ s.length

Abstraction

Given an array, and a variable size window, find the longest window that can be changed to all the same character while staying under the budget of k character changes.

(expand while valid, shrink while invalid)

Pseudocode

Sol 1: HashMap Tracking CharCount To Find MostOccur
1. (charCount = {}) 
2. (left, right = 0, 0)  
3. (mostOccur = 0) 
4. (maxLen = 0)
2. while right < n:
   a. charCount[s[right]] += 1
   b. mostOccur = max(mostOccur, charCount[s[right]])
   c. currWindowLen = right - left + 1
   d. allOtherChars = currWindowLen - mostOccur
   e. while k < allOtherChars:
        charCount[s[left]] -= 1
        left += 1
        currWindowLen = right - left + 1
        allOtherChars = currWindowLen - mostOccur
   f. maxLen = max(maxLen, currWindowLen)
   g. right += 1
3. return maxLen

Solution 1: [Sliding Window] Count HashMap Tracking Char In Window To Find MostOccur To Verify AllOtherChars In Window Are Replaceable Or Shrink From Left Until AllOtherChars Are Replaceable Less Than Or Equal To K - Sliding Window/Variable Size Window

    def characterReplacement(self, s: str, k: int) -> int:
        
        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - HashMap tracks char count within window
        #   - Valid window contains a single consecutive character
        #   - Valid window changes at most k characters from c -> seqChar

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Add new char to count
        #   - mostOccur is total count of the most occurring character in the window
        #   - allOtherChars is the total count of all other characters in the window
        #   - If allOtherChars is more than the char change budget k, shrink window from left until allOtherChars <= k

        n = len(s)

        # char count within window boundaries
        count = defaultdict(int)

        # Sliding Window Variables
        # sc: O(1)
        # left and right window boundaries
        left = 0
        right = 0

        # Sliding Window Data
        # sc: O(1)
        # mostOccur: most occurring character in window
        mostOccur = 0  
        # max length seen so far
        maxLen = 0

        # tc: O(n)
        while right < n:

            # Character we just encountered
            # tc: O(1)
            currChar = s[right]
            charCount[currChar] += 1

            # Update mostOccur in window
            # tc: O(1)
            mostOccur = max(mostOccur, charCount[currChar])

            # length of curr window
            currWindowLen = right - left + 1

            # how many other chars exists, (anything that is not mostOccur)
            allOtherChars = currWindowLen - mostOccur

            # Shrink from left while window is invalid:
            # We need allOtherChars to be less than k (num of chars we can replace),
            # so that we can replace allOtherChars with the mostOccur.
            # thus, shrink window from left by 1 until allOtherChars is valid
            # tc: O(1) amortized
            while k < allOtherChars:

                # Do NOT update mostOccur here:
                #   - while shrinking, the mostOccur may become stale and decreased
                #   - mostOccur may have shifted to a different character

                # This is fine because:
                #   - for window [left:right], we have agreed 
                #     that mostOccur is highest char up to right
                #   - A stale mostOccur will under estimate allOtherChars 
                
                # This is safe because: 
                #   - No char can exceed the current mostOccur while shrinking,
                #     we are only shrinking the window so counts can only go down
                
                # Thus during shrink, mostOccur:
                #   - not acting as a true most occurring character
                #   - acting as a flag for the original mostOccur for original window
                #     that existed before shrinking
                
                # Remove left character from window
                charCount[s[left]] -= 1

                left += 1

                # Recompute currWindowLen and allOtherChars after shrink
                currWindowLen = right - left + 1
                allOtherChars = currWindowLen - mostOccur

            # Once allOtherChars is valid, check maxLen
            maxLen = max(maxLen, currWindowLen)

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return maxLen

209. Minimum Size Subarray Sum ::2:: - Medium

Topics: Binary Search, Sliding Window, Prefix Sum

Intro

Given an array of positive integers nums and a positive integer target, return the minimal length of a whose sum is greater than or equal to target. If there is no such subarray, return 0 instead. Follow up: If you have figured out the O(n) solution, try coding another solution of which the time complexity is O(n log(n)).

Example InputOutput
target = 7, nums = [2,3,1,2,4,3]2
target = 4, nums = [1,4,4]1
target = 11, nums = [1,1,1,1,1,1,1,1]0

Constraints:

1 ≤ target ≤ 10^9

1 ≤ nums.length ≤ 10^5

1 ≤ nums[i] 10^4

Abstraction

Given an array, and a variable size window, find the smallest window that has a sum greater than or equal to target.

(expand while invalid, shrink while valid)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Running Sum Window Shrink While Valid - Sliding Window/Variable Size Window

    def minSubArrayLen(self, target: int, nums: List[int]) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Running count tracks sum of window
        #   - Valid window has sum greater than or equal to target

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Add new value to running sum
        #   - All nums are positive, sum always increases when extending and decreases when shrinking
        #   - If sum exceeds target, shrink from left until sum is valid again
        #   - For each valid window compare size to max window

        n = len(nums)

        left = 0

        # Sum of current window
        runningSum = 0
        
        minLen = float('inf')

        # tc: O(n)
        for right in range(n):

            # Add num to running count
            windowSum += nums[right]

            # Shrink from left while window is valid:
            # - windowSum meets or exceeds target
            # tc: O(1) amortized
            while target <= windowSum:

                # Valid window, compare to min length 
                currWindowLen = right - left + 1
                minLen = min(minLen, currWindowLen)

                # Remove left num from window sum
                windowSum -= nums[left]

                # Shrink window
                left += 1

        # No valid window was found
        if minLen == float('inf'):
            return 0
        
        # overall: tc O(n)
        # overall: sc O(1)
        return minLen

76. Minimum Window Substring ::2:: - Hard

Topics: Hash Table, String, Sliding Window

Intro

Given two strings s and t of lengths m and n respectively, return the minimum window of s such that every character in t (including duplicates) is included in the window. If there is no such substring, return the empty string "". The testcases will be generated such that the answer is unique.

Example InputOutput
s = "ADOBECODEBANC", t = "ABC""BANC"
s = "a", t = "a""a"
s = "a", t = "aa"""

Constraints:

m == s.length

n == t.length

1 ≤ m, n ≤ 105

s and t consist of uppercase and lowercase English letters.

Abstraction

Given two arrays, and a variable size window with a min length of the first array, find the smallest array such that every element in the first array is included in the window of the second array.

(expand while valid, shrink while valid)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Needle and Window Count HashMap with Have and Need Flag Indicate Valid Window Shrink While Valid - Sliding Window/Fixed Size Window

    def checkInclusion(self, s: str, t: str) -> bool:

        # Sliding Window (Fixed Size Window)

        # Window Representation:
        #   - Two Hashmaps:
        #       needleFreq -> required counts
        #       windowFreq -> counts in current window
        #   - Valid window has equal char counts needle chars between needle and window freq

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Track how many required character are satisfied via needleFreq
        #   - Add new char count to windowFreq
        #   - While chars between needle and window freq are satisfied, 
        #     shrink from left to minimize window
        #   - For each valid window compare size to min window

        # haystack < needle: 
        # If haystack is smaller than needle, 
        # substring is not possible
        if len(s) < len(t):
            return ""

        # Build needle character map
        # sc: O(m)
        needleCharCount = defaultdict(int)
        for c in t:
            needleCharCount[c] += 1

        # Sliding Window Variables
        left = 0
        right = 0

        # Char count for current window
        windowCharCount = defaultdict(int)

        # Length and starting index for min window
        minLen = float('inf')
        minStart = 0

        # Flags for unique chars slots from needle hashmap length
        needCharSlots = len(needleCharCount) 
        haveCharSlots = 0                 

        # tc: O(n)
        for right in range(len(s)):

            # Expand window
            rightChar = s[right]
            windowCharCount[rightChar] += 1

            # If char is a specific slot char
            if rightChar in needleCharCount:

                # If char has met requirements
                if windowCharCount[rightChar] == needleCharCount[rightChar]:
                    haveCharSlots += 1


            # Shrink from left while window is valid:
            #   - All specific slot chars have met requirements
            while haveCharSlots == needCharSlots:
                
                # Valid window, compare to min
                windowLen = right - left + 1                
                if windowLen < minLen:
                    minLen = windowLen
                    minStart = left

                # Remove left character
                leftChar = s[left]
                windowCharCount[leftChar] -= 1
                
                # If char is a slot char with requirements
                if leftChar in needleCharCount:

                    # If char no longer meets requirements
                    if  windowCharCount[leftChar] < needleCharCount[leftChar]:
                        haveCharSlots -= 1

                # Shrink window
                left += 1

        # If no valid substring was found
        if minLen == float("inf"):
            return ""

        # Grab min substring
        res = s[minStart:minStart + minLen]

        # overall: tc O(n + m)
        # overall: sc O(n)
        return res

Solution 2: [Sliding Window] Needle Hashmap And stillNeedThisMany And GlobalRemainingCountAcrossChars Indicate Valid Window Shrink While Valid - Sliding Window/Variable Size Window

    def minWindow(self, s: str, t: str) -> str:

        # Sliding Window (Variable Size Window)

        # Substring Representation:
        #   - One HashMap: 
        #       char -> remaining required count
        #   - targetCharsRemaining: tracks total chars with requirements still needed

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Update char remaining needed count
        #   - 
        #   - Decrease count of remaining required count
        #   - When targetCharsRemaining == 0, window is valid
        #   - Shrink from left to minimize window
        #   - Track smallest valid window

        # Needle is longer than haystack:
        # substring not possible
        if len(s) < len(t):
            return ""

        # Build required frequency map
        windowCharCount = defaultdict(int)
        for ch in t:
            windowCharCount[ch] += 1

        # Total chars still needed
        globalRemainingCountAcrossChars = len(t)         

        # Sliding Window Variables
        left = 0
        right = 0

        # Min window substring
        minWindowRight = float('inf')
        minWindowLeft = 0          

        # tc: O(n)
        for right in range(len(s)):

            # Expand window
            rightChar = s[right]
            
            # windowCharCount:
            # To start, hashmap will contains:
            #   - positive values of required needle chars
            #   - all other possible keys are set to 0
            # Moving forward, all other possible keys will 
            # always be less than or equal to 0 (regardless of adding or removing).
            # Thus, to track chars that are in the needle,
            # we only count chars that at some point have a positive count,
            # to contribute towards decreasing or increasing the global remaining 
            stillNeedThisMany = windowCharCount[rightChar]

            if stillNeedThisMany > 0:
                globalRemainingCountAcrossChars -= 1

            # Increase char count for windowCharCount 
            # regardless of it being a required or non-required char,
            # both cases covered per above
            windowCharCount[rightChar] -= 1

            # Shrink from left while window is valid:
            #   - Total needle char count is covered
            # tc: amortized O(n)
            while globalRemainingCountAcrossChars == 0:

                # Valid window, compare to min
                currWindowLen = right - left + 1
                minWindowLen = minWindowRight - minWindowLeft + 1
                if currWindowLen < minWindowLen:
                    minWindowRight = right
                    minWindowLeft = left

                # Remove left character, and to char required count,
                # again per above:
                #   - required needle chars have a positive required count
                #   - non-required chars have a required count of 0
                leftChar = s[left]
                windowCharCount[leftChar] += 1

                # If this char has a positive required count:
                #   - it is a required needle char
                #   - we need one more copy of the char, since we just lost a occurrence of it
                #   - add to global need count
                stillNeedThisMany = windowCharCount[leftChar]

                if stillNeedThisMany > 0:
                    globalRemainingCountAcrossChars += 1

                # Shrink window
                left += 1

        # No valid window was found
        if minWindowRight == float("inf"):
            return ""

        # Grab min window
        res = s[minWindowLeft:minWindowRight + 1]

        # overall: tc O(n + m)
        # overall: sc O(n)
        return res

1234. Replace the Substring for Balanced String :::: - Medium

Topics: String, Sliding Window

Intro

You are given a string s of length n containing only four kinds of characters: 'Q', 'W', 'E', and 'R'. A string is said to be balanced if each of its characters appears n / 4 times where n is the length of the string. Return the minimum length of the substring that can be replaced with any other string of the same length to make s balanced. If s is already balanced, return 0.

Example InputOutput
s = "QWER"0
s = "QQWE"1
s = "QQQW"2

Constraints:

n == s.length

4 ≤ n ≤ 10^5

n is a multiple of 4

s contains only 'Q', 'W', 'E', and 'R'.

Abstraction

Given an array, and a variable size window, find the smallest window that can be replaced to make the array balanced, valid meaning that each character appears no more than n/4 times.

(expand while invalid, shrink while valid)

(outside window count tracking)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Full Frequency HashMap Tracking All Chars Outside Window - Sliding Window/Variable Size Window

    def balancedString(self, s: str) -> int:

        # Sliding Window (Variable Size Window)

        # String Representation:
        #   - HashMap tracks char count OUTSIDE window
        #   - Valid window makes up for uneven outside count by changing chars INSIDE the window

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Remove chars from OUTSIDE count when they enter the window
        #   - Once all outside frequencies <= target, window is valid
        #   - For each valid window compare size to min window
        
        #   s = "QQWE", target = 1
        #   outsideCount = {Q:2, W:1, E:1, R:0}
        
        # When window is at the first Q,
        # that Q is remove from the OUTSIDE char count,
        # leaving the max(outsideCount.values()) -> 1
        # which is equal to the target == 1,
        # meaning that the current window is the smallest possible section
        # we can replace to make the entire string balanced

        n = len(s)

        # Target char count:
        #   - s length is multiple of 4, and has 4 possible characters (Q W E R)
        #   - balanced s has each char appear n // 4 times
        target = n // 4

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Outside HashMap:
        #   - Stores frequencies of chars OUTSIDE current window
        outsideCount = defaultdict(int)
        for c in s:
            outsideCount[c] += 1

        # Minimum valid window size
        minLen = float('inf')

        # Balanced Check:
        #   - Current window length is 0
        #   - All chars are outside the window (we are checking the original string)
        #   - If balanced value == target, then the string is already balanced
        # tc: O(1)
        if max(outsideCount.values()) == target:
            return 0

        # Expand sliding window
        # tc: O(n)
        for right in range(n):

            # Add char to window,
            # remove new char from OUTSIDE count
            rightChar = s[right]
            outsideCount[rightChar] -= 1

            # Shrink from left while window is valid: 
            #   - OUTSIDE count balanced value is equal to the target  
            #   - string is balanced
            #   - left has not passed right (left and catch up to right, leaving a window length of 1)
            # tc: O(1) amortized
            while left <= right and max(outsideCount.values()) == target:

                # Valid window, compare to min
                currWindowLen = right - left + 1
                minLen = min(minLen, currWindowLen)

                # Remove char from window,
                # add char back to OUTSIDE count
                leftChar = s[left]
                outsideCount[leftChar] += 1

                # Shrink window
                left += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return minLen

1358. Number of Substrings Containing All Three Characters ::2:: - Medium

Topics: Hash Table, String, Sliding Window

Intro

Given a string s consisting only of characters a, b and c. Return the number of substrings containing at least one occurrence of all these characters a, b and c.

Example InputOutput
s = "abcabc"10
s = "aaacb"3
s = "abc"1

Constraints:

3 ≤ s.length ≤ 5 * 10^4

s only consists of a, b or c characters

Abstraction

Given a string and a variable size window, count the substrings that contain all three characters: a, b, c.

(expand right, shrink while valid)

(Monotonic: a valid window stays valid when extended outward, so count the whole batch at once instead of visiting each substring)

(Count starts for each end, after the shrink loop) left = "how many starts can I slide back (toward index 0) and still be valid?"

(Count ends for each start, inside the shrink loop) n - right = "how many ends can I slide forward (toward index n-1) and still be valid?"

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Shrink Until Invalid Then Add Left (Count Starts For Each End) - Sliding Window/Variable Size Window

    def numberOfSubstrings(self, s: str) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - HashMap stores frequency of each char in current window
        #   - Window is valid when it contains at least one of each: a, b, c

        # Idea:
        #   - Expand right to add new characters
        #   - Once window is valid (has all 3 chars), shrink from left
        #     to find the smallest valid window ending at right
        #   - Every starting index from [0, left-1] also forms a valid substring
        #     ending at right, since adding more chars to the left keeps it valid
        #   - So add left to result at each right

        n = len(s)-1

        # HashMap: char -> frequency in current window
        # sc: O(1) at most 3 chars
        count = defaultdict(int)

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Total valid substrings found
        subarrayCount = 0

        # tc: O(n)
        while right <= n:

            # Add char count to window
            count[s[right]] += 1

            # Shrink from left while window is valid:
            # - window contains all 3 chars
            while True:

                # Check if window contains all 3 characters
                windowValid = True
                for c in "abc":
                    if count[c] <= 0:
                        windowValid = False
                        break

                # Stop Shrinking
                if not windowValid:
                    break

                # Decrease char count
                count[s[left]] -= 1

                # Shrink window
                left += 1

            # Include all valid subarrays from 0 to left:
            # [z, z, z, a b c] -> left is set to index 4, when window becomes invalid
            # so there are 4 valid subarrays
            # tc: O(1)
            subarrayCount += left

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return subarrayCount

Solution 2: [Sliding Window] Shrink While Valid Then Add N Minus Right (Count Ends For Each Start) - Sliding Window/Variable Size Window

    def numberOfSubstrings(self, s: str) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - HashMap stores frequency of each char in current window
        #   - Window is valid when it contains at least one of each: a, b, c

        # Idea:
        #   - Expand right to add new characters
        #   - Once window is valid (has all 3 chars), count every end that
        #     keeps this start valid, then shrink from left
        #   - Window [left..right] is valid, so with the same start,
        #     [left..right+1], [left..right+2], ... [left..n-1] are also valid,
        #     since adding more chars to the right keeps it valid
        #   - So add n - right to result for each valid start

        n = len(s)

        # HashMap: char -> frequency in current window
        # sc: O(1) at most 3 chars
        count = defaultdict(int)

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Total valid substrings found
        subarrayCount = 0

        # tc: O(n)
        while right < n:

            # Add char count to window
            count[s[right]] += 1

            # Shrink from left while window is valid:
            # - window contains all 3 chars
            while True:

                # Check if window contains all 3 characters
                windowValid = True
                for c in "abc":
                    if count[c] <= 0:
                        windowValid = False
                        break

                # Stop Shrinking
                if not windowValid:
                    break

                # Include all valid subarrays starting at left:
                # [a b c z z z] -> left = 0, right = 2, n = 6
                # ends at 2, 3, 4, 5 are all valid
                # so there are n - right = 4 valid subarrays
                # tc: O(1)
                subarrayCount += n - right

                # Decrease char count
                count[s[left]] -= 1

                # Shrink window
                left += 1

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return subarrayCount

Solution 3: [Greedy] 3 MRI Variables Add Min MRI Plus 1 (Count Starts For Each End) - Sliding Window/Variable Size Window

    def numberOfSubstrings(self, s: str) -> int:

        # Last Seen Index Tracking (Single Pass)

        # Representation:
        #   - Track the last seen index of each char: a, b, c
        #   - Valid window contains all 3 chars

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - Ignore all other chars
        #   - Update abc index to mri
        #   - All valid windows start at the left most mri after full iteration

        n = len(s) - 1

        # Init mri to -1,
        # if char is never encountered then count results in 0
        mriA = mriB = mriC = -1

        # Total valid substrings found
        count = 0

        right = 0

        # tc: O(n)
        while right <= n:

            # Update last seen index for current char
            if s[i] == 'a':
                mriA = i
            elif s[i] == 'b':
                mriB = i
            elif s[i] == 'c':
                mriC = i

            # Leftmost mri:
            # [z, z, z, a b c] -> mri for a is 3, 
            # so there are 3 + 1 = 4 subarrays with all 4 chars
            count += min(mriA, mriB, mriC) + 1

            # Extend window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return count

2537. Count the Number of Good Subarrays ::1:: - Medium

Topics: Array, Hash Table, Sliding Window

Intro

Given an integer array nums and an integer k, return the number of good subarrays of nums. A subarray arr is good if there are at least k pairs of indices (i, j) such that i < j and arr[i] == arr[j]. A subarray is a contiguous non-empty sequence of elements within an array.

Example InputOutput
nums = [1,1,1,1,1], k = 101
nums = [3,1,4,3,2,2,4], k = 24

Constraints:

1 ≤ nums.length ≤ 10^5

1 ≤ nums[i], k ≤ 10^9

Abstraction

Given an array and a variable size window, count the subarrays that have at least k pairs of equal values (i < j, nums[i] == nums[j]).

(Monotonic: a valid window stays valid when extended outward, since adding a num can only add pairs, never remove them, so count the whole batch at once instead of visiting each subarray)

(expand right, shrink while valid)

(Pair count: pairs += count[num] = "how many earlier copies of this num are in the window to pair with?") (Read count BEFORE incrementing, since a num can't pair with itself) (Removal mirrors it: decrement first, then pairs -= count[num])

(Count starts for each end, after the shrink loop) left = "how many starts can I slide back (toward index 0) and still be valid?"

(Count ends for each start, inside the shrink loop) n - right = "how many ends can I slide forward (toward index n-1) and still be valid?"

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Frequency HashMap With Pair Count, Shrink While Valid Then Add Left After Loop (Count Starts For Each End) - Sliding Window/Variable Size Window

    def countGood(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - HashMap stores frequency of each num in current window
        #   - pairs tracks number of equal-value index pairs (i < j) in current window
        #   - Window is valid when pairs >= k

        # Idea:
        #   - "At least k pairs" is monotonic under extension:
        #       adding a num can only add pairs, so a valid window stays valid when extended
        #   - Expand right to add new nums
        #   - Adding a num with c existing copies creates c new pairs
        #   - Shrink from left while window is valid (pairs >= k) until it becomes invalid
        #   - Removing a num with c remaining copies destroys c pairs
        #   - Every starting index from [0, left-1] forms a valid subarray
        #     ending at right, since adding more nums to the left keeps it valid
        #   - So add left to result at each right

        n = len(nums)

        # HashMap: num -> frequency in current window
        # sc: O(n)
        count = defaultdict(int)

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Equal-value index pairs in current window
        pairs = 0

        # Total valid subarrays
        subarrayCount = 0

        # tc: O(n)
        while right < n:

            # Add right num to window
            # - each existing copy forms a new pair with it
            # - read count before incrementing, a num can't pair with itself
            rightNum = nums[right]
            pairs += count[rightNum]
            count[rightNum] += 1

            # Shrink from left while window is valid:
            # - at least k pairs in window
            # tc: O(1) amortized
            while pairs >= k:

                # Remove left num from window
                # - decrement first, remaining copies are the pairs lost
                leftNum = nums[left]
                count[leftNum] -= 1
                pairs -= count[leftNum]

                # Shrink window
                left += 1

            # Include all valid subarrays ending at right:
            # nums = [1, 1, 1, 1, 1], k = 3, right = 3
            # shrink loop stops with left = 2 (window [1, 1] has only 1 pair)
            # starts 0 and 1 are valid:
            #   [1, 1, 1, 1], [1, 1, 1]
            # so there are left = 2 valid subarrays
            #
            # tc: O(1)
            subarrayCount += left

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return subarrayCount

Solution 2: [Sliding Window] Frequency HashMap With Pair Count, Shrink While Valid Then Add N Minus Right (Count Ends For Each Start) - Sliding Window/Variable Size Window

    def countGood(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - HashMap stores frequency of each num in current window
        #   - pairs tracks number of equal-value index pairs (i < j) in current window
        #   - Window is valid when pairs >= k

        # Idea:
        #   - "At least k pairs" is monotonic under extension:
        #       adding a num can only add pairs, so a valid window stays valid when extended
        #   - Expand right to add new nums
        #   - Adding a num with c existing copies creates c new pairs
        #   - Once window is valid (pairs >= k), count every end that
        #     keeps this start valid, then shrink from left
        #   - Window [left..right] is valid, so with the same start,
        #     [left..right+1], [left..right+2], ... [left..n-1] are also valid,
        #     since adding more nums to the right keeps it valid
        #   - Removing a num with c remaining copies destroys c pairs
        #   - So add n - right to result for each valid start

        n = len(nums)

        # HashMap: num -> frequency in current window
        # sc: O(n)
        count = defaultdict(int)

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Equal-value index pairs in current window
        pairs = 0

        # Total valid subarrays
        subarrayCount = 0

        # tc: O(n)
        while right < n:

            # Add right num to window
            # - each existing copy forms a new pair with it
            # - read count before incrementing, a num can't pair with itself
            rightNum = nums[right]
            pairs += count[rightNum]
            count[rightNum] += 1

            # Shrink from left while window is valid:
            # - at least k pairs in window
            # tc: O(1) amortized
            while pairs >= k:

                # Include all valid subarrays starting at left:
                # nums = [1, 1, 1, 1, 1], k = 3, n = 5
                # left = 0, right = 2
                # ends at 2, 3, 4 are all valid:
                #   [1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1, 1]
                # so there are n - right = 5 - 2 = 3 valid subarrays
                #
                # tc: O(1)
                subarrayCount += n - right

                # Remove left num from window
                # - decrement first, remaining copies are the pairs lost
                leftNum = nums[left]
                count[leftNum] -= 1
                pairs -= count[leftNum]

                # Shrink window
                left += 1

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return subarrayCount

2962. Count Subarrays Where Max Element Appears at Least K Times ::1:: - Medium

Topics: Array, Sliding Window

Intro

You are given an integer array nums and a positive integer k. Return the number of subarrays where the maximum element of nums appears at least k times in that subarray. A subarray is a contiguous sequence of elements within an array.

Example InputOutput
nums = [1,3,2,3,3], k = 26
nums = [1,4,2,1], k = 30

Constraints:

1 ≤ nums.length ≤ 10^5

1 ≤ nums[i] ≤ 10^6

1 ≤ k ≤ 10^5

Abstraction

Given an array and a variable size window, count the subarrays where the global maximum element appears at least k times.

(precompute global max, then track its frequency in the window) (expand right, shrink while valid)

(Monotonic: a valid window stays valid when extended outward, so count the whole batch at once instead of visiting each subarray)

(Count starts for each end, after the shrink loop) left = "how many starts can I slide back (toward index 0) and still be valid?"

(Count ends for each start, inside the shrink loop) n - right = "how many ends can I slide forward (toward index n-1) and still be valid?"

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Shrink While Valid Then Add Left After Loop (Count Starts For Each End) - Sliding Window/Variable Size Window

    def countSubarrays(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - maxCount tracks occurrences of the global max element in window
        #   - Window is valid when maxCount >= k

        # Idea:
        #   - Find global maximum element
        #   - Expand right to add new nums
        #   - Once window is valid (max appears at least k times), shrink from left
        #     until the window becomes invalid
        #   - Every starting index from [0, left-1] forms a valid subarray
        #     ending at right, since adding more nums to the left keeps it valid
        #   - So add left to result at each right

        n = len(nums)

        # Global maximum element
        maxNum = max(nums)

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Occurrences of maxNum in current window
        maxCount = 0

        # Total valid subarrays
        subarrayCount = 0

        # tc: O(n)
        while right < n:

            # Add right num to window
            if nums[right] == maxNum:
                maxCount += 1

            # Shrink from left while window is valid:
            # - max element appears at least k times
            # tc: O(1) amortized
            while maxCount >= k:

                # Remove left num from window
                if nums[left] == maxNum:
                    maxCount -= 1

                # Shrink window
                left += 1

            # Include all valid subarrays ending at right:
            # nums = [1, 3, 3], k = 2, right = 2
            # shrink loop stops with left = 2 (window [3] has only one 3)
            # starts 0 and 1 are valid -> [1, 3, 3], [3, 3]
            # so there are left = 2 valid subarrays
            # tc: O(1)
            subarrayCount += left

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return subarrayCount

Solution 2: [Sliding Window] Shrink While Valid Then Add N Minus Right (Count Ends For Each Start) - Sliding Window/Variable Size Window

    def countSubarrays(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - maxCount tracks occurrences of the global max element in window
        #   - Window is valid when maxCount >= k

        # Idea:
        #   - Find global maximum element
        #   - Expand right to add new nums
        #   - Once window is valid (max appears at least k times), count every end
        #     that keeps this start valid, then shrink from left
        #   - Window [left..right] is valid, so with the same start,
        #     [left..right+1], [left..right+2], ... [left..n-1] are also valid,
        #     since adding more nums to the right keeps it valid
        #   - So add n - right to result for each valid start

        n = len(nums)

        # Global maximum element
        maxNum = max(nums)

        # Sliding Window Boundaries
        left = 0
        right = 0

        # Occurrences of maxNum in current window
        maxCount = 0

        # Total valid subarrays
        subarrayCount = 0

        # tc: O(n)
        while right < n:

            # Add right num to window
            if nums[right] == maxNum:
                maxCount += 1

            # Shrink from left while window is valid:
            # - max element appears at least k times
            # tc: O(1) amortized
            while maxCount >= k:

                # Include all valid subarrays starting at left:
                # nums  = [3, 3, 1], k = 2, n = 3
                # index =  0  1  2
                #          ^  ^
                #          l  r
                #
                # ends at 1, 2 are valid
                # so there are n - right = 3 - 1 = 2 valid subarrays
                #
                #   [0..1] = [3, 3]       two 3's => valid
                #   [0..2] = [3, 3, 1]    two 3's => valid
                #
                # tc: O(1)
                subarrayCount += n - right

                # Remove left num from window
                if nums[left] == maxNum:
                    maxCount -= 1

                # Shrink window
                left += 1

            # Iterate window
            right += 1

        # overall: tc O(n)
        # overall: sc O(1)
        return subarrayCount

713. Subarray Product Less Than K ::1:: - Medium

Topics: Array, Binary Search, Sliding Window, Prefix Sum

Intro

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.

Example InputOutput
nums = [10,5,2,6], k = 1008
nums = [1,2,3], k = 00

Constraints:

1 ≤ nums.length ≤ 3 * 10^4

1 ≤ nums[i] ≤ 1000

0 ≤ k ≤ 10^6

Abstraction

Given an array, and a variable size window, find the number of contiguous subarrays where the product of all the elements in the subarray is strictly less than k.

(expand while valid, shrink while invalid)

Add all arrays ending at right to total count of valid subarrays, via right - left + 1.

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Running Product Sliding Window - Sliding Window/Variable Size Window

    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:

        # Sliding Window + Running Product

        # Window Representation:
        #   - Product tracks product of current window
        #   - Valid window has a product <= k

        # Rules:
        #   - Expand variable size window over list by extending right
        #   - Multiple new num into running product
        #   - If product is greater than k, shrink window from left until product is valid again 
        #   - For each valid window compare size to max window

        # Early Exit:
        # No array can have a product less than 1,
        # since int values are guaranteed to be 1 <= int
        if k <= 1:
            return 0

        # Sliding window boundaries
        left = 0

        # Running product of current window
        runningProduct = 1

        # Total valid subarrays
        validArrayCount = 0

        # Expand right boundary
        # tc: O(n)
        for right in range(len(nums)):

            # Include current element into window
            runningProduct *= nums[right]

            # Shrink from left while window is invalid:
            # - runningProduct < k
            # tc: O(1) amortized
            while runningProduct >= k:

                # Remove left element from runningProduct
                runningProduct //= nums[left]

                # Shrink window
                left += 1

            # Add all valid subarray ending at right,
            # all other subarrays are counted at different iterations
            arraysEndingAtRight = right - left + 1
            validArrayCount += arraysEndingAtRight

        # overall: tc O(n)
        # overall: sc O(1)
        return validArrayCount

930. Binary Subarrays With Sum ::3:: - Medium

Topics: Array, Hash Table, Sliding Window, Prefix Sum

Intro

Given a binary array nums and an integer goal, return the number of non-empty subarrays with a sum goal. A subarray is a contiguous part of the array.

Example InputOutput
nums = [1,0,1,0,1], goal = 24
nums = [0,0,0,0,0], goal = 015

Constraints:

1 ≤ nums.length ≤ 3 * 10^4

nums[i] is either 0 or 1

0 ≤ goal ≤ nums.length

Abstraction

Given a binary array and a variable size window, count the subarrays whose sum is exactly k.

(Not monotonic: extending a valid window can push the sum past k and break it, so left / n - right do NOT apply)

(Split: exactly k = atMost(k) - atMost(k-1))

(atMost is monotonic under shrinking: since nums are 0 or 1 (never negative), removing a num can't raise the sum, so a valid window stays valid when shrunk inward) (expand right, shrink while invalid, sum too large)

(Count starts for each end, after the shrink loop) right - left + 1 = "how many starts can I slide forward (toward right) and still be valid?"

(Edge case: k = 0 makes atMost(k-1) = atMost(-1), which must return 0 since no subarray has a negative sum)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Two Pointer Sliding Window atMost(k) minus atMost(k minus 1), Add Right Minus Left Plus 1 (Count Starts For Each End) - Sliding Window/Variable Size Window

    def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - windowSum tracks sum of nums in current window
        #   - Window is valid when its sum is at most currGoal

        # Idea:
        #   - "Exactly k" is not monotonic under extension, so left / n - right do NOT apply
        #   - Split it into two "at most" problems:
        #       exactly k = atMost(k) - atMost(k-1)
        #   - "At most" IS monotonic under shrinking:
        #       nums are 0 or 1 (never negative), so removing a num never raises the sum
        #       and a valid window stays valid when shrunk inward
        #   - Expand right to add new nums
        #   - Shrink from left while window is invalid (sum too large)
        #   - Every starting index from [left, right] forms a valid subarray
        #     ending at right, since removing nums from the left keeps it valid
        #   - So add right - left + 1 to result at each right

        # Why the split works:
        #   - atMost(k)   counts subarrays with sum 0, 1, 2, ... k
        #   - atMost(k-1) counts subarrays with sum 0, 1, 2, ... k-1
        #   - subtracting leaves only subarrays with sum exactly k

        # Window Example:
        #   nums = [1, 0, 1, 0, 1], k = 2
        #
        #   atMost(2): all windows with sum <= 2
        #   [1, 0, 1, 0, 1]
        #    LR              sum=1, res += 1
        #    L  R            sum=1, res += 2
        #    L     R         sum=2, res += 3
        #    L        R      sum=2, res += 4
        #    L           R   sum=3 > 2, shrink -> left=1, sum=2, res += 4
        #   atMost(2) = 14
        #
        #   atMost(1): all windows with sum <= 1
        #   [1, 0, 1, 0, 1]
        #    LR              sum=1, res += 1
        #    L  R            sum=1, res += 2
        #    L     R         sum=2 > 1, shrink -> left=1, sum=1, res += 2
        #       L     R      sum=1, res += 3
        #       L        R   sum=2 > 1, shrink -> left=3, sum=1, res += 2
        #   atMost(1) = 10
        #
        #   exactly(2) = atMost(2) - atMost(1) = 14 - 10 = 4
        #   valid subarrays: [1,0,1], [1,0,1,0], [0,1,0,1], [1,0,1]

        # Count of subarrays with sum at most currGoal
        def atMost(currGoal: int) -> int:

            # No subarray has a negative sum (happens when goal = 0)
            if currGoal < 0:
                return 0

            n = len(nums)

            # Sliding Window Boundaries
            left = 0
            right = 0

            # Sum of nums in current window
            # sc: O(1)
            windowSum = 0

            # Total valid subarrays with sum at most currGoal
            subarrayCount = 0

            # tc: O(n)
            while right < n:

                # Add right num to window
                windowSum += nums[right]

                # Shrink from left while window is invalid:
                # - sum is greater than currGoal
                # tc: O(1) amortized
                while windowSum > currGoal:

                    # Remove left num from window
                    windowSum -= nums[left]

                    # Shrink window
                    left += 1

                # Include all valid subarrays ending at right:
                # [left..right] is the largest valid window ending at right,
                # and shrinking it keeps it valid, so every start from
                # left to right works
                #
                # nums = [1, 0, 1, 0, 1], currGoal = 2, right = 3, left = 0
                # starts 0, 1, 2, 3 are valid:
                #   [1, 0, 1, 0], [0, 1, 0], [1, 0], [0]
                # so there are right - left + 1 = 3 - 0 + 1 = 4 valid subarrays
                #
                # Note: this is the mirror of "+= left" (at least k):
                #   at least k: valid starts are 0 ... left-1     -> left
                #   at most k:  valid starts are left ... right   -> right - left + 1
                #
                # tc: O(1)
                subarrayCount += right - left + 1

                # Iterate window
                right += 1

            # tc: O(n)
            # sc: O(1)
            return subarrayCount

        # atMost(k):   counts all subarrays with sum <= k
        # atMost(k-1): counts all subarrays with sum <= k-1
        # Subtracting leaves only subarrays with sum exactly k
        res = atMost(k) - atMost(k - 1)

        # overall: tc O(n)
        # overall: sc O(1)
        return res

Solution 2: [Prefix Sum] Prefix Sum With HashMap [Any Input] - Sliding Window/Variable Size Window

    def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
        
        # Prefix Sum + HashMap

        # Representation:
        #   - prefix_count[s] = number of times prefix sum s has occurred
        #   - curr_sum = running prefix sum up to current index

        # Idea:
        #   - For each index, check how many previous prefix sums
        #     equal curr_sum - goal
        #   - If prefix_count[curr_sum - goal] exists, those subarrays
        #     all sum to goal
        #   - prefix_count[0] = 1 accounts for subarrays starting at index 0

        # goal = 2
        # nums = [1, 0, 1, 0, 1]
        
        # nums = [1, 0, 1, 0, 1]
        # vaild: [.......]
        #        L       R
        #        currSum = 2


        #        exludingSum = 1
        #        [....]       
        # nums = [1, 0, 1, 0, 1]
        # valid:        [......]
        #               L      R
        #               currSum = 3

        # goal - currSum = exludingSum
        # 3 - 2 = 1


        n = len(nums)
        
        # Empty check
        # tc: O(1)
        if n == 0:
            return 0

        # HashMap: prefixSum -> number of occurrences
        # sc: O(n)
        prefixSumCounts = defaultdict(int)

        # Base case: empty prefix has sum 0, seen once
        # tc: O(1)
        prefixSumCounts[0] = 1

        # Running prefix sum
        currSum = 0

        # Total valid subarrays found
        res = 0

        # right boundary of prefixSum [0, right]
        right = 0

        # tc: O(n)
        while right < n:

            # Expand prefix sum
            # tc: O(1)
            currSum += nums[right]

            # If: prefixSum is higher than goal,
            # check if we can restrict left, so that we exclude enough numbers
            # to get our prefixSum back down to goal
            # tc: O(1)
            if currSum >= goal:

                # Check Target:
                # if at any point when iterating left to right,
                # the prefixSum added up to this target
                excludingSum = currSum - goal

                # Check Times:
                # number of times we encountered the target,
                # this tells us how many places we can shift left
                # to exclude numbers to get our prefixSum back down to goal
                excludingSumTimes = prefixSumCounts[excludingSum]

                # Add Times:
                # each location we can move left to,
                # is another prefixSum window that can be restricted via left to achieve goal
                res += excludingSumTimes

            # Record current prefixSum to occurrence count
            # tc: O(1)
            prefixSumCounts[currSum] += 1

            # Move right pointer
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return res

Solution 3: [Prefix Sum] Prefix Sum With Bounded Array [Binary Input Opt] - Sliding Window/Variable Size Window

    def numSubarraysWithSum(self, nums: List[int], goal: int) -> int:
        
        # Prefix Sum + Array of (N+1)

        # Representation:
        #   - prefix_count[s] = number of times prefix sum s has occurred
        #   - curr_sum = running prefix sum up to current index

        # Idea:
        #   - For each index, check how many previous prefix sums
        #     equal curr_sum - goal
        #   - If prefix_count[curr_sum - goal] exists, those subarrays
        #     all sum to goal
        #   - prefix_count[0] = 1 accounts for subarrays starting at index 0

        # goal = 2
        # nums = [1, 0, 1, 0, 1]
        
        # nums = [1, 0, 1, 0, 1]
        # vaild: [.......]
        #        L       R
        #        currSum = 2


        #        exludingSum = 1
        #        [....]       
        # nums = [1, 0, 1, 0, 1]
        # valid:        [......]
        #               L      R
        #               currSum = 3

        # goal - currSum = exludingSum
        # 3 - 2 = 1

        n = len(nums)
        
        # Empty check
        # tc: O(1)
        if n == 0:
            return 0

        # Tracks for up to all possible sums (everything set to 1)
        # how many times we've seen a certain sum
        # sc: O(n)
        prefixSumCount = [0] * (n + 1)

        # Base case: we encounter the sum 0, for the first time
        # tc: O(1)
        prefixSumCount[0] = 1

        # Sum from left up to right
        currSum = 0

        # Total valid subarrays found
        res = 0

        # tc: O(n)
        for num in nums:

            # Expand prefix sum
            # tc: O(1)
            currSum += num

            # If: currSum is higher than goal,
            # check if we can restrict left, so that we exclude enough numbers
            # to get our currSum back down to goal
            # tc: O(1)
            if currSum >= goal:
                
                # Check Target: 
                # if at any point when iterating left to right,
                # the currSum added up to this target
                exludingSum = currSum - goal 

                # Check Times:
                # check number of times we encountered the target,
                # this tells us how many places we can shift left to exlucde numbers to get our currSum back down to goal
                exludingSumTimes = prefixSumCount[exludingSum]

                # Add Times: 
                # each location we can move left to, 
                # is another currSum window that can be restricted via left to achieve goal
                res += exludingSumTimes

            # Record current prefix sum to use in future
            # tc: O(1)
            prefixSumCount[currSum] += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return res

992. Subarrays with K Different Integer ::1:: - Hard

Topics: Array, Hash Table, Sliding Window, Counting

Intro

Given an integer array nums and an integer k, return the number of good subarrays of nums. A good array is an array where the number of different integers in that array is exactly k. For example, [1,2,3,1,2] has 3 different integers: 1, 2, and 3. A subarray is a contiguous part of an array.

Example InputOutput
nums = [1,2,1,2,3], k = 27
nums = [1,2,1,3,4], k = 33

Constraints:

1 ≤ nums.length ≤ 2 * 10^4

1 ≤ nums[i], k ≤ nums.length

Abstraction

Given an array and a variable size window, count the subarrays that contain exactly k distinct integers.

(Not monotonic: extending a valid window can add a (k+1)th distinct value and break it, so left / n - right do NOT apply)

(Split: exactly k = atMost(k) - atMost(k-1))

(atMost is monotonic under shrinking: a valid window stays valid when shrunk inward) (expand right, shrink while invalid, too many distinct)

(Count starts for each end, after the shrink loop) right - left + 1 = "how many starts can I slide forward (toward right) and still be valid?"

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Two Pointer Sliding Window atMost(k) minus atMost(k minus 1), Add Right Minus Left Plus 1 (Count Starts For Each End) - Sliding Window/Variable Size Window

    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - HashMap stores frequency of each num in current window
        #   - Window is valid when it contains at most currK distinct nums

        # Idea:
        #   - "Exactly k" is not monotonic under extension, so left / n - right do NOT apply
        #   - Split it into two "at most" problems:
        #       exactly k = atMostK(k) - atMostK(k-1)
        #   - "At most" IS monotonic under shrinking:
        #       a valid window stays valid when shrunk inward
        #   - Expand right to add new nums
        #   - Shrink from left while window is invalid (too many distinct nums)
        #   - Every starting index from [left, right] forms a valid subarray
        #     ending at right, since removing nums from the left keeps it valid
        #   - So add right - left + 1 to result at each right

        # Count of subarrays with at most currK distinct nums
        def atMostK(nums, currK):

            n = len(nums)

            # HashMap: num -> frequency in current window
            # sc: O(k)
            count = defaultdict(int)

            # Distinct nums in current window
            uniqueCount = 0

            # Sliding Window Boundaries
            left = 0
            right = 0

            # Total valid subarrays with at most currK distinct nums
            subarrayCount = 0

            # tc: O(n)
            while right < n:

                # Add right num to window
                rightNum = nums[right]

                # Check if a new distinct num was found
                if count[rightNum] == 0:
                    uniqueCount += 1

                count[rightNum] += 1

                # Shrink from left while window is invalid:
                # - more than currK distinct nums
                # tc: O(1) amortized
                while uniqueCount > currK:

                    # Remove left num from window
                    leftNum = nums[left]
                    count[leftNum] -= 1

                    # Check if a distinct num was lost
                    if count[leftNum] == 0:
                        uniqueCount -= 1

                    # Shrink window
                    left += 1

                # Include all valid subarrays ending at right:
                # [left..right] is the largest valid window ending at right,
                # and shrinking it keeps it valid, so every start from
                # left to right works
                #
                # nums = [1, 2, 1, 3], currK = 2, right = 2, left = 0
                # starts 0, 1, 2 are valid:
                #   [1, 2, 1], [2, 1], [1]
                # so there are right - left + 1 = 2 - 0 + 1 = 3 valid subarrays
                #
                # Note: this is the mirror of "+= left" (at least k):
                #   at least k: valid starts are 0 ... left-1     -> left
                #   at most k:  valid starts are left ... right   -> right - left + 1
                #
                # tc: O(1)
                subarrayCount += right - left + 1

                # Iterate window
                right += 1

            # tc: O(n)
            # sc: O(k)
            return subarrayCount

        # atMostK(k):   counts all subarrays with <= k distinct nums
        # atMostK(k-1): counts all subarrays with <= k-1 distinct nums
        # Subtracting leaves only subarrays with exactly k distinct nums
        res = atMostK(nums, k) - atMostK(nums, k - 1)

        # overall: tc O(n)
        # overall: sc O(k)
        return res

Solution 2: [Sliding Window] Single Pass, Single Map, Two-Phase Shrink (Optimized) - Sliding Window/Variable Size Window

    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window) — Single Pass, Single Map

        # Window Representation:
        #   - Maintain one window [left, right] with one frequency map
        #   - distinct: number of distinct values currently in window
        #   - prefix: number of "free" extra left positions that also
        #     give exactly k distinct, beyond the minimal left boundary

        # Idea (Two-Phase Shrink):
        #   Phase 1 — forced shrink while distinct > k (standard invalid shrink):
        #     moves left until window has at most k distinct; resets prefix
        #     once, after shrinking, since the window boundary genuinely changed
        #   Phase 2 — opportunistic shrink while distinct == k:
        #     keep removing nums[left] as long as its frequency > 1
        #     (removing it won't drop distinct count below k)
        #     each such removal is one more valid left boundary -> prefix += 1

        # Why Phase 2 must run EVERY iteration where distinct == k:
        #   Adding a DUPLICATE value at right increases that value's frequency
        #   inside the window — which can retroactively make the current left
        #   element removable, even though `distinct` itself didn't change.
        #   So Phase 2 must re-attempt shrinking every time distinct == k,
        #   not just the first time k is reached.

        # Window Example:
        #   nums = [1, 2, 1, 2, 3], k = 2
        #
        #   right=0 (1): distinct=1, not k, ans += 0
        #   right=1 (2): distinct=2 == k, freq[nums[left]]=freq[1]=1, not >1, no shrink
        #                ans += prefix(0) + 1 = 1         -> ans=1
        #   right=2 (1, dup): distinct stays 2, freq[nums[left=0]]=freq[1]=2 > 1, shrink:
        #                left=1, prefix=1
        #                ans += prefix(1) + 1 = 2          -> ans=3
        #   right=3 (2, dup): distinct stays 2, freq[nums[left=1]]=freq[2]=2 > 1, shrink:
        #                left=2, prefix=2
        #                ans += prefix(2) + 1 = 3           -> ans=6
        #   right=4 (3, new): distinct=3 > k, Phase 1 shrink:
        #                remove nums[2]=1, freq[1]->0, distinct=2, left=3, prefix=0
        #                Phase 2: freq[nums[left=3]]=freq[2]=1, not >1, no shrink
        #                ans += prefix(0) + 1 = 1            -> ans=7
        #
        #   ans = 7  (matches expected output for this input)

        n = len(nums)

        # Frequency of each value in current window
        # sc: O(k)
        freq = defaultdict(int)

        # Sliding Window Boundary
        left = 0

        # Number of extra "free" valid left starts for the current right
        prefix = 0

        # Number of distinct values in window
        distinct = 0

        # Total valid subarrays found
        ans = 0

        # tc: O(n)
        for right in range(n):

            # Check if new distinct value found
            if freq[nums[right]] == 0:
                distinct += 1
            freq[nums[right]] += 1

            # Phase 1 — Shrink from left while window is invalid:
            # - too many distinct values (distinct > k)
            # - boundary genuinely moved, so reset prefix once after shrinking
            # tc: O(1) amortized
            if distinct > k:
                while distinct > k:
                    freq[nums[left]] -= 1

                    if freq[nums[left]] == 0:
                        distinct -= 1

                    left += 1
                prefix = 0

            # Phase 2 — window has exactly k distinct:
            # opportunistically shrink further while it doesn't cost distinctness
            # tc: O(1) amortized
            while distinct == k and freq[nums[left]] > 1:
                freq[nums[left]] -= 1
                left += 1
                prefix += 1

            # All left positions from (left - prefix) to left are valid starts
            if distinct == k:
                ans += prefix + 1

        # overall: tc O(n)
        # overall: sc O(k)
        return ans

1248. Count Number of Nice Subarrays ::3:: - Medium

Topics: Array, Hash Table, Math, Sliding Window, Prefix Sum

Intro

Given an array of integers nums and an integer k. A continuous subarray is called nice if there are k odd numbers on it. Return the number of nice sub-arrays.

Example InputOutput
nums = [1,1,2,1,1], k = 32
nums = [2,4,6], k = 10

Constraints:

1 ≤ nums.length ≤ 50000

1 ≤ nums[i] ≤ 10^5

1 ≤ k ≤ nums.length

Abstraction

Given an array and a variable size window, count the subarrays that contain exactly k odd numbers.

(Not monotonic: extending a valid window can add a (k+1)th odd number and break it, so left / n - right do NOT apply)

(Split: exactly k = atMost(k) - atMost(k-1))

(atMost is monotonic under shrinking: a valid window stays valid when shrunk inward) (expand right, shrink while invalid, too many odd numbers)

(Count starts for each end, after the shrink loop) right - left + 1 = "how many starts can I slide forward (toward right) and still be valid?"

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] Two Pointer Sliding Window atMost(k) minus atMost(k minus 1), Add Right Minus Left Plus 1 (Count Starts For Each End) [SC Opt] - Sliding Window/Variable Size Window

    def numberOfSubarrays(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [left, right]
        #   - oddCount tracks number of odd nums in current window
        #   - Window is valid when it contains at most currK odd nums

        # Idea:
        #   - "Exactly k" is not monotonic under extension, so left / n - right do NOT apply
        #   - Split it into two "at most" problems:
        #       exactly k = atMost(k) - atMost(k-1)
        #   - "At most" IS monotonic under shrinking:
        #       a valid window stays valid when shrunk inward
        #   - Expand right to add new nums
        #   - Shrink from left while window is invalid (too many odd nums)
        #   - Every starting index from [left, right] forms a valid subarray
        #     ending at right, since removing nums from the left keeps it valid
        #   - So add right - left + 1 to result at each right

        # Why the split works:
        #   - atMost(k)   counts subarrays with 0, 1, 2, ... k odds
        #   - atMost(k-1) counts subarrays with 0, 1, 2, ... k-1 odds
        #   - subtracting leaves only subarrays with exactly k odds

        # Window Example:
        #   nums = [1, 1, 2, 1, 1], k = 3
        #
        #   atMost(3): all windows with oddCount <= 3
        #   [1, 1, 2, 1, 1]
        #    LR              oddCount=1, res += 1
        #    L  R            oddCount=2, res += 2
        #    L     R         oddCount=2, res += 3
        #    L        R      oddCount=3, res += 4
        #    L           R   oddCount=4 > 3, shrink -> left=1, oddCount=3, res += 4
        #   atMost(3) = 14
        #
        #   atMost(2): all windows with oddCount <= 2
        #   [1, 1, 2, 1, 1]
        #    LR              oddCount=1, res += 1
        #    L  R            oddCount=2, res += 2
        #    L     R         oddCount=2, res += 3
        #    L        R      oddCount=3 > 2, shrink -> left=1, oddCount=2, res += 3
        #       L        R   oddCount=3 > 2, shrink -> left=2, oddCount=2, res += 3
        #   atMost(2) = 12
        #
        #   exactly(3) = atMost(3) - atMost(2) = 14 - 12 = 2
        #   valid subarrays: [1,1,2,1] and [1,1,2,1,1]

        # Count of subarrays with at most currK odd nums
        def atMost(currK: int) -> int:

            n = len(nums)

            # Sliding Window Boundaries
            left = 0
            right = 0

            # Odd nums in current window
            # sc: O(1)
            oddCount = 0

            # Total valid subarrays with at most currK odd nums
            subarrayCount = 0

            # tc: O(n)
            while right < n:

                # Add right num to window, check if odd
                if nums[right] % 2 == 1:
                    oddCount += 1

                # Shrink from left while window is invalid:
                # - more than currK odd nums
                # tc: O(1) amortized
                while oddCount > currK:

                    # Remove left num from window, check if odd
                    if nums[left] % 2 == 1:
                        oddCount -= 1

                    # Shrink window
                    left += 1

                # Include all valid subarrays ending at right:
                # [left..right] is the largest valid window ending at right,
                # and shrinking it keeps it valid, so every start from
                # left to right works
                #
                # nums = [1, 1, 2, 1, 1], currK = 3, right = 3, left = 0
                # starts 0, 1, 2, 3 are valid:
                #   [1, 1, 2, 1], [1, 2, 1], [2, 1], [1]
                # so there are right - left + 1 = 3 - 0 + 1 = 4 valid subarrays
                #
                # Note: this is the mirror of "+= left" (at least k):
                #   at least k: valid starts are 0 ... left-1     -> left
                #   at most k:  valid starts are left ... right   -> right - left + 1
                #
                # tc: O(1)
                subarrayCount += right - left + 1

                # Iterate window
                right += 1

            # tc: O(n)
            # sc: O(1)
            return subarrayCount

        # atMost(k):   counts all subarrays with <= k odd nums
        # atMost(k-1): counts all subarrays with <= k-1 odd nums
        # Subtracting leaves only subarrays with exactly k odd nums
        res = atMost(k) - atMost(k - 1)

        # overall: tc O(n)
        # overall: sc O(1)
        return res

Solution 2: [Sliding Window] Single Pass With Start Gap [TC Opt] - Sliding Window/Variable Size Window

    def numberOfSubarrays(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window)

        # Window Representation:
        #   - Maintain window [lp, rp]
        #   - counter: number of odd numbers in current window
        #   - start_gap: number of valid left boundaries for current window

        # Idea:
        #   - Expand right to add new elements
        #   - Once counter == k, shrink from left counting how many positions
        #     keep counter == k (each is a valid left boundary)
        #   - start_gap persists across iterations: once we've found valid
        #     left boundaries for a window with k odds, any future right
        #     expansion that doesn't add another odd number still has the
        #     same valid left boundaries
        #   - Add start_gap to total at every right, not just when counter == k

        # Why start_gap persists:
        #   nums = [1, 1, 2, 1, 1], k = 3
        #
        #   rp=3 (odd): counter=3 == k, start_gap reset to 0
        #   [1, 1, 2, 1, 1]
        #    L           R
        #   shrink: lp=0 (odd) -> counter=2, start_gap=1
        #   [1, 1, 2, 1, 1]
        #       L        R
        #   counter != k, stop shrinking. total += 1
        #
        #   rp=4 (odd): counter=3 == k, start_gap reset to 0
        #   [1, 1, 2, 1, 1]
        #       L           R
        #   shrink: lp=1 (odd) -> counter=2, start_gap=1
        #   [1, 1, 2, 1, 1]
        #          L        R
        #   counter != k, stop shrinking. total += 1
        #
        #   total = 2
        #   valid subarrays: [1,1,2,1] and [1,1,2,1,1]

        # Single pass — no need to call atMost twice

        # Sliding Window Boundaries
        # sc: O(1)
        lp = 0

        # Number of odd numbers in current window
        counter = 0

        # Number of valid left boundaries for current window
        start_gap = 0

        # Total valid subarrays found
        total = 0

        # tc: O(n)
        for rp in range(len(nums)):

            # Add incoming element to window
            # tc: O(1)
            if nums[rp] % 2 != 0:
                counter += 1

            # Once counter == k, count valid left boundaries
            # Reset start_gap since the new odd resets the boundary count
            # tc: O(1) amortized
            if counter == k:
                start_gap = 0

                # Shrink from left while window is valid:
                # - counter == k
                # Each shrink that keeps counter == k is another valid left boundary
                while counter == k:
                    start_gap += 1
                    if nums[lp] % 2 != 0:
                        counter -= 1
                    lp += 1

            # start_gap persists: even if right adds an even number,
            # the same left boundaries remain valid from the previous valid window
            # tc: O(1)
            total += start_gap

        # overall: tc O(n)
        # overall: sc O(1)
        return total

Solution 3: [Sliding Window] Deque + Threshold (Reference: index arithmetic variant) - Sliding Window/Variable Size Window

    def numberOfSubarrays(self, nums: List[int], k: int) -> int:

        # Sliding Window (Variable Size Window) + Deque

        # Window Representation:
        #   - ones_indicies: deque holding indices of the last k odd numbers seen
        #                    (once k odds are tracked, the oldest odd index gets evicted)
        #   - threshold: index of the odd number just evicted (the boundary
        #                marking how far left a valid window could start)
        #   - count_ones: number of odd numbers currently tracked (capped at k)

        # Idea:
        #   - Scan right pointer j across nums
        #   - Every time we see an odd number, record its index
        #   - If k odds are already tracked, evict the oldest odd index into
        #     threshold and drop count_ones by 1 (keeps at most k odds tracked)
        #   - Whenever count_ones == k, the window ending at j has exactly k odds;
        #     the number of valid left boundaries is the gap between the
        #     first tracked odd index and threshold
        #   - Add that gap to total at every j where count_ones == k
        #     (same idea as start_gap in Solution 2, computed via index
        #     arithmetic instead of a running counter)

        # Why ones_indicies[0] - threshold works:
        #   - threshold marks the last position that, if included in the window,
        #     would push the odd count above k
        #   - Any left boundary in (threshold, ones_indicies[0]] keeps exactly
        #     k odds in [left, j]: starting any earlier includes one more odd
        #     (pushing past k), and starting later than the first tracked odd
        #     doesn't change the odd count
        #   - So there are (ones_indicies[0] - threshold) valid left starts

        # Window Example:
        #   nums = [1, 1, 2, 1, 1], k = 3
        #
        #   j=0 (odd): ones=[0], count_ones=1
        #   j=1 (odd): ones=[0,1], count_ones=2
        #   j=2 (even): count_ones still 2, skip
        #   j=3 (odd): ones=[0,1,3], count_ones=3 == k
        #              total += ones[0] - threshold = 0 - (-1) = 1   -> total=1
        #   j=4 (odd): count_ones == k already, evict oldest:
        #              threshold = ones.popleft() = 0, count_ones=2
        #              append 4 -> ones=[1,3,4], count_ones=3
        #              total += ones[0] - threshold = 1 - 0 = 1      -> total=2
        #
        #   total = 2
        #   valid subarrays: [1,1,2,1] and [1,1,2,1,1]

        # Total valid subarrays found
        # sc: O(1)
        total = 0

        # Number of odd numbers currently tracked (capped at k)
        # sc: O(1)
        count_ones = 0

        # Indices of the last (at most) k odd numbers seen
        # sc: O(k)
        ones_indicies = deque()

        # Index just left of the earliest odd number still valid as a
        # window start; -1 means no odd has been evicted yet
        # sc: O(1)
        threshold = -1

        # tc: O(n)
        for j in range(len(nums)):

            # Track index of odd numbers only
            # tc: O(1)
            if nums[j] % 2 == 1:
                ones_indicies.append(j)

                # Already have k odds tracked - evict the oldest one
                # so the deque never holds more than k indices
                # tc: O(1) amortized
                if count_ones == k:
                    threshold = ones_indicies.popleft()
                    count_ones -= 1

                count_ones += 1

            # Window ending at j has exactly k odds tracked -
            # count how many left boundaries keep it that way
            # tc: O(1)
            if count_ones == k:
                total += ones_indicies[0] - threshold

        # overall: tc O(n)
        # overall: sc O(k)
        return total

1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit ::1:: - Medium

Topics: Array, Queue, Sliding Window, Heap (Priority Queue), Ordered Set, Monotonic Queue

Intro

Given an array of integers nums and an integer limit, return the size of the longest non-empty subarray such that the absolute difference between any two elements of this subarray is less than or equal to limit.

Example InputOutput
nums = [8,2,4,7], limit = 42
nums = [10,1,2,4,7,2], limit = 54
nums = [4,2,2,2,4,4,2,2], limit = 03

Constraints:

1 ≤ nums.length ≤ 10^5

1 ≤ nums[i] ≤ 10^9

0 ≤ limit ≤ 10^9

Abstraction

Given an array, and a variable size window, find the longest window that has an absolute difference between any two elements less than or equal to limit.

(expand while valid, shrink while invalid)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] [Monotonic] [Deque] Two Pointer Sliding Window - Sliding Window/Variable Size Window

    def longestSubarray(self, nums: List[int], limit: int) -> int:
        
        # Sliding Window + Monotonic Deques
    
        # Window Representation:
        #   - Max and min deque track max and min for current window
        #   - Window boundaries kept via [left, right]
        #   - Valid window has absolute diff less <= limit

        # Idea:
        #   - Expand variable size window over list by extending right
        #   - 
        #   - Maintain two deques for the cu:
        #       max_d: decreasing deque for maximum
        #       min_d: increasing deque for minimum
        #   - Expand right pointer; shrink left if max - min > limit.
        #   - Track maximum window length.
        
        # Rules:
        #   1. Append nums[right] to both deques, maintaining monotonic quality
        #   2. while current window violates limit (max - min > limit), shrink from left
        #   3. Update maximum window length

        # Sliding Window Data
        # sc: O(n)
        maxDeque = deque()  # stores elements in decreasing order
        minDeque = deque()  # stores elements in increasing order

        # Window Representation:
        # sc: O(1)
        left = 0
        right = 0

        # Current max length
        maxLen = 0
        n = len(nums)

        # Iterate over window 
        while right < n:
            
            # Add new num to window
            rightNum = nums[right]

            # Maintain Monotonic Decreasing max deque
            while maxDeque and rightNum > maxDeque[-1]:
                maxDeque.pop()
            maxDeque.append(rightNum)

            # Maintain Monotonic increasing min deque
            while minDeque and rightNum < minDeque[-1]:
                minDeque.pop()
            minDeque.append(rightNum)

            # No need to use abs()
            # since minDeque[0] <= maxDeque[0]
            # so result will always be positive
            difference = maxDeque[0] - minDeque[0]

            # Shrink from left while window is invalid:
            # - difference exceeds limit
            while limit < difference:

                # if current max is the left most num, remove from maxDeque from window
                if nums[left] == maxDeque[0]:
                    maxDeque.popleft()
                
                # if current min is the left most num, remove from minDeque from window
                if nums[left] == minDeque[0]:
                    minDeque.popleft()

                # Shrink window
                left += 1

                # grab new difference
                difference = maxDeque[0] - minDeque[0]

            # Update maximum window length
            maxLen = max(maxLen, right - left + 1)

            # Iterate window 
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return maxLen

862. Shortest Subarray with Sum at Least K ::1:: - Hard

Topics: Array, Binary Search, Queue, Sliding Window, Heap (Priority Queue), Prefix Sum, Monotonic Queue

Intro

Given an integer array nums and an integer k, return the length of the shortest non-empty subarray of nums with a sum of at least k. If there is no such subarray, return -1. A subarray is a contiguous part of an array.

Example InputOutput
nums = [1], k = 11
nums = [1,2], k = 4-1
nums = [2,-1,2], k = 33

Constraints:

1 ≤ nums.length ≤ 10^5

-10^5 ≤ nums[i] ≤ 10^5

1 ≤ k ≤ 10^9

Abstraction

Given an array, and a variable size window, find the smallest window that has a sum greater than or equal to k.

(expand while invalid, shrink while valid)

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Sliding Window] [Monotonic] [Deque] Prefix Sum With Monotonic Deque - Sliding Window/Variable Size Window

    def shortestSubarray(self, nums: List[int], k: int) -> int:

        # Prefix Sum + Monotonic Increasing Deque

        # Representation:
        #   - Prefix sum for each index in the array
        #   - Subarray sum [i, j] = P[j+1] - P[i]
        #   - Deque stores indices of monotonic increasing prefix sums

        # Idea:
        #   - We want shortest subarray with sum >= k
        #   - Equivalent to finding shortest j - i where P[j] - P[i] >= k
        #   - Maintain monotonic increasing deque of prefix sum indices
        #   - For each j, pop from front when P[j] - P[dq[0]] >= k
        #     (valid subarray found, pop to find even shorter one)
        #   - Pop from back when P[j] <= P[dq[-1]]
        #     (current prefix is smaller, back element can never be a useful left boundary)

        # Why not standard sliding window?
        #   - nums can contain negative numbers
        #   - Expanding window doesn't guarantee increasing sum
        #   - Standard two pointer breaks down with negatives
        #   - Prefix sum + monotonic deque handles negatives correctly

        # Deque Representation:
        #   - Monotonic Increasing order means:
        #     Low -> High
        #
        #   left (low)               right (high)
        #   [ P[2], P[4], P[5], P[7] ]  prefix sums always increasing
        #   [ idx=2, idx=4, idx=5, idx=7 ]  indices always increasing

        n = len(nums)

        # Prefix sum array
        # sc: O(n)
        p = [0] * (n + 1)

        # Build Prefix Sum Array:
        # p[i] = sum of nums[0] to nums[i-1]
        # p[i] = prefixSum of previous index + previous index's value
        #      = p[i-1] + nums[i-1]
        # tc: O(n)
        for i in range(1, n+1):
            p[i] = p[i-1] + nums[i-1]

        # Monotonic Increasing Deque:
        #        
        # dq = [dq[0], dq[1], dq[2], ..., dq[-1]]
        #        ^                           ^
        #  older (left most) index    newer (right most) index
        #      small p[i]                  large p[]

        # sc: O(n)
        dq = deque()

        # Min valid subarray length:
        # set to larger than array to say we have not found a valid array yet
        minLen = float('inf')

        right = 0

        # Need to check up to nth prefix sum (indices 0 through n, n+1 total prefix sums)
        # tc: O(n)
        while right <= n:

            # Shrink from left while window is valid:
            #   - dq[0]: index of the oldest, smallest prefix sum still in the deque
            #   - p[i]: prefix sum up to (but not including) the current index i (sum of nums[0..i-1])
            #   - k: min sum required
            #   - Take the difference between the prefix sums to get the sum of the '+' array in between the indexes
            #
            #   [ --------- +++++++ xxxxxxxxx ]
            #           dq[0]       i
            #                     p[i]
            #
            #   p[i] - dq[0] => results in sum of the '+' area

            # tc: O(1) amortized
            while dq and k <= p[right] - p[dq[0]]:

                # Valid window, compare to min:

                # Grab left index of window
                left = dq[0]

                # Compute length of window:
                #   - no need for (right - left + 1)
                #   - prefixSum array is already 1-indexed relative to nums and accounts for the +1
                subarrayLen = right - left

                # Compare to min
                if subarrayLen < minLen:
                    minLen = subarrayLen

                # Shrink window
                dq.popleft()

            # Maintain Monotonic Increase:
            #   - remove the most recently added prefix sum that are larger than the current prefix
            #   - append newest large prefix sum to deque
            # tc: O(1) amortized
            while dq and p[right] <= p[dq[-1]]:
                dq.pop()

            # Extend window
            dq.append(right)
            right += 1

        # No valid window found
        if minLen == float('inf'):
            return -1

        # overall: tc O(n)
        # overall: sc O(n)
        return minLen

239. Sliding Window Maximum ::3:: - Hard

Topics: Array, Queue, Sliding Window, Heap (Priority Queue), Monotonic Queue

Intro

You are given an array of integers nums, there is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves right by one position. Return the max sliding window.

Example InputOutput
nums = [1,3,-1,-3,5,3,6,7], k = 3Output: [3,3,5,5,6,7]
nums = [1], k = 1[1]

Constraints:

1 ≤ nums.length ≤ 105

-104 ≤ nums[i] ≤ 104

1 ≤ k ≤ nums.length

Abstraction

Given list of numbers, and sliding window size k, return array of max element for each window.

Pseudocode

Sol 3: Fixed Size K Window With Indices Monotonic Decreasing Dequeue
1. (n = len(nums))
2. (right = 0)
2. (res = [])
3. (dq = deque())
4. while right < n:
    a. while dq and nums[dq[-1]] < nums[right]:
         dq.pop()
    dq.append(right)
    left = right - k + 1
    if not left <= dq[0] <= right:
         dq.popleft()
    if k - 1 <= right:
         res.append(nums[dq[0]])
    right += 1
5. return res

Solution 1: [Sliding Window] [Heap] Fixed Size K Window With MaxHeap - Sliding Window/Fixed Size Window

    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:

        # Sliding Window (Fixed Size Window)

        # Window Representation:
        #   - Maintain window of size k [left, right]
        #   - MinHeap will store (-value, index), which turns it into a MaxHeap
        #   - MaxHeap top element will always hold the window maximum

        # Idea:
        #   - Push first k elements into heap
        #   - Iterate fixed window over array:
        #       1. Add right as new element
        #       2. Remove left as old element (index <= (right - k))
        #       3. Heap top is max of current window

        # Empty check:
        # tc: O(1)
        if not nums or k == 0:
            return []

        n = len(nums)

        # List of max values for each window
        # sc: O(n)
        result = []

        # MaxHeap():
        #   - a Python minHeap with negative values, resulting in a maxHeap
        #   - O(log n) insertion and removal 

        # MaxHeap data representation: (-value, index)
        #   - Need the -value to turn MinHeap into MaxHeap 
        #   - Need the index to check if top of heap is outside window
        maxHeap = []  

        # MaxHeap top representation (maxHeap[0]):
        #   - Top: largest element index in current window
        #   - All other: no guarantee for any other element, 
        #              we are only guaranteed that the top is the current max in the MaxHeap
        # 

        # Sliding Window Variables
        # sc: O(1)
        right = 0

        # Initialize first window:
        # push first k elements to maxHeap
        # tc: O(k)
        while right < k:
            
            # tc: O(log k)
            heapq.heappush(maxHeap, (-nums[right], right))

            # Expand substring
            # tc: O(1)
            right += 1

        # MaxHeap:
        # root of maxHeap now holds the max of the current window,
        # so its holding the max for the first window,
        # we append to the result list
        peekMax = -maxHeap[0][0]
        result.append(peekMax)

    
        # tc: O(n)
        while right < n:

            # Expand window
            newElem = nums[right]

            # Push new element: (-value, index)
            heapq.heappush(maxHeap, (-newElem, right))

            # Index Distance No More Than k Apart (not element count): 
            # The distance between left and right indices in the current window is right - left
            left = right - k

            # Keep removing top of maxHeap if it is stale and outside window
            # tc: O(1)
            while not left <= maxHeap[0][1] <= right:
                heapq.heappop(maxHeap)

            # MaxHeap:
            # root of maxHeap now holds the max of the current window,
            # we append to the result list
            peekMax = -maxHeap[0][0]
            result.append(peekMax)

            # Expand substring
            # tc: O(1)
            right += 1

        # overall: tc O(n log k)
        # overall: sc O(n)
        return result

Solution 2: [Dynamic Programming] K Block Partitions Shifted L To R and R to L Tracking Grabbing Leftmost and Rightmost For Each Window Iteration [TC Opt] - Sliding Window/Fixed Size Window

    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:

        # Sliding Window (Fixed Size Window)

        # Substring Representation:
        #   - Partition array into blocks of size k
        #   - leftMax[i]  = max from block start to i
        #   - rightMax[i] = max from block end to i (reverse scan)

        # Empty check:
        if not nums or k == 0:
            return []

        # We will split the original nums array into k sized blocks:
        # index:   0  1  2    3  4  5    6  7
        # nums:    1  3 -1   -3  5  3    6  7
        # blocks: [------]  [------]   [----]
        #           B0         B1        B2

        # Blocks are based only on index position and k size
        # They are not based on the direction of the scan 

        n = len(nums)

        # L to R Max Values, to L Max Values
        leftMax = [0] * n
        rightMax = [0] * n

        # Fill leftMax: find the running max on a per block basis L to R
        for i in range(n):

            # Start of new block: max is first element of block
            if i % k == 0:
                leftMax[i] = nums[i]

            # In Middle of block: compare currNum to prevMax
            else:
                leftMax[i] = max(leftMax[i-1], nums[i])

        # Fill rightMax: find the running max on a per block basis R to L
        for i in range(n-1, -1, -1):

            # Start of new block: max is first element of block
            if (i+1) % k == 0 or i == n-1:
                rightMax[i] = nums[i]

            # In Middle of block: compare currNum to prevMax
            else:
                rightMax[i] = max(rightMax[i+1], nums[i])

    
        # Compute sliding fixed window max 
        res = []
        left = 0
        right = k - 1

    
        # -----------------------------
        # Combining Precomputed with Fixed Window:
        # we are iterating a fixed window of size k:

        # Case 1: fixed window fully inside block
        #
        #   [  Window   ]
        #   [   BLOCK   ]
        #  

        # Case 2: fixed window spans 2 blocks
        #
        #     [  Window   ]
        # [ BLOCK A ][ BLOCK B ]


        # -----------------------------
        # Grabbing Farthest Data Available To Use From Left and Right:

        # Case 1: fixed window fully inside block
        #
        #   [  Window   ]
        #   [   BLOCK   ]
        #   ^           ^
        #   R           L

        # Case 2: fixed window spans 2 blocks
        #
        #     [  Window   ]
        # [ BLOCK A ][ BLOCK B ]
        #     ^           ^
        #     R           L

        # -----------------------------
        # Window Max Coverage:
        #       R = index max from iterating Right to Left (<---)
        #       L = index max from iterating Left to Right (--->)

        # Case 1: Entire Window Is Accounted For
        #
        #   [  Window   ]
        #   [   BLOCK   ]
        #   ^           ^
        #   R --------- L        
        #  (<---)     (--->)

        # Case 2: Entire Window Is Accounted For
        #
        #     [  Window   ]
        # [ BLOCK A ][ BLOCK B ]
        #     ^           ^
        #     R --------- L        
        #    (<---)     (--->)
        
        # tc: iterate over nums array O(n)
        while right < n:

            # Re: see above diagrams
            res.append(max(rightMax[left], leftMax[right]))
            left += 1
            right +=1

        # overall: tc O(n)
        # overall: sc O(n)
        return res

Solution 3: [Sliding Window] [Monotonic] [Deque] Fixed Size K Window With Indices Monotonic Decreasing Deque Max On Left MRI Min On Right Safe For Duplicates [TC Opt] - Sliding Window/Fixed Size Window

    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:

        # Sliding Window (Fixed Size Window)

        # Substring Representation
        #   - Maintain window of size k [left, right]
        #   - Monotonic Decreasing Deque storing indices within window
        #   - Front of deque always holds max element index
        #   - Back of deque always holds the min element index
        #   - Back of deque always holds the most recently added index

        # Idea:
        #   - Remove elements smaller than incoming element from the back of the deque
        #   - Remove max front element if it exits window
        #   - Front of deque will always window max

        # Safe For Duplicates:
        #   - Stores indices instead of values
        #   - When removing outgoing element, we compare by index not value
        #   - Each index is unique, so we always know exactly which element is leaving
        #   - Duplicate values are handled correctly since we never confuse two
        #     elements at different positions

        # Deque():
        #   - a double ended queue
        #   - O(1) insertion and removal from both sides

        # Deque left and right ends representation:
        #   - Front Left dq[0]: largest element index in current window
        #   - Back Right dq[-1]: most recently added index, min element in window

        # Monotonic decreasing order means:
        #   High -> Low

        # left (high)             right (low)
        # [ idx=2,  idx=4,  idx=5,  idx=7 ]  Our Queue: Monotonic Increasing Indexes
        # [ val=8,  val=6,  val=3,  val=1 ]  We Can Access: Values always Monotonic Decreasing

        # Empty check:
        if not nums or k == 0:
            return []

        n = len(nums)

        right = 0

        # Max element list for each window
        # sc: O(n)
        res = []

        # stores indices in monotonic decreasing order of their values
        # sc: O(k)
        dq = deque()

        # tc: O(n)
        while right < n:

            # Monotonic Property:
            # dq[0]: current max element in deque (front of deque)
            # dq[-1]: current min element in deque (back of deque)  <-- pop this one
            # remove any elements smaller than the current new element,
            # so that the new element ends up at the back of the deque,
            # and we keep the Monotonic property dq[0] High -> Low dq[-1]
            # tc: O(1) amortized
            while dq and nums[dq[-1]] < nums[right]:
                dq.pop()

            # Add INDEX to be the new back of deque: dq[-1] => index
            dq.append(right)

            # Sliding Window Single Iteration:
            # since at most, we are only adding 1 new element
            # and since we have a fixed window size,
            # only 1 element can go stale (end up out of bounds of the window),
            # however, we will only check the top of the deque for stale indexes
            
            # Monotonic Trick:
            # Since we keeping both:
            #   - Monotonic High -> Low
            #   - Iterating by 1 Index at a time

            # left (high)             right (low)
            # [ idx=2,  idx=4,  idx=5,  idx=7 ]  Our Queue: Monotonic Increasing Indexes
            # [ val=8,  val=6,  val=3,  val=1 ]  We Can Access: Values always Monotonic Decreasing

            # So we only have to check if the highest value,
            # which by definition will have the lowest index (oldest value in the deque)
            # is now out of bounds of the fixed window

            # Element Count of Max k (not index distance):
            # left = right - (k-1): the leftmost index needed so the window
            # spans exactly k elements (fencepost: k elements = k-1 steps back)
            # left is INSIDE the window (first valid index),
            # calculate NEW left distance after adding currNum
            left = right - k + 1

            # Keep removing top of deque if it is stale and outside window
            # dq[0]: current max element in deque (front of deque)  <-- pop this one
            # dq[-1]: current min element in deque (back of deque)
            if not left <= dq[0] <= right:
                dq.popleft()

            # Left K Fixed Window:
            # We generated left from the fixed window size k,
            # so we cannot use it in a right - left + 1 
            # as this would just always give us k,
            # instead check if right has passed the kth index
            if 0 <= left:

                # Deque front now holds max valid index within the current window,
                # now we only want to record once we have reached our fixed window full length,
                # to ensure we have grabbed all the elements we need
                res.append(nums[dq[0]])

            # Iterate window:
            right += 1

        # overall: tc O(n)
        # overall: sc O(n)
        return res